Skip to content

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

await working_layer.push(scope, entry, durability="transient")

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.