FORGETTING_PASS_ENABLED (default off), and its proposals are reviewed through review_proposals. It only ever writes proposals. The thresholds are starting values until the eval sets them. The report-only harness and its synthetic mirror ship in eval/ (eval/src/forgetting-eval.mjs, run nightly, never gated); the private golden set is still to come.
Context
A long-running agent’s memory fills up with decisions that were overtaken, preferences that drifted, and facts that went out of date. Drew Breunig puts it plainly in “The State of Context Engineering” (2026-10-24): “the instructions and memories start to build up and conflict with one another”, and strategic forgetting is unsolved. 3ngram already has the data model for forgetting without loss:- append-and-supersede, where nothing is merged or deleted on a write path (ADR-0005, memory model);
- type-aware consolidation that never auto-applies an edge to an
eventmemory (ADR-0005 §3: at most an advisoryextendsproposal); - an archive transition that leaves history readable.
Two things follow:
- Supersession only happens if something proposes it. People do not maintain lineage by hand.
- Proposals cannot be applied blindly. In the S1 data, high similarity could not tell “same fact, restated” from “same ritual, new day”.
Decision (proposed)
Add a worker job,forgetting-pass. Once a day, for each tenant that opted in to judgment, it:
- Selects candidates: stale single memories, plus pairs of same-type, same-scope memories that likely conflict.
- Asks for verdicts: sends them in batches to
Gateway.decide()(Jev, pinned model) for typed verdicts with calibrated confidence:keep,archive,supersede-by:<id>orconflict-with:<id>. - Writes proposals: confident non-keep verdicts become proposals that the user reviews through
review_proposals.
Candidate query
The pass reads two candidate streams per tenant. Both use the live-row predicate (valid_to IS NULL AND status = 'active') and the tenant selector of briefing.
Stale singles. Exactly briefing’s stale predicate (packages/db/src/briefing-read.ts):
- type in
STALE_CANDIDATE_TYPES(decision, preference, blocker, fact); - freshness
GREATEST(updated_at, last_reviewed_at)older thanSTALE_WINDOW_DAYS(30); - ordered most stale first;
- capped at 60 per tenant per run.
keep or conflict-with, because blockers settle through resolve. A stale blocker with no newer neighbour is therefore not judged at all.
Conflict pairs. The findSimilarPairs skeleton (packages/db/src/search.ts), with these conditions added inside the lateral kNN:
-
eventis excluded from pairing, as ADR-0005 §3 requires. Recurring episodic entries are near-identical by construction. -
noteis excluded in v1. In the S1 data, notes were 79% of destructive-merge targets, and their free form makes “conflict” hard to judge. Revisit after the eval. - Pair order: pairs are oriented by recency, newer first, as consolidation orients them. Pairs whose older side is stale rank first. The cap is 40 per tenant per run.
-
Anti-joins:
- an existing edge;
- a consolidation proposal in any status;
- a retention proposal in any status created after both memories’ freshness.
Judgment: the Jev state shape
-
Calls: one
decide()call per batch of up to 12 items, through the composed gateway.JEV_API_KEYandJEV_MODELare pinned (jev-1.14.0), the same configuration the session closer uses. -
Size limits: the judged memory’s content is cut to 1,200 characters, and each
newerneighbour’s to 300. A single item then carries at most about 2,100 characters of content and a pair about 2,400. Twelve items plus JSON overhead stay under 40k characters, against Jev’s limits of 64k (MAX_DECIDE_STATE_CHARS) and 100 questions. The batcher measures the rendered state and splits a batch that would pass 48k, so an unusually long topic cannot tripassertDecideRequest. -
Metering: each call is metered as a new operation,
memory.forget.decide. That takes two registry edits inpackages/llm/src/operations.ts:- an
llmOperationsentry with its own cost ceiling; - an addition to
METERED_JUDGMENT_OPERATIONS.
BudgetEnforcementand the closer’s metered-call shape: reserve, send, record usage, release. - an
-
Opt-in:
isJudgmentEnabled(userId)is read immediately before every send. -
No fallback: there is no fallback to
complete(). A tenant without the opt-in, or a deployment without a judgment gateway, gets no forgetting pass at all.
fenceExcerpt, which is exported fromsession-closer-prompt.ts;jsonLiteral, which is private tosession-closer-judgment.tstoday. PR 2 moves it besidefenceExcerptand exports it.
newer lists up to three newer live memories of the same type and scope, ranked by similarity. That makes “superseded by” answerable for singles.
Each item gets one choice question, so Jev returns a confidence and a full probability map:
The answer maps to a typed verdict:
keep,keep_both: keep.archive,archive_older: archive.supersede_by:…,supersede_older: supersede-by:<newer id>.conflict: conflict-with:<other id>.
keep and its probability clears that verdict’s threshold. A verdict below its threshold is stored dismissed with its real action and probabilities, so calibration can use it and the pass does not judge an uncertain memory again every day. The thresholds start at 0.85 for archive and supersede and 0.6 for conflict. The eval sets the final values.
Proposals
A new kind is needed.resolution_proposals cannot be reused:
- its
session_run_idis NOT NULL with a foreign key toagent_sessions, and a forgetting pass has no session; - it has no action column;
- it only stages open or waiting commitments;
- it means “this commitment is done”.
consolidation_proposals can express a supersede edge, but not an archive. Its similarity column means cosine, so storing a judge probability there would blur the per-type precision audit that ADR-0005 asks for.
So the pass gets a fourth review_proposals kind, retention, backed by a new table:
- Keep verdicts, and verdicts below their threshold, are stored with status
dismissedand are never listed. Only the pass setsdismissed; a reviewer’s no isrejected, withdecided_atset, so calibration can tell the two apart. They stop the pass from re-judging a memory until that memory’s freshness moves past the verdict, through an edit, a reaffirm or a newer neighbour. - Table count: this is the 32nd physical table. Its JTBD is “a memory the judge thinks is finished waits for a human yes instead of lingering in every briefing, or being dropped”, and no existing proposal table can carry it, as argued above.
- Accepting:
- archive:
archiveMemoryInwith thedispositionpolicy, the same rejection rules and an auditedarchiveevent. - supersede: a
supersedesedge from the newer to the older memory, withapplyProposal’s predecessor-close semantics. It refuses event-type and no-longer-live memories. - conflict: records the review only, with no memory change. The user resolves it with
revise.
- archive:
- Rejecting: final for that memory and action, until the memory’s freshness moves.
review_proposals output counts, so this ships as a V4 output under ADR-0011. V4 itself left the tool description unchanged; a follow-up rewrote it to name all four kinds, regenerated the tool-selection embeddings, replaced one consolidation scenario with a retention one, and reran the eval gate at the current floors. Hard rule 8 still requires exactly 5 scenarios per tool.
Flag and schedule
The pattern isSESSION_CLOSER_ENABLED:
FORGETTING_PASS_ENABLEDinpackages/config, astringboolthat defaults to false, loaded withloadForgettingPassConfig().- Resident worker: with the flag on, upsert a job scheduler at
0 30 4 * * *, daily at 04:30 UTC, clear of the hourly consolidation tick. With it off, remove the scheduler, and gate the processor so a residual job completes as a no-op. - One-shot: add
forgetting-passtoONCE_JOBS. With the flag off it exits 0 without reading anything. Add a row to the operate job table (30 4 * * *). - Budget: the injected
BudgetEnforcementreserves a slot before everydecide()call, as the closer does, and the pass rejects instead of billing past the cap.
Eval plan
Golden set. It is built from real memories in the 3ngram project of one consenting account, so it never goes into the publiceval/fixtures. It lives in a private repository. A small synthetic mirror in eval/ tests the harness wiring in CI. Target slices:
The recurring-episodic slice must stay at zero false collapses. The fixture-backed eval measures judge verdicts on supplied candidates; it does not exercise production candidate selection. Neither
event nor note rows enter stale singles or conflict pairing in the shipped pass. The core forgetting-pass.int.test.ts integration suite checks both exclusions through dbForgettingRepo.readCandidates, with otherwise-eligible recurring episodes and a decision-pair positive control.
Metrics.
- Headline: precision on archive and supersede proposals, each separately and then combined, reported per memory type as ADR-0005 asks. A wrong archive costs more than a missed one, so recall is secondary.
- Also reported: conflict precision, the keep rate on still-valid items, and calibration (expected calibration error) of the judge’s confidences.
- labels are frozen before the first judged run;
- thresholds are chosen on a development split, and precision is reported on a held-out split;
- recorded Jev responses become fixtures for deterministic CI, as
jev-recorded.test.tsdoes; - a live re-measurement runs whenever
JEV_MODELchanges.
revise#disposition slice, until both hold on the held-out split:
- archive precision of at least 0.95 with at least 40 proposals;
- zero recurring-episodic collapses.
Rollout
It ships as a stack of small PRs tostaging, all default-off:
- Migration and db functions for
retention_proposals(RLS + FORCE), plus export and account-deletion coverage. - Core candidate selection and judgment rendering, tested with
FakeGateway. - Worker job, flag and one-shot entry, plus the operate docs row.
review_proposalsV4 with theretentionkind; the description rewrite, embeddings regeneration and eval-gate rerun followed in their own PR.- Private golden set and report-only harness.
Open questions
- Should confident
keepverdicts also propose areaffirm? It would shrink the stale list, but it is a write the user did not ask for. The v1 answer is no: a keep stays silent. - Should notes join candidate selection after the eval, as singles, pairs, or neither?
- Should accepting a
conflictoffer the two resolutions inline (archive the older, or supersede), instead of sending the user torevise? - How does the pass share the daily budget with the session closer when both are on?