symfonic.capabilities.memory.consolidation¶
consolidation ¶
Deliverable 3: one runtime behind quick nap, nightly nap, and Deep Sleep.
The shipped consolidator is a 600-line method per cadence with the roster
inlined: run names its phases in sequence, quick_nap re-implements a
four-phase subset, and nightly_nap calls run while temporarily
mutating instance state to apply per-call overrides. Adding a phase means
editing every cadence that should have it, and forgetting one is invisible
until an adopter reports that a configuration flag does nothing.
Here the cadence is a roster (:data:PHASE_ROSTER) and the runtime is the
thing that executes one. The properties that make a half-finished run safe are
the runtime's, so every phase gets them for free:
- A roster is complete or the runtime refuses it. A cycle whose roster
named phases nothing implemented used to run the ones it had, skip the rest
and report
clean— so a runtime composed with no phases at all answered "consolidation succeeded" having done nothing, which is the worst available answer: it is indistinguishable from a scope with nothing left to do. Completeness is checked at construction for every declared cadence, and again at :meth:ConsolidationRuntime.runfor a runtime that declared none. - A phase's name is recorded before it runs. Presence with a zero counter means "ran and found nothing"; absence means "never ran". Both produce a zero otherwise, and they call for opposite responses.
- A phase may decline.
appliesis how a phase says it has nothing to work on — no episodic layer, no profile schema, no judge model — which is the condition legacy expressed by wrapping the call in anif. It lands inskipped, which is a different sentence from "no implementation" and now cannot be confused with it. - A failing phase does not end the cycle. Consolidation is maintenance: the pruning failing is no reason to skip the decay.
- Cancellation is not a failure.
CancelledErrorpropagates unswallowed (CXL-2), so a cancelled run is never reported as a completed one — and because cancellation skips the commit, a cycle cut short publishes nothing. - A cycle that recorded an error publishes nothing. Writes stay pending, so a partial consolidation is invisible until a clean cycle commits it. This is the rollback the write/flush split exists to provide.
- A cycle that lost its scope stops rather than finishing.
still_authorisedis the lease, asked before every durable mutation (through :class:~.fencing.FencedGraph) and once more at the end. The status is thenlease_lost, which is neithercleannordegraded: the work is being redone properly by whoever holds the scope now, and this worker standing down is the mechanism working rather than anything to alert on.
What runs inside a phase is not the runtime's business — a phase reaches whatever store it owns and returns how many things it changed. That is what lets the same runtime drive the in-process reference HMS and a graph backend.
ConsolidationPhase ¶
Bases: Protocol
One maintenance step. Named, so a roster can ask for it.
ConsolidationRuntime ¶
ConsolidationRuntime(*, phases: Sequence[ConsolidationPhase] = (), writes: MemoryWriteCoordinator | None = None, cycles: Sequence[ConsolidationCycle] = (), participants: Sequence[Any] = (), max_new_edges_per_sweep: int | None = None)
Runs one cadence's roster over one scope.
Source code in src/symfonic/capabilities/memory/consolidation.py
participants
property
¶
The stores this runtime's phases write through.
Published so a composition root can prove, before any cycle runs, that the graph the phases mutate is in the same transaction domain as the lease that authorises them. Empty for a hand-built roster that declared none, which is the one case where the check has to wait for the commit.
writes
property
¶
The coordinator this runtime publishes through, if it has one.
Published so a composition root can check, before any cycle runs, that the staged records and the graph mutations share one transaction domain.
run
async
¶
run(scope: MemoryScope, cycle: ConsolidationCycle, *, run_id: str = '', root_run_id: str = '', still_authorised: Any = None, commit_authority: Any = None, domain: Any = None) -> ConsolidationState
Execute cycle's roster over scope and report what happened.
run_id/root_run_id name the turn this cycle is attributable to.
A quick nap fires from a turn and one of its phases can spend a model
call, so the cost has an owner rather than landing on whichever run was
in flight when the background task was scheduled. Empty is the honest
answer for a cycle a scheduler ran outside any turn.
Source code in src/symfonic/capabilities/memory/consolidation.py
serves ¶
ConsolidationState
dataclass
¶
ConsolidationState(scope_path: str, cycle: ConsolidationCycle, tenant_id: str = '', started_at: datetime = (lambda: datetime.now(UTC))(), finished_at: datetime | None = None, run_id: str = '', root_run_id: str = '', registered: tuple[str, ...] = (), phases_run: tuple[str, ...] = (), skipped: tuple[str, ...] = (), failed: tuple[str, ...] = (), deferred: tuple[str, ...] = (), mutations: dict[str, int] = dict(), ledger: dict[str, int] = dict(), counters: dict[str, int] = dict(), errors: tuple[str, ...] = (), committed: tuple[str, ...] = (), already_running: bool = False, lease_lost: bool = False)
What one cycle did — in this capability's terms and in legacy's.
status
property
¶
The cycle's final word.
running, clean, degraded -- and two more that are none of
those and must not be reported as any of them.
already_running: another holder had the scope, so this cycle never
started. Saying clean would make "somebody else is consolidating
this" indistinguishable from "there was nothing to consolidate", and a
scheduler reading a dashboard would conclude the cycle had run.
lease_lost: this cycle started, then lost the scope partway. Not
degraded either, though it is a kind of failure, because the two
want opposite responses: degraded is a phase that broke and wants
looking at, while lease_lost is a worker that correctly stood down
so another one could do the work properly. Alerting on the second is
alerting on the mechanism working. Ranked above degraded because a
cycle that loses its lease also collects the fence's error, and the
specific fact is the useful one.
telemetry ¶
The safe record of this cycle: integers, roster names, and a status.
Everything here is either a framework constant or a count. The scope
is not: tenant_id identifies whose consolidation this was, which
is what makes the model cost a phase spends attributable, and it is
already the key every other metric in the system carries.
Source code in src/symfonic/capabilities/memory/cycle_state.py
to_legacy_dict ¶
The shipped ConsolidationReport.to_dict() shape, plus this cycle.
Every legacy key is present with its legacy type, so a reader written
against the old report needs no change. The additions
(cycle, scope_path, skipped, committed) are new keys,
which a dict consumer ignores.
Source code in src/symfonic/capabilities/memory/cycle_state.py
PhaseContext
dataclass
¶
PhaseContext(scope: MemoryScope, cycle: ConsolidationCycle, started_at: datetime, writes: MemoryWriteCoordinator | None = None, run_id: str = '', root_run_id: str = '', ledger: dict[str, int] = dict(), legacy: dict[str, int] = dict(), seen: set[str] = set())
What a phase is told about the cycle it is running inside.
Frozen, and the two mutable fields are the cycle's own accumulators rather than state a phase can rewrite: a phase adds to the ledger and names what it read, and cannot reach anything another phase decided.