Decay Mechanics
Problem
Without decay, knowledge stores grow monotonically. Team standup notes from January sit alongside today's context. A machine proposal that was never reviewed but never rejected lingers with the same visibility as fresh insights. Over time:
- Search quality degrades. Stale L2 context pollutes queries. A team's working memory from three sprints ago is noise.
- Review fatigue. Governance reviewers face an ever-growing backlog of L3 proposals, many of which are outdated by the time they are reviewed.
- Storage grows unbounded. Ephemeral context was never meant to live forever.
Deletion is not the answer — decayed entries may be referenced by evidence chains in L3 proposals or L4 canon. Destroying them breaks audit trails.
Solution
SOMA implements time-based decay with activity extension. Entries in L2 and L3 have a decay_at timestamp. When that timestamp passes, the Decay Processor moves the entry to L1 (the Execution Archive) with a decayed_from marker. The entry is not deleted — it sinks to the historical layer where it remains queryable but no longer surfaces in active searches.
Key design decisions:
- Decay to L1, not deletion. Evidence chains remain intact.
- Activity-based extension. Reading an entry resets its decay timer, keeping actively-referenced knowledge alive.
- Promoted/rejected entries are exempt. Once a governance decision is made, the entry is permanent regardless of layer.
- Reference updates on decay. Evidence links in L3/L4 entries are rewritten to point to the new L1 location.
Architecture
Code Examples
Creating the Decay Processor
import { createDecayProcessor } from './decay';
import { Vault } from './vault';
const vault = new Vault({ dir: '.soma/vault' });
const decayProcessor = createDecayProcessor(vault, {
l2DefaultDays: 14, // Team working memory TTL
l3DefaultDays: 90, // Emerging knowledge TTL
teamDecayDays: { // Per-team overrides
'backend': 7, // Backend team: faster decay
'security': 30, // Security team: slower decay
},
});
Processing decay
// Run the decay processor — checks all L2 and L3 entries
const result = await decayProcessor.processDecay();
// Example output:
// {
// decayed: [
// {
// id: 'standup-backend-20260307',
// from_layer: 'working',
// to_layer: 'archive',
// new_id: 'decayed-standup-backend-20260307',
// decayed_from: 'working',
// references_updated: 0
// },
// {
// id: 'proposal-old-pattern',
// from_layer: 'emerging',
// to_layer: 'archive',
// new_id: 'decayed-proposal-old-pattern',
// decayed_from: 'emerging',
// references_updated: 2 // Two L4 entries had evidence_links updated
// }
// ],
// skipped_promoted: 3,
// skipped_rejected: 1,
// total_checked: 47
// }
Activity-based extension
// When an entity is accessed, its decay timer resets
import { extendDecayOnAccess } from './decay';
// This happens automatically during queryByLayer and vault.get()
// But can also be called explicitly:
await extendDecayOnAccess(vault, 'proposal-retry-limit');
// The entry's decay_at is reset to now + configured TTL
// So an L3 entry accessed today won't decay for another 90 days
What a decayed entry looks like
---
type: insight
id: decayed-standup-backend-20260307
name: "Backend team standup notes (decayed)"
status: active
layer: archive
source_worker: decay
decayed_from: working
original_id: standup-backend-20260307
original_decay_at: "2026-03-21T00:00:00.000Z"
team_id: backend
tags: ["team-context", "decayed"]
created: "2026-03-07T10:00:00.000Z"
updated: "2026-03-21T00:05:00.000Z"
---
## Backend team standup notes (decayed)
Original team working memory entry, decayed to archive on 2026-03-21.
Reference update on decay
// Before decay: L4 canon entry references an L3 proposal
// {
// id: 'canon-retry-limit',
// evidence_links: ['exec-invoice-agent-001', 'proposal-old-pattern', 'exec-email-router-042']
// }
// After 'proposal-old-pattern' decays to L1:
// {
// id: 'canon-retry-limit',
// evidence_links: ['exec-invoice-agent-001', 'decayed-proposal-old-pattern', 'exec-email-router-042']
// }
// The link is updated, not broken.
CLI commands
# Show entries approaching expiry
soma decay status
# Example output:
# L2 Working Memory:
# standup-backend-20260318 expires in 3 days team: backend
# standup-security-20260310 expires in 1 day team: security
#
# L3 Emerging:
# proposal-payload-limit expires in 12 days
# proposal-logging-pattern expires in 67 days
#
# Exempt (promoted/rejected): 4 entries
Rules and Invariants
Decay rules by layer
| Layer | Decays? | Default TTL | Configurable? | Decay Target |
|---|---|---|---|---|
| L1 Archive | Never | - | - | - |
| L2 Working Memory | Yes | 14 days | Per-team via teamDecayDays | L1 |
| L3 Emerging (active) | Yes | 90 days | Globally via l3DefaultDays | L1 |
| L3 Emerging (promoted) | Never | - | - | - |
| L3 Emerging (rejected) | Never | - | - | - |
| L4 Canon | Never | - | - | - |
Decay exemptions
| Condition | Exempt? | Reason |
|---|---|---|
L3 entry with status: promoted | Yes | Governance decision is permanent |
L3 entry with status: rejected | Yes | Governance decision is permanent |
| L1 entry (any status) | Yes | Archive is permanent by definition |
| L4 entry (any status) | Yes | Canon is permanent by definition |
| L2/L3 entry accessed within TTL | Extended | Activity resets the decay timer |
Reference integrity on decay
When an entry decays, the processor performs these steps in order:
- Create a new L1 entity with
decayed-{originalId}as the ID anddecayed_frommetadata. - Scan all L3 entries for
evidence_linkscontaining the old ID. Update them to the new ID. - Scan all L4 entries for
evidence_linkscontaining the old ID. Update them to the new ID. - Mark the original entry as decayed (it is replaced by the L1 copy).
The checkDanglingReferences() function can be run independently to audit all evidence links across the vault and report any that point to non-existent entities.
Invariants
- L1 and L4 entries never have
decay_at. The schema rejects it. - Every L2 entry has both
team_idanddecay_at. Missing either fails validation. - Decayed entries always land in L1. There is no decay path to L2, L3, or L4.
- Evidence references are updated atomically with decay. No operation leaves dangling links.
- Reading extends decay. Any access to an entry resets its
decay_attonow + TTL. - Promoted/rejected L3 entries never decay. The governance decision overrides the timer.
- Decay is non-destructive. The original content is preserved in L1 with full provenance metadata.