symfonic.capabilities.memory.schedule¶
schedule ¶
When consolidation runs, and what it runs — three cadences, one cursor.
The shipped system schedules exactly one cadence in-process (quick_nap,
every N turns) and leaves the full roster to whatever cron the adopter owns.
The consequence is documented in the shipped code itself: a phase that lived
only in the full roster was dead configuration for every adopter without a
scheduler, and had to be added to the quick roster after the fact.
So the cadence is a value here, not a call site:
- quick — the every-N-turns nap. Cheap enough to run inside a turn.
Transcribes legacy's
quick_nap. - nightly — a profile this capability defines: the full roster without entity extraction, for a quiet window that should not pay for it. Nothing in the shipped system runs this one.
- deep — Deep Sleep: roster parity with the shipped consolidator plus
scope_promotion. The widest, including every phase that costs a model call, on a much longer period.
Legacy has two rosters and not three: nightly_nap calls run, so
"nightly" and "Deep Sleep" are the same sixteen phases on two schedules. The
middle cadence here is new, and saying so is what keeps DEEP the one that
answers for parity.
Two rules make the cursor safe to persist and safe to resume:
- One cycle per tick, the widest that is due. Running quick and nightly in the same tick would run the quick phases twice — every counter doubled, every idempotency claim tested for no reason.
- A wider cycle satisfies the narrower ones. Deep Sleep did the quick phases; leaving the turn counter untouched would fire a quick nap on the very next turn.
ConsolidationCycle ¶
Bases: StrEnum
The three consolidation cadences, narrowest first.
ConsolidationSchedule
dataclass
¶
ConsolidationSchedule(quick_every_turns: int = 5, nightly_after_seconds: float = 24 * 60 * 60, deep_after_seconds: float = 7 * 24 * 60 * 60)
The cadences, and the rule that picks one.
due ¶
due(cursor: ScheduleCursor, *, now: datetime | None = None, among: Sequence[ConsolidationCycle] | None = None) -> ConsolidationCycle | None
The widest cycle due at now, or None.
among narrows the answer to cadences the caller can actually run.
It is not a convenience: a cadence that has never run reads as
infinitely overdue, so on a fresh deployment every cadence is due on
turn one — and a turn-driven nap asked for the widest would be told to
run Deep Sleep, which is the roster it was never composed for and the
cost nobody scheduled. A caller that runs one cadence asks about one.
Source code in src/symfonic/capabilities/memory/schedule.py
validate ¶
Refuse a schedule under which a cadence can never fire.
Source code in src/symfonic/capabilities/memory/schedule.py
ScheduleCursor
dataclass
¶
ScheduleCursor(turns_since_quick: int = 0, last_quick_at: datetime | None = None, last_nightly_at: datetime | None = None, last_deep_at: datetime | None = None)
What has run so far. The persisted half of the schedule.
completed ¶
Record cycle as run, satisfying every narrower cadence too.
Source code in src/symfonic/capabilities/memory/schedule.py
from_state
classmethod
¶
Read a persisted cursor, tolerating one written by an older build.
Missing keys default rather than raise: a cursor written before Deep Sleep existed is a valid cursor with no deep run behind it, and refusing it would make a rollback forward-incompatible.
Source code in src/symfonic/capabilities/memory/schedule.py
to_state ¶
The persisted form: ISO-8601 timestamps, like every other record.