Skip to content

ADR-0392: Agent lesson corpus — amending ADR-0018's single-native-store assumption

StatusAccepted — implemented 2026-08-12 under Epic #2382; parked in hypercare to 2026-08-19
Date2026-08-12
DecidersArron + Claude (design pressure-tested via /grillme, 9 branches; approved 2026-08-12)
AmendsADR-0018 — branch B1 only. Not a supersession: the reference/feedback split, the push/pull-by-kind principle and the RAG-over-docs decision all stand.
ContextADR-0018 put corrective feedback_* gotchas in the PUSH layer — native MEMORY.md, always loaded — because a gotcha only works if it is passively present. That reasoning is still correct. Its unstated premise was that native memory is one store. The cc-pool invalidated that on 2026-06-24, and nothing surfaced it for seven weeks.

Claude Code keys its auto-memory store by project path. The cc-pool launches each of five sessions with its working directory at ~/sessions/cc-N — a plain folder that contains worktrees rather than being a repo itself — so the harness falls back to path-keying and every session slot gets a private store. ADR-0018’s PUSH layer silently became five PUSH layers, plus three more from other working directories.

Measured 2026-08-12: 297 lesson files across 8 stores, 297 distinct names, zero duplication.

StoreLessons
/home/pit86
sessions/cc-160
sessions/cc-245
sessions/cc-342
sessions/cc-442
sessions/cc-518
/home/pit/.claude5
/home/pit/ansible3

Zero duplication is the diagnostic finding. Nothing is redundantly stored, so every lesson exists in exactly one silo and any given session can read about 14% of what the estate has learned. A trap learned expensively in cc-3 is unavailable to cc-1, and the same mistake can be paid for up to five times. Nine feedback-* citations in the operating doctrine resolve to nothing from a given session; two of them were never written at all.

This is a textbook instance of the corpus’s own largest failure class — an empty result treated as a fact. Every session’s memory index looked complete, because a store that holds a fifth of the knowledge is indistinguishable from a store that holds all of it.

Decision — the push layer carries generalisations, the pull layer carries instances

Section titled “Decision — the push layer carries generalisations, the pull layer carries instances”

ADR-0018 split knowledge by kind (descriptive → pull, corrective → push). That split is retained. What changes is that “corrective” is itself split by altitude:

  • PUSH — ~/.claude/failure-classes.md, @-imported by CLAUDE.md exactly as soul.md is. A hand-authored set of failure classes: named shapes of self-deception, each generalising many instances. ~1.6k tokens, loaded into every session. This preserves ADR-0018’s load-bearing property — the agent will not self-trigger a search for a fact it does not know it needs, so the corrective layer must stay passively present.
  • PULL — docs/feedback/ in pitlab-docs, indexed by pit-memory, excluded from the site nav. The 297 instances, one page each, carrying the failure class they belong to plus an evidence tier and date. Retrieved on demand by docs_search, and citable by URL.

Pushing all 297 pointers was rejected on the same ceiling argument ADR-0018 itself made: a 15–20k token always-loaded file that grows unbounded is the documented way an agent stops prioritising it. Classes generalise to traps not yet made; pointers only label traps already made.

Capture stays one call. memory_write(kind="feedback") is repointed from native memory to the new tier rather than replaced. 297 lessons exist because capture was cheap; a design that made it an edit-commit-push-wait cycle mid-task would end capture entirely.

Capture asserts its own artifact. The service writes the lesson, indexes it into the live retrieval index, then proves the file exists and that its content is retrievable through the real search path before reporting success. A capture that cannot be proven is refused loudly. This is the in-band control that replaces a capture-rate dead-man: capture rate is behavioural, so any threshold on it is either too loose to fire or gets muted.

Durability rides an existing gate. Lessons are committed to pitlab-docs at /wrapup, which already runs and already commits docs. No service account gets push rights to estate documentation, and no scheduled job or bot is introduced.

The cross-context canary is the control that matters. stacks/pit-memory/testplan.yaml gains a golden-path step that writes under one MCP client context and asserts retrieval by content under a different one. Only an assertion that crosses contexts can fail if fragmentation returns — a same-context write/read passes today, before anything is built, which makes it an assertion satisfiable without the feature.

AlternativeWhy rejected
Supersede ADR-0018Its reasoning was not wrong. Only one premise of one branch failed; recording that as a supersession would discard a still-correct decision and lose the reason the push layer exists at all.
Consolidate the native storesStays machine-local and un-versioned, does not survive a control01 rebuild, and fights upstream path-keying churn indefinitely.
Corpus-only, no push layerConverts every gotcha from a warning that arrives before the mistake into a lookup that only happens if the agent already suspects it needs one — the exact failure ADR-0018 B1 was written to prevent.
Push all 297 pointersBloats the always-loaded file into the range where it stops being prioritised, and grows without bound.
Generate the class file nightlyThe class list changes ~monthly while lessons arrive ~6/week. Generation would add a scheduled job, a dead-man, and a bot with commit rights to the agent’s own operating instructions — to maintain a file that changes twelve times a year.
Reuse docs/_drafts/Conflates two lifecycles: a draft awaits promotion, a lesson never gets promoted.
Publish lessons into the site navThe editorial cost of making each one human-readable is what kills capture.
Fold lessons into the incident corpusMost are technique traps, not incidents; it would degrade incidents_search precision for the records that are.
  • The corrective layer is now two artifacts, not one, and they can drift. A class list that no longer matches the corpus is the failure mode. It is owned by /wrapup, which reviews whether a session’s lessons revealed a class that does not yet exist. There is deliberately no automation behind this: a monthly-changing hand-authored file does not warrant a cron and a dead-man.
  • Lessons become citable. Doctrine can cite the class (already in context) and link the instance (one URL), so a rule arrives with its reasoning attached and costs no retrieval. The nine dangling feedback-* citations become resolvable as the instances migrate.
  • A session that ends abnormally loses that session’s uncommitted lessons. Bounded to one session, and visible as uncommitted files. Accepted rather than solved with a service account.
  • Freshness is demand-driven, not swept. A retrieved lesson past its age threshold is re-confirmed and re-stamped at the moment it is acted on. Re-verifying a dormant corpus on a schedule is patching a machine nobody uses; false staleness warnings train the agent to ignore the warning.
  • No new service, host, image, alert or scheduled job. This extends pit-memory, whose monitoring, dashboard, Loki shipping, Renovate tracking and Trivy/Dependency-Track coverage all apply unchanged.
  • The 297 back-catalogue is migrated at tiered proof, not dropped. Full live re-verification is not achievable — several lessons were learned by breaking production — so each carries an evidence tier. Verifying only what is safe to verify would systematically delete the highest-severity lessons because they were expensive.

Delivered under Epic #2382; design record: Agent lesson corpus.

Amended at delivery — the push layer also carries 12 standing directives (2026-08-12, Issue #2385)

Section titled “Amended at delivery — the push layer also carries 12 standing directives (2026-08-12, Issue #2385)”

The decision above split the corrective layer by altitude: classes are pushed, instances are pulled. Delivery found a third category the split did not account for. Of the 255 migrated pages, 54 are standing directives — settled rulings of Arron’s, not shapes of self-deception — and an audit at close-out found 12 of them stated nowhere in soul.md or CLAUDE.md.

A directive in the pull layer cannot do its job. This ADR’s own argument is that the agent will not search for a fact it does not know it needs; that reasoning applies at least as strongly to a rule as to a gotcha, because a rule’s whole purpose is to fire before the action. Left pull-only, those twelve were effectively dormant.

Amendment (Arron’s ruling, 2026-08-12): promote the load-bearing directives into soul.md as one line plus a link each — the rule loads every session, the incident that produced it stays in the corpus one search away. The remaining 42 stay pull-only as elaboration.

This is consistent with the payload-economy argument rather than an exception to it. The rejected alternative was 297 pointers, unbounded and growing; this is 12 rules, hand-picked against a stated test (is it a directive, and is it absent from doctrine?), costing roughly 700 tokens. What is pushed is still generalisation and obligation, never an index of instances. The /wrapup Check R review owns keeping the set honest, exactly as it owns the class list.