Node-class durability (v7.26.2)¶
Durability tells the sleep-consolidation pipeline whether a memory row is
a fact about the world (promote it freely) or a transient state
(refuse promotion, age it out fast). It closes the "still syncing" class
of consolidation bugs, where a working-memory row like
"Slack roll-up is still syncing -- ETA ~30s" was promoted into semantic
memory and recited as a current fact two days later.
The three values¶
| Value | Meaning | Promotion | TTL behaviour |
|---|---|---|---|
durable |
A fact worth keeping. Default. | Promoted freely (Phases 10/11/12). | Normal TTL. |
transient |
A state-bearing row that will be wrong soon. | Refused at every promotion phase. | Capped at a 1h implicit floor (Phase 8). |
expired |
A transient row whose moment has passed. | Refused. | Pruned unconditionally on the next Phase 8. |
A node without a durability marker reads as durable. This is
byte-identical to pre-v7.26.2 behaviour, so every existing row in every
adopter database keeps consolidating exactly as before. Durability is
strictly opt-in.
Where it lives¶
The marker is stored inside the existing MemoryNode.properties dict
(properties["durability"]). There is no schema migration -- all
three graph backends (Postgres JSONB, Mongo, InMemory) already persist
arbitrary property keys. MemoryNode.durability is a computed property,
not a model Field, so the serialised shape is unchanged.
Writing a transient row¶
When the durability kwarg is omitted, no key is written and the node
reads as durable. An invalid value raises ValueError at the call
site (fail-loud), so a typo never silently bypasses the gate.
The marker can also ride in entry.metadata["durability"] (e.g. from the
extraction pipeline); the explicit push kwarg wins on conflict.
Upgrading a row¶
await orchestrator.upgrade_durability(scope, node_id, "durable") # promote
await orchestrator.upgrade_durability(scope, node_id, "expired") # retire
upgrade_durability is the explicit promotion / retirement signal. There
is no automatic "this state has stabilised" detection in v7.26.2 -- the
adopter writes the flip. The implementation is a read-modify-write that
preserves every other property on the node.
Recipe: the Slack roll-up¶
# t=0 / t=15s -- transient status rows
await working.push(scope, row1, durability="transient")
await working.push(scope, row2, durability="transient")
# t=85s -- the durable outcome (no kwarg needed -> reads as durable)
await working.push(scope, row3)
# Once the durable row lands, retire the transient ones.
await orchestrator.upgrade_durability(scope, row1_id, "expired")
await orchestrator.upgrade_durability(scope, row2_id, "expired")
The consolidator skips rows 1+2 at every promotion phase, Phase 8 prunes them on the next cleanup, and row 3 promotes normally. Forty-eight hours later the agent only sees the durable outcome.
durability is not importance¶
These are orthogonal. durability answers "is this a fact about the
world or a transient state?"; importance answers "given it IS a fact,
how heavily should it weigh?". A transient row may be highly important
right now and still must not become a permanent fact.
Observability (no silent drops)¶
Every gate rejection increments ConsolidationReport.transient_rejected
and emits a consolidation.transient_rejected log line carrying the
rejecting phase and the entry id. Adopters can audit exactly why a row
was not consolidated.