symfonic.capabilities.memory.journal¶
journal ¶
A consolidation cycle mutates the graph all at once, or not at all.
The fence stopped a cycle that lost its lease from writing again. It could
not undo what the cycle had already written, and on this roster that is most of
the danger: ten phase modules call add_node, update_node,
delete_node, add_edge and delete_edge on the graph directly across
nineteen call sites, and each one is durable the moment it returns. A cycle
abandoned halfway therefore left a visible half -- strengthened importances
without the merge that justified them, pruned orphans without the meta nodes
they were orphaned from -- and the runtime's own promise that a cycle which
recorded an error publishes nothing was true only of the staged writes.
Nor was the lease the only way to get there. Any ordinary phase failure in the
middle of Deep Sleep produced the same split: _commit withheld the staged
half while the direct half was already in the store.
So the cycle gets a journal. Every graph mutation goes into
:class:CycleJournal instead of the store; reads are answered from an overlay
of the scope with those mutations applied, so a phase sees its own work and the
work of the phases before it; and at the end the whole batch is applied at once
under the lease, or thrown away. Failure, cancellation and a lost lease all
take the same path -- the journal is discarded and the store never knew.
What it covers, exactly. Graph mutations -- which on this roster is every
mutation a phase makes. No shipped phase writes through context.writes, and
the vector index is written on the turn path (GraphBackedHms.write) rather
than by any phase. A future phase that staged a record through the write
coordinator would have its graph row in the batch and its vector row written
immediately, so a discarded cycle would leave an orphan vector; that is worth
knowing before writing such a phase rather than discovering afterwards.
Bound per cycle, through a ContextVar, like the fence and like run attribution. The wrapper is built once by the composition root, long before any cycle exists, and a journal threaded through every factory argument would be a thing each future phase author has to remember. Unbound -- the default -- means no cycle is running on this task and every call passes straight through, which is what an ordinary turn does while a nap runs beside it.
CycleJournal ¶
One cycle's deferred mutations, and the scope they are read against.
Keyed by durable backend, because a deployment may compose more than one and a cycle that mixed their operations into one ordered list would replay them against the wrong store.
Source code in src/symfonic/capabilities/memory/journal.py
apply
async
¶
Write every recorded mutation to its store, in order. Returns the count.
The caller is responsible for having checked authority, and for doing
it close enough to this call that nothing can intervene -- see
:func:symfonic.capabilities.memory.commit.commit_cycle, which does
both inside one database transaction where the store offers one.
Source code in src/symfonic/capabilities/memory/journal.py
discard ¶
Throw the batch away. Returns how many mutations never happened.
read
async
¶
Answer a read from the overlay, seeding it from durable first.
Source code in src/symfonic/capabilities/memory/journal.py
stores ¶
The durable backends this cycle touched, for the commit to lock.
write
async
¶
Apply a mutation to the overlay and record it for the commit.
Applied to the overlay first so its return value is the one the durable store would have produced -- the persisted node, the upserted edge with its incremented weight -- and so the next phase reads the result rather than the input.
Source code in src/symfonic/capabilities/memory/journal.py
JournalledGraph ¶
The graph backend a deployment composes once, for everything.
Wraps rather than subclasses: the backends are protocol implementations
with surfaces this module has no business restating, and anything outside
:data:GRAPH_MUTATIONS and :data:GRAPH_READS is forwarded exactly as it
was.
Composed at the root rather than around the phases, so that everything a
cycle writes through goes into the same journal -- including the layers.
ProceduralLayer.store_skill writes through a GraphMemoryStore, and
a journal wrapped around only the phase factory's graph= argument would
have left the one mutation an adopter is most likely to notice, the
promoted draft skill, durable on its own.
Source code in src/symfonic/capabilities/memory/journal.py
current_journal ¶
journalled
async
¶
Defer this task's graph mutations into journal for the duration.