symfonic.core.learning.phases_semantic_expiry¶
phases_semantic_expiry ¶
Phase 9.2: durability/validity-driven SEMANTIC expiry (J1 + J2).
Closes the gap documented in
the prompt-part-blocks architecture note (an adopter's stale-memory
incident): SEMANTIC nodes have no expiry path at all.
cleanup_working_ttl (phases_maintenance.py) only looks at
MemoryLayer.WORKING; decay_importance lowers importance but never
removes a node; prune_retracted needs a retraction marker that nothing
stamps for a semantic fact that simply stopped being true.
This phase gives the SEMANTIC layer that missing expiry path, entirely time-based -- no model is ever asked whether a memory is stale:
- J1 -- honour
durability. A node markeddurability="expired"is retracted unconditionally, mirroringcleanup_working_ttl's unconditionalexpiredbranch (:96-107). A node markeddurability="transient"is retracted oncettl_hours(explicit, or :data:TRANSIENT_TTL_HOURS_DEFAULT) has elapsed sinceupdated_at-- the SEMANTIC-layer analogue of the WORKING-layer transient TTL floor, except the terminal action here is soft-retract, never delete. - J2 --
valid_until+INCIDENT:. Any node carrying avalid_untilISO timestamp is retracted once that instant passes, independent of itsdurabilityvalue -- this is the generic validity axis the design doc calls for. Nodes labelledINCIDENT:/INCIDENTare treated as at-least-transient even whendurabilityis absent or explicitly"durable": this is deliberately the first prefix convention that makes a node age out FASTER rather than slower (the opposite ofSOUL:/AGENT_IDENTITY:), so writing an incident as a plain fact can never accidentally pin it forever. A futurevalid_untilon an INCIDENT node extends its life past the default TTL; there is no way to make an INCIDENT node permanently exempt -- that is what theSOUL:/AGENT_IDENTITY:vocabulary is for.
Design decision -- placement (sibling phase, not inside decay_importance):
decay_importance's documented contract is "lower importance"; this
phase's terminal action is retraction, a different and stronger effect.
Keeping them separate also keeps each file under the ~300-line budget and
lets each phase fail independently in SleepConsolidator.run without
one phase's exception aborting the other.
Design decision -- the identity exemption applies, the importance
exemption does not: This phase reuses
:func:phases_maintenance.is_identity_labelled (SOUL: /
AGENT_IDENTITY:) as an unconditional skip -- an identity fact must
never age out, full stop, regardless of what a caller writes into
durability or valid_until on it. It deliberately does NOT reuse
CRITICAL_IMPORTANCE_THRESHOLD (importance >= 8.0): that exemption
protects high-signal knowledge from decay, but a high-importance
INCIDENT fact ("production is down") is exactly the case J2 exists to
expire (§2.5a). Applying the importance exemption here would silently
defeat the whole point of the INCIDENT: prefix.
expire_semantic_durability
async
¶
expire_semantic_durability(
graph: GraphMemoryStore,
scope: TenantScope,
all_nodes: list[MemoryNode],
) -> int
Soft-retract SEMANTIC nodes whose durability/validity has elapsed.
Terminal action is always soft-retraction (the namespaced marker from
symfonic.memory.retraction), never delete: this flows through the
same reversible, audited path as retract_node, and the existing
prune_retracted phase reclaims the row after the grace window.
Returns:
| Type | Description |
|---|---|
int
|
Number of nodes retracted. |