symfonic.core.prompt.blocks.memory_lane¶
memory_lane ¶
The memory lane: a label-prefix scan turned into a block revision.
One half of :mod:~symfonic.core.prompt.blocks.resolver, kept separate
because it answers a different question. The resolver decides which
blocks to read and what to do when a read fails; this module decides
what a set of graph nodes is once they have been read -- which of
them carry a statement, in what order, with whose provenance, and under
what revision.
Why a scan and not a retrieval¶
:meth:MemoryLaneScanner.query_nodes is a filter, not a ranker. It
takes no query text, computes no similarity, and -- called with
limit=None -- truncates nothing. The Protocol is deliberately
narrower than the graph store it is satisfied by: a caller holding only
this type cannot reach retrieve() even by mistake, because the
method that would rank is not on the object it was handed.
Facts, not a paragraph¶
Each matching node becomes one
:class:~symfonic.core.prompt.blocks.types.BlockFact carrying that
node's own source and recorded_at. Flattening the scan into
one string would force the renderer to either omit provenance or invent
a single timestamp for statements recorded years apart, and an invented
timestamp on a user-facing profile is worse than an absent one.
Provenance a node did not record stays None.
EMPTY_MEMORY_REVISION
module-attribute
¶
Revision of a memory-backed block whose scan matched no node.
A concrete, comparable value rather than "": an empty lane is a real
state that must be cacheable, and :class:BlockRevision rejects a blank
revision precisely so "nothing was found" cannot be confused with
"nothing was recorded".
MemoryLaneScanner ¶
Bases: Protocol
The one memory operation the pinned lane is allowed to perform.
Satisfied structurally by the graph store, whose query_nodes
accepts these arguments among others. Nothing here can rank, score
or embed.
query_nodes
async
¶
query_nodes(
scope: TenantScope,
*,
label_prefix: str | None = None,
limit: int | None = None,
) -> Sequence[Any]
Return every node in scope whose label starts with the prefix.
memory_revision ¶
A durable revision for the lane: mem:<count>:<max updated_at>:<digest>.
Durable because every component comes from the store, not from this
process: a write by the onboarding router or the consolidation
worker advances max(updated_at) and the next scan here sees it.
An in-process counter would not, and this agent would go on serving
a block the user already corrected elsewhere.
max(updated_at) is computed over parsed instants, not over ISO
text: string order only agrees with instant order when every node
shares one UTC offset, and a naive stamp is always a string-prefix of
(hence lexicographically less than) an aware one at the same wall
clock. Comparing text let a corrected fact keep the revision of the
fact it replaced whenever the writer's offset happened to sort
"smaller".
The count is in the key because a deletion moves neither the remaining nodes' stamps nor the maximum, and a digest of the ordered fact values is in the key because an edit that preserves both count and max(updated_at) -- a migration pass that re-labels a node under its original timestamp -- is otherwise invisible to this revision.
Source code in src/symfonic/core/prompt/blocks/memory_lane.py
scan_memory_lane
async
¶
scan_memory_lane(
memory: MemoryLaneScanner,
scope: TenantScope,
label_prefix: str,
profile_fields: frozenset[str] = frozenset(),
) -> BlockRevision
Scan one block's label space and assemble its revision.
limit=None is passed explicitly rather than left to default:
the store's own page size would truncate the scan, which is the
top-K failure this lane exists to avoid, one layer down.
The returned revision always carries a facts tuple -- empty when
nothing is recorded yet. A learned block with facts=None means
"the source violated its contract", and an empty lane must not
masquerade as one.
profile_fields names the node properties that render as facts
in their own right, alongside the label. Without it this lane reads
labels only, and a correction that
:func:~symfonic.core.learning.phases_profile.promote_profile_corrections
wrote to a property -- which is where it writes all of them -- could
never reach the prompt: the two halves wrote and read different
parts of the same node. The set is declared, never inferred from
what a node happens to carry, so a property an extractor invented
(or an attacker talked one into writing) is not a way to add a line
to the model's standing context.