Skip to content

Commit b8628fd

Browse files
docs(adr): ADR-0011 v1.1 — council-revised after two-family adversarial review
Two seats (gpt-5.6-terra: REJECT/10 blocking; deepseek-3.2: APPROVE-WITH-CHANGES/7) reviewed v1.0 and the Stage B store with an identical brief, run separately. The orchestrator re-ran every probe the seats claimed and reproduced seven defects against the committed store, plus measured the corpus for the one whose severity depended on it (position-free dedup would drop 54.7% of user/assistant rows — 72,949 of them tool-echo markers, so `retried` could never fire). Changes written into the decision (not just the notes): - evidence is one row per prose event (citation surface = the event), append-only - a claim cannot exist without a citation — DB-enforced via deferred FK + trigger - position-bearing content_hash; adapters collapse adjacent exporter duplicates - lineage_pending reconciled when the parent lands (deferred edges were data loss) - prose_fts external content over a filtered VIEW so 'rebuild' stays prose-only - read contract: active / disputed claims; supersession same-session-or-lineage - search planner phrase-quotes every token; raw FTS syntax is a separate API - versions everywhere (adapter, classifier, derivation) + run manifest per receipt; SEALED look runs on a fresh work copy rebuilt from the manifest - derivation rules ship with a hand-labelled cross-harness fixture set Two Stage B notes withdrawn with the reproduction that killed them. All 42 findings and their dispositions (including four rejections with reasons) are in receipts/council-adr-0011.md. No ruler threshold or statistic changed. Stage B.1 (store hardening) is inserted before Stage C; its acceptance tests are the seven reproductions flipping to refused/correct.
1 parent e334a7d commit b8628fd

2 files changed

Lines changed: 257 additions & 35 deletions

File tree

docs/adr/0011-claim-centric-learning-memory.md

Lines changed: 92 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
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):**
44
the retrieval half of PR #18 (`memory_recall` over legacy concepts, ontology as a recall arm).
55
Retains 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

2727
1. **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.)*
3136
3. **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.
3850
5. **Lineage and harness/project are claim metadata**, usable as filters. The tier-1 ontology is
3951
not a recall arm.
4052
6. **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
4760
events(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)
5067
lineage(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
5576
claims(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)
6083
claim_relations(from_id, to_id, kind CHECK(kind IN ('supports','contradicts','corrects')))
6184
review_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
98139
alongside 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

126183
Tokenizer for `prose_fts`/claims (porter vs unicode61); whether embeddings on claims clear G4;

0 commit comments

Comments
 (0)