11# ADR-0011: Claim-centric learning memory from agent sessions
22
3- ** Status:** proposed (PoC under measurement) · ** Date:** 2026-09-10 · ** Supersedes (if the gates pass):**
3+ ** Status:** proposed (PoC under measurement) · ** Version: ** 1.1 (council-revised, see Council record) · ** Date:** 2026-09-10 · ** Supersedes (if the gates pass):**
44the retrieval half of PR #18 (` memory_recall ` over legacy concepts, ontology as a recall arm).
55Retains PR #18 's capture half (native evidence, hash binding, the bound-proof trigger design).
66
@@ -26,15 +26,27 @@ left to agent discipline at session end. Concretely:
2626
27271 . ** Canonical typed events** , not flat messages. Every adapter emits `kind ∈ {user, assistant_prose,
2828 tool_call, tool_result, system, thinking, error}` and a ` turn_id` from the parser.
29- 2 . ** Evidence in the same transaction as events.** Native bytes where the harness still has them;
30- for the archive, the prose we hold, labelled ` basis=REPORTED, origin=archive ` .
29+ 2 . ** Evidence in the same transaction as events, one row per prose event.** The citation surface
30+ is the event, not the session: every ` user ` /` assistant_prose ` event gets an evidence row whose
31+ body is that event's text (` origin=archive, basis=REPORTED ` for history), so a claim's offsets
32+ survive re-derivation, reclassification and reordering, and quote ambiguity is bounded by one
33+ message. Where the harness still has the native transcript, its raw bytes are retained as an
34+ ` OBSERVED ` capture row alongside. Evidence is append-only (triggers refuse UPDATE/DELETE).
35+ * (v1.1 — council finding 7; the v1.0 session-sized concatenated body was withdrawn.)*
31363 . ** Derivation runs in the export sweep.** A deterministic pass ($0) threads turns into exchanges,
3237 flags questions/errors/retries, tags concepts, detects cross-session recurrence, and writes the
3338 learning tier. A budgeted model pass distils exchanges into ** claims** (Problem · Finding ·
34- Decision · Procedure · Preference), each quote-bound by trigger, with sub-agent outcomes rolled
35- up through lineage; Findings seed review items.
36- 4 . ** Claims are the retrieval unit.** Serve claims first (latest non-superseded, with quote and
37- provenance), sessions as drill-down. Index prose only. Embed claims, never messages.
39+ Decision · Procedure · Preference), ** each with at least one quote-bound citation — enforced by
40+ the database, not the caller** (citations are written first under a deferred FK; an AFTER
41+ INSERT trigger aborts a citation-less claim), with sub-agent outcomes rolled up through
42+ lineage; Findings seed review items.
43+ 4 . ** Claims are the retrieval unit.** Serve claims first, sessions as drill-down. Index prose only.
44+ Embed claims, never messages. Read contract * (v1.1)* : a claim is ** active** iff no claim
45+ ` supersedes ` it; ** disputed** iff an active claim ` contradicts ` it; retrieval returns active
46+ claims and marks disputed ones, never silently dropping either. Supersession is same-session-
47+ or-lineage only; a cross-session replacement is a ` corrects ` relation. Natural-language input
48+ to any search surface goes through a planner that phrase-quotes every token — raw FTS syntax is
49+ a separate, explicit API.
38505 . ** Lineage and harness/project are claim metadata** , usable as filters. The tier-1 ontology is
3951 not a recall arm.
40526 . ** Session ids are unchanged** from today's exporters so every existing gold question, receipt and
@@ -43,26 +55,39 @@ left to agent discipline at session end. Concretely:
4355## Canonical model (PoC schema, package ` packages/learning-memory ` , own SQLite file)
4456
4557``` sql
46- sessions(id PK, harness, project, branch, parent_id NULL REFERENCES sessions, started_at, ended_at, scope, intent, outcome)
58+ sessions(id PK, harness, project, branch, parent_id NULL REFERENCES sessions, started_at, ended_at, scope, intent, outcome,
59+ classifier_version, adapter_version) -- v1.1: provenance of the typing
4760events(id PK, session_id FK, turn_id INT , seq INT , kind CHECK (kind IN (...)), actor, text , tool_name, ts,
4861 content_hash, UNIQUE(session_id, content_hash))
49- evidence(id PK = sha256(payload), session_id FK, body, body_sha256, origin, basis, captured_at)
62+ -- v1.1: content_hash covers (turn_id, seq, kind, actor, tool_name, text): every observed occurrence is a row;
63+ -- re-parse of the same source is still a no-op. Adjacent exporter duplicates are the adapter's to collapse.
64+ evidence(id PK = sha256(payload), session_id FK, event_id NULL FK, body, body_sha256, raw BLOB NULL , origin, basis, captured_at)
65+ -- v1.1: one REPORTED row per prose event (event_id set); one OBSERVED row per native capture (raw bytes retained)
66+ TRIGGER evidence_immutable BEFORE UPDATE / BEFORE DELETE : RAISE(ABORT)
5067lineage(parent_id FK, child_id FK, PRIMARY KEY (parent_id, child_id))
51- prose_fts -- FTS5 external-content over events WHERE kind IN ('user','assistant_prose'); tokenize measured (porter vs unicode61)
52- exchanges(id PK, session_id FK, turn_id, question_event_id, answer_event_ids JSON, is_question, had_error, retried, resolved)
53- concept_tags(exchange_id FK, concept, source CHECK (source IN (' vocab' ,' alias' ,' model' )))
54- recurrence(concept, session_ids JSON, first_seen, last_seen, count)
68+ lineage_pending(child_id FK, parent_id TEXT , PRIMARY KEY (child_id, parent_id)) -- v1.1: reconciled when the parent lands
69+ prose_events -- VIEW: SELECT id, text FROM events WHERE kind IN ('user','assistant_prose')
70+ prose_fts -- FTS5 external-content over prose_events (v1.1: so 'rebuild' stays prose-only); tokenize measured
71+ exchanges(id PK, session_id FK, derivation_version, turn_id, question_event_id, answer_event_ids JSON,
72+ is_question, had_error, retried, resolved, UNIQUE(session_id, derivation_version, turn_id))
73+ concepts(id PK, canonical) concept_aliases(alias PK, concept_id FK) -- v1.1
74+ concept_tags(exchange_id FK, concept_id FK, source CHECK (source IN (' vocab' ,' alias' ,' model' )))
75+ concept_occurrences(concept_id FK, session_id FK, derivation_version, observed_at) -- v1.1: replaces recurrence.session_ids JSON
5576claims(id PK, session_id FK, kind CHECK (kind IN (' Problem' ,' Finding' ,' Decision' ,' Procedure' ,' Preference' )),
5677 title <= 120 , statement <= 500 , tags JSON 2 ..5 , confidence 0 .5 ..1 .0 , writer, created_at, supersedes NULL FK)
57- claim_citations(claim_id FK, evidence_id FK, start INT , end INT , quote) -- code-point offsets
58- TRIGGER claim_citation_bound_proof BEFORE INSERT: RAISE(ABORT) unless substr(evidence .body , start+ 1 , end- start) = quote
78+ -- v1.1: id covers the canonical citation-set fingerprint and supersedes (Stage E)
79+ claim_citations(claim_id FK DEFERRABLE INITIALLY DEFERRED, evidence_id FK, start INT , end INT , quote) -- code-point offsets
80+ TRIGGER claim_citation_bound_proof BEFORE INSERT/ UPDATE : RAISE(ABORT) unless substr(evidence .body , start+ 1 , end- start) = quote
81+ TRIGGER claims_need_citation AFTER INSERT ON claims: RAISE(ABORT) unless ≥1 claim_citations row exists -- v1.1
5982 TRIGGER claims_immutable BEFORE UPDATE ON claims: RAISE(ABORT)
6083claim_relations(from_id, to_id, kind CHECK (kind IN (' supports' ,' contradicts' ,' corrects' )))
6184review_items(id PK, claim_id FK, front, back, kind CHECK (kind IN (' flashcard' ,' quiz' ,' teach_back' )), created_at)
6285```
6386
64- Invariants (property-tested): re-import is a no-op by ` content_hash ` ; no session row without ≥1
65- evidence row; a claim cannot be inserted with a non-binding quote; claims never change.
87+ Invariants (property-tested): re-import of the same source is a no-op; every observed event
88+ occurrence is a row; no session row without ≥1 evidence row; evidence never changes; a claim
89+ cannot exist without a binding citation; claims never change; an FTS rebuild indexes no tool text;
90+ a child ingested before its parent acquires its lineage edge when the parent lands.
6691
6792## Adapter contract
6893
@@ -73,21 +98,37 @@ class HarnessAdapter(Protocol):
7398 def parse (self , ref : SourceRef) -> ParsedSession: ... # Session, list[Event], native_source: bytes, lineage: list[str]
7499```
75100
76- The shared base owns dedupe, evidence, lineage, derivation. Each adapter ships a scrubbed golden
77- fixture and passes the shared contract suite: no tool text in prose events; ` turn_id ` on every event;
78- native source present; lineage where the harness supports sub-agents. The ** archive adapter** reads
79- ` sessions.db ` and classifies ` kind ` deterministically; it is the only path for history.
101+ The shared base owns dedupe, evidence, lineage, derivation. * (v1.1)* ` SourceRef ` carries
102+ ` source_sha256 ` ; ` ParsedSession ` carries ` adapter_version ` , ` classifier_version ` and
103+ ` exporter_dupes_collapsed ` (adjacent identical rows the adapter folded before emitting — 37,528 in
104+ the archive). Each adapter ships a scrubbed golden fixture and passes the shared contract suite:
105+ no tool text in prose events; ` turn_id ` on every event and ` (turn_id, seq) ` monotone; a closed
106+ actor vocabulary; ISO-8601 UTC ` ts ` ; ` Session.id ` derived from the source; native source present
107+ where the harness has one; lineage where the harness supports sub-agents. The ** archive adapter**
108+ reads ` sessions.db ` and classifies ` kind ` deterministically under a named ` classifier_version ` ; it
109+ is the only path for history.
80110
81111## Derivation rules (deterministic pass)
82112
83- - Exchange = a ` user ` event plus all following non-` user ` events until the next ` user ` event.
113+ * (v1.1)* These rules ship as ` derive.py ` under a ` derivation_version ` , with a hand-labelled
114+ cross-harness fixture set (≥ 20 exchanges per harness, labelled by the orchestrator, not a model)
115+ and one test per rule. Events that cannot be threaded deterministically are quarantined
116+ (` exchanges.resolved = NULL ` , reason recorded), never guessed.
117+
118+ - Exchange = a ` user ` event plus all following non-` user ` events until the next ` user ` event,
119+ correlated by ` turn_id ` , tool-call id where the harness has one, and lineage for sub-agent
120+ traffic.
84121- ` is_question ` : user text contains ` ? ` or begins with an interrogative; ` had_error ` : any ` error `
85- or ` tool_result ` matching a failure lexicon in the exchange; ` retried ` : same tool_call signature
86- twice; ` resolved ` : the exchange ends with ` assistant_prose ` and the next user turn is not a repeat.
87- - Concept tags: match the learner's topic vocabulary (` studyloop.topics ` ) and aliases; model tags
88- only in the model pass, marked ` source='model' ` .
89- - Recurrence: a concept tagged in ≥ 2 distinct sessions ≥ 1 day apart → ` struggled ` backlog item.
90- - ` intent ` = first user event's prose (≤ 200 chars); ` outcome ` = last resolving ` assistant_prose ` .
122+ or ` tool_result ` matching a versioned failure lexicon in the exchange; ` retried ` : same
123+ ` (tool_name, normalised arguments) ` twice in one exchange; ` resolved ` : the exchange ends with
124+ ` assistant_prose ` and the next user turn is not a repeat.
125+ - Concept tags: match the learner's topic vocabulary (` studyloop.topics ` ) through ` concepts ` +
126+ ` concept_aliases ` (canonical id, casing-insensitive); model tags only in the model pass, marked
127+ ` source='model' ` .
128+ - Recurrence: a concept with ` concept_occurrences ` in ≥ 2 distinct sessions ≥ 1 day apart →
129+ ` struggled ` backlog item.
130+ - ` intent ` = first user event's prose (≤ 200 chars); ` outcome ` = last ` assistant_prose ` of the last
131+ ` resolved ` exchange.
91132
92133## Evaluation binding
93134
@@ -97,6 +138,11 @@ entailment audit), G4 (claim embeddings), G6 (decision correctness), operational
97138** Answer-grain** — whether the top returned claim contains the gold's atomic answer — is reported
98139alongside recall@5 on every receipt. Every arm keeps the same session ids as ` sessions.db ` .
99140
141+ * (v1.1 — council finding 13)* Every arm receipt carries a ** run manifest** : adapter versions,
142+ ` classifier_version ` , ` derivation_version ` , writer model id + prompt sha256 + parameters, corpus
143+ digest, store schema version. The single SEALED look is executed on a ** fresh work copy rebuilt
144+ from the manifest** , never on the store the DEV looks were tuned against.
145+
100146## Consequences
101147
102148- If the gates pass: PR #18 's ` memory_recall ` /legacy-concept path is superseded; its native
@@ -106,21 +152,32 @@ alongside recall@5 on every receipt. Every arm keeps the same session ids as `se
106152- Retention becomes an explicit contract: harnesses rotate transcripts within weeks, so the sweep
107153 cadence and a doctor check on "age of last capture" are load-bearing.
108154
109- ## Implementation notes accepted from Stage B (2026-09-10)
155+ ## Implementation notes accepted from Stage B (2026-09-10) — as revised by the council (v1.1)
110156
111- - ` lineage ` edges whose parent is not yet ingested are ** deferred** (` IngestResult.lineage_deferred ` )
112- and land on the child's re-ingest; a stub parent would violate the no-session-without-evidence
113- invariant.
114- - ` content_hash ` covers text and kind, ** not** position, so re-import is a no-op and exact
115- duplicates collapse — the two properties the archive's 6,591 duplicates require together.
157+ - ~~ ` lineage ` edges whose parent is not yet ingested are deferred and land on the child's re-ingest~~
158+ ** Withdrawn (council 6):** reproduced as data loss — the edge never landed when the parent arrived
159+ later. Replaced by ` lineage_pending ` , reconciled in the parent's ingest transaction.
160+ - ~~ ` content_hash ` covers text and kind, not position, so exact duplicates collapse~~ ** Withdrawn
161+ (council 5):** on the archive this collapses 54.7% of user/assistant rows, including every
162+ repeated tool call in a session, so ` retried ` could never fire. Position is in the hash; adjacent
163+ exporter duplicates are collapsed by the adapter and counted.
116164- ` body_sha256 ` is taken over native bytes for ` OBSERVED ` evidence and over the UTF-8 prose for
117- ` REPORTED ` ; the row is a capture receipt of what was actually read.
165+ ` REPORTED ` ; the row is a capture receipt of what was actually read. * (v1.1: raw bytes retained.) *
118166- Hardening beyond the ADR text: ` claim_citations ` CHECKs ` length(quote) > 0 ` and ` end > start `
119167 (a zero-width extent would bind vacuously), and a BEFORE UPDATE twin of the bound-proof trigger
120168 so citations cannot be rebound after the fact.
121169- ` INSERT OR IGNORE ` was rejected for events because it swallows CHECK and FK violations; the
122170 dedupe conflict is handled explicitly and any other violation fails the whole ingest.
123171
172+ ## Council record
173+
174+ Two-family adversarial review of v1.0 + the Stage B store (gpt-5.6-terra REJECT/10 blocking;
175+ deepseek-3.2 APPROVE-WITH-CHANGES/7 blocking). Seven defects reproduced by the orchestrator against
176+ the committed store; dispositions of all 42 findings in
177+ ` docs/architecture/session-memory/receipts/council-adr-0011.md ` . v1.1 is this document. Stage B.1
178+ (store hardening) precedes any corpus ingest; its acceptance tests are the seven reproductions
179+ flipping to refused/correct.
180+
124181## Open questions (to be settled by measurement, not debate)
125182
126183Tokenizer for ` prose_fts ` /claims (porter vs unicode61); whether embeddings on claims clear G4;
0 commit comments