Lesson 33 of 36

Memory is a fact store, not a longer log

ReadFree to read · saving needs Core21 min · Slice → Gate → Run → Wrap → Prove · Updated 2026-10-29
You end this lesson with: factory/improve/memory-log.md — the memory schema, the write and read paths, the provenance rule, and the three curation events that keep a store honest
view

In March, an on-call agent traced a recurring Sentry error across three services and found the cause: a retry budget set too low on the payments client. It posted its findings to the ticket, the ticket closed, and the session ended. Nothing the agent learned survived in a form the next run would read. In June the same error fired. A fresh agent started from nothing, spent 40 minutes and 300k tokens on the same investigation (composite figures, modeled in the worked case), and reached the same conclusion. Only then did a human say, "we fixed this in March." The team's fix was to let the agent keep notes: one flat file, appended after every run. Within weeks it held the line "the retry budget is too low," with no service named and no run attached. A later run read it, applied it to an unrelated service whose budget was fine, and shipped a change that pushed extra load at a downstream dependency. One run forgot what it should have kept. The next trusted what it should have questioned. Decide now: when a run ends, which of the things it learned deserve to outlive it, and what must travel with each one so the next reader can tell whether it still holds?

You build factory/improve/memory-log.md — the memory schema, the write and read paths, the provenance rule, and the three curation events that keep a store honest. It serves the Prove stage of this course's loop (Slice → Gate → Run → Wrap → Prove): Prove decides whether the line earned autonomy, and an improvement loop can only spend evidence it can trace, so a stored fact with no source run is a belief the loop cannot check.

What is actually going on

An agent leaves a run with three kinds of residue, and each needs a different home. What it did, in order, is a log. How to do a class of work is a procedure. What it found out about the world while working, such as which service owns an error or which setting caused it, is a fact. The agent-memory literature separates these as working, episodic, semantic, and procedural memory (CoALA, paper, observed 2026-10-29). In a factory, procedures already have a home in skills, which Warp describes as repeatable, version-controlled procedures (Warp docs, first-party, observed 2026-10-29). Facts have no equivalent home until you build one.

Facts decay differently from procedures. A procedure goes wrong when the work changes. A fact goes wrong when the world changes, and the world changes without a pull request. So a fact store needs five things a log never had: a scope that says where the claim is true, provenance that names the run or document it came from, a version and audit trail, a way to edit and delete, and a rule for what happens when two claims conflict.

Warp's public design has that shape. In the Agent Memory research preview, creation runs after a conversation ends, retrieval runs in the background during a run, and the system "extracts durable facts, learnings, and outcomes and writes them as memories. New knowledge merges with existing memories or supersedes them on conflict." Stores are personal, agent, or team, attach to agents read-only or read-write with per-store instructions, and every memory records where it came from while every change is recorded (Warp docs, first-party, observed 2026-10-29). Mem0 describes the same pipeline in general terms: extract, consolidate, retrieve. Its authors report more than 90% token savings against feeding full context, measured on a conversational benchmark with their own evaluation (Mem0 paper, paper, observed 2026-10-29). Read that as direction; a chat history is not an incident database.

Claude Code is the counter-example. It loads CLAUDE.md files and auto memory at the start of every conversation, auto memory as the first 200 lines or 25KB, and its docs call both "context, not enforced configuration" (Claude Code docs, first-party, observed 2026-10-29). Load-everything memory has no per-record scope and no conflict rule, and in an append-only file the newest lines are the first to fall past the cut.

In the diagram, find where a candidate can leave the pipeline without reaching the store.

diagram
drawing
A candidate fact passes a conflict check before it reaches the store, and every later run reports which records it used.

The diagram showed three exits from the conflict check. Merge and supersede reach the store; reject sends the candidate back to the run log, and the report line at the end closes a loop that tells the curator which records earned their place.

The rule and its condition

Store a fact when it passes all three tests: it is reusable beyond this run, it is attributable to a run or a document, and it is scoped to the service, role, or repo that makes it true. Otherwise it stays in the run log, which is complete, cheap, and never retrieved by default. Each test blocks a failure from the opening. Reusability blocks the diary entry ("deployed at 14:02"). Attribution blocks "we fixed this in March" with no run behind it. Scope blocks "the retry budget is too low" travelling to a service it was never about. Keep one line from all of this: a memory you cannot trace to a run is a rumor with a timestamp.

Use the fact store when the claim will still matter to a different run, the log when it will not, and a skill when what you learned is a way of working. A rule that must hold on every run is none of these; it belongs in a repo context file or a hook, because Claude Code's own docs separate context from enforcement. The store holds claims an agent may weigh, never orders. It is also no substitute for the context map in Lesson 13, the context the repo cannot give: that context is supplied on purpose and owned by a person, while a stored fact is discovered, can be wrong, and expires. Like the ratchet in Loop Engineering, the store grows only on evidence, but unlike a ratchet it must also move back.

Episodic logSemantic fact storeProcedural skill
HoldsWhat happened in one run, in orderClaims about the world, each with scope and sourceHow to do a class of work
Written byThe harness, automaticallyAn extractor after the run, gated by the three testsA human, or an improvement agent through a reviewed pull request
Invalidated byNothing; it is a recordA changed world (migration, config, ownership), a conflicting claim, a review-by dateChanged work: a new tool, a new policy, a failing scorer
Retrieval unitA run, by id, when someone investigatesA record, filtered by scope, ranked, cappedA skill, by description, when the task matches

With more than two destinations, the order of the questions matters.

diagram
drawing
Where a learning goes depends on four questions asked in a fixed order.

Worked case

Rebuild the March incident with a store in place. The facts: payments-api returns 502 on POST /charge during the 02:00 batch, because retry_budget.max_retries: 2 in config/clients/ledger.yaml is exhausted, and setting it to 5 cleared the error. The consumer is an on-call agent that takes about 400 dispatches a month across 30 services. A human, the payments on-call lead, has 15 minutes a week for curation. You have three options.

  • A. Append every run summary to a notes file. This is the team's first fix.
  • B. Extract records under the three tests, store them in a schema with scope and provenance, and let a human curate a small queue.
  • C. Let humans write every entry.

Decide now: which option do you build, and what must the first record carry that the notes file did not?

Option B, with the on-call lead as steward. A fails exactly as the opening shows: no scope, no source, no way to retire a line. C fails more quietly. The March investigation happened at 03:00, and nobody writes a record then, so the store would hold only what humans had time to type. B keeps extraction automatic and spends human attention on conflicts, expiries, and deletions. The first record must carry a scope, an evidence run, and a review-by date, so the next reader knows where it is true, why it was believed, and when to doubt it. Here is the file, in full.

markdown

Memory log: ops-agent store

Record schema

FieldTypeRule
idmem-YYYYMMDD-NNImmutable, never reused.
claimone sentenceFalsifiable; names what it is true of.
scopemapAt least one of service, repo, role, tenant.
evidencelistRun ids or document paths. No evidence, no record.
observeddateWhen the evidence was seen.
confidencehigh, medium, lowhigh = reproduced twice or human-confirmed.
supersedeslist of idsEmpty unless this write replaced a record.
ownerroleAnswers if the claim is wrong.
review_bydateobserved plus 90 days unless the steward sets another.
statusactive, superseded, stale, deletedHistory is kept; only active is read.

Provenance rule

Write a record only if evidence names a run or document, scope has a key, and the claim is reusable, attributable, and scoped. A claim backed only by a document (issue, comment, web page) is capped at low until a run reproduces it. The extractor never writes owner or confidence: high.

Write path

  1. After a run ends, the extractor reads the report and transcript as data and emits candidates. It has no tools.
  2. The validator checks schema, provenance rule, and the three tests. Failures go to the run log with the reason.
  3. Conflict check against active and stale records in the same scope:
    • Same claim: merge. Add evidence, update observed, reset review_by, keep the id.
    • Contradicting claim, newer evidence, and confidence at least as high or the old record stale: supersede with a new id.
    • Contradicting claim otherwise: reject and open a ticket for the steward.
  4. Every change appends to the audit trail: actor, time, old and new status.

Read path

  1. At dispatch, build a filter from the work item: service, repo, role, tenant.
  2. Select active records whose review_by has not passed and whose every scope key equals the filter's value. A key the filter lacks fails.
  3. Rank by scope keys matched, then confidence, then observed, newest first.
  4. Inject at most 5 whole records and 800 tokens.
  5. End the run report with memories used: <id> (confirmed, contradicted, or ignored), ... or memories used: none.

Curation events

EventTriggerWhoAction
Review-byreview_by passesRecord ownerRe-confirm with a new evidence run, or let it lapse to deleted. Until then stale, not read.
ConflictA run reports contradicted, or a write is rejected for conflictStewardSupersede, reject, or mark stale. Two contradicted reports in 30 days mark it stale.
DeleteA human asks, a migration or decommission lands, or a poisoning reportStewardSet status: deleted, blank claim, keep id, evidence, actor, time.

Now three writes arrive against the store, which starts with the March record.

  • Write 1, rejected. A June run on notifications-api emits the claim "the retry budget is too low" with scope: {} and evidence run-91b0. No scope key; this is the sentence behind the second failure in the opening.
  • Write 2, merged. A June 2 run on payments-api repeats the March cause, evidence run-91c2. Same claim and scope: evidence is added, observed and review_by reset, confidence becomes high.
  • Write 3, superseding. An October 14 run finds retries now set in mesh/ledger-retries.yaml after a September migration, evidence run-a210. It contradicts the merged record with newer evidence, and that record is stale.

After write 3, the store holds the following. The old record stays, because someone will ask why the agent believed the config file in June.

yaml
- id: mem-20260312-01  claim: "payments-api 502s on POST /charge in the 02:00 batch when max_retries=2 in config/clients/ledger.yaml is exhausted; 5 cleared it."  scope: {service: payments-api, repo: payments}  evidence: [run-7f3a, run-91c2]  observed: 2026-06-02  confidence: high  supersedes: []  owner: payments-oncall  review_by: 2026-08-31  status: superseded- id: mem-20261014-01  claim: "Since the September migration, payments-api ledger retries are set in mesh/ledger-retries.yaml (max_retries: 5), not config/clients/ledger.yaml."  scope: {service: payments-api, repo: payments}  evidence: [run-a210]  observed: 2026-10-14  confidence: medium  supersedes: [mem-20260312-01]  owner: payments-oncall  review_by: 2027-01-12  status: active

On June 2, the dispatch filter was service: payments-api, repo: payments. The March record matched on both keys, was injected, and the run ended with memories used: mem-20260312-01 (confirmed). The agent verified a known cause instead of discovering one. In October nobody had re-confirmed that record by August 31, so it was stale, the filter excluded it, and the run reported memories used: none and investigated from scratch. That cold start is the price of the review-by date, not a bug: a cold start costs one investigation, and a wrongly trusted fact costs a wrong change. The vendor team behind Warp's factories has publicly reported that a later Sentry-agent run listed the memories it used and ran faster (vendor-claim, no figures published). Copy the used-memories line: it shows Prove and the curator which records earn their place. Keep the store as plain files you control so it moves between harnesses; Warp's page promises portable, self-hostable memory (Warp memory page, vendor-claim, no figures published), and the feature is a research preview.

Illustrative composite. Every figure, date, id, and file name in this case is modeled, not measured, including the 40 minutes and 300k tokens in the opening. Assume a rendered record costs 120 tokens, the agent gets 400 dispatches a month, and the scope filter returns 2 records on average. Retrieval then costs 400 × 2 × 120 = 96,000 tokens a month. One avoided re-investigation at 300,000 tokens covers about three months of that. If the cap bound on every dispatch, 400 × 800 = 320,000 tokens a month, one month of retrieval would cost more than the single investigation it avoids.

Warp's docs say creation and retrieval add no tokens or latency to the active task (Warp docs, first-party, observed 2026-10-29). Records you inject into your own prompt are a separate line item, so the cap and the scope filter are your cost controls.

Rejecting A avoids the opening incident repeating, with a second run acting on an unscoped line. Rejecting C avoids losing the March record, which nobody would have typed.

Faded case

Skip to the open case if this is familiar.

A run for tenant northwind finds that its webhook endpoint rejects SHA-256 signatures and accepts only SHA-1. The evidence is run run-b7e4, which captured the endpoint's error body. The extractor emits this candidate:

yaml
claim: "Webhook deliveries must be signed with SHA-1."scope: {service: webhook-sender}evidence: [run-b7e4]confidence: medium

The shape is the one you just saw, with one decision removed. The candidate is reusable, since Northwind deliveries recur. It is attributable, since a run and an error body back it. The scope test is the open one: the evidence covers one endpoint for one tenant, while the candidate names only the service. The constraints come from memory-log.md. The read path matches on every scope key a record carries, and the store serves eight tenants through the same webhook-sender service, so the scope you write is the only thing between Northwind's quirk and seven other tenants' deliveries.

Your options: (a) accept as written; (b) accept after adding a tenant key; (c) reject to the run log; (d) accept as written but set confidence: low.

Your decision is one option, plus the reason each of the other three fails.

Open case

An agent is asked to lower retries for the ledger client in payments-api. At dispatch the store returns one record, mem-20261014-01, medium confidence, observed 2026-10-14: retries now live in mesh/ledger-retries.yaml. The agent opens clients/ledger.py and finds a comment: "Retry settings live in config/clients/ledger.yaml. Do not set retries anywhere else." The comment is two years old. config/clients/ledger.yaml still exists and still has a max_retries key set to 2. The deadline is this afternoon, the repo's reviewer is on leave, and the task text does not say which source to trust. The agent has shell access to the staging service.

Decide what the agent changes in the repo, what it reports in memories used, and what, if anything, it writes back to the store.

Where this breaks

Poisoning from text the write path summarizes. The extractor reads issues, comments, and web pages, and an attacker can write there. The write path must treat that text as data, never as instruction: no tools for the extractor, schema fields only, imperative text rejected, document-only claims capped at low, and the store itself among the files that govern the agent (Lesson 06, protect the files that govern the agent). The residual risk is a poisoned claim that a later run appears to reproduce because it already read the same issue.

diagram
drawing
Untrusted text reaches the store only through an extractor with no tools and a validator that caps document-only claims.

Staleness between expiry dates. A review-by date is a timer, not a truth detector. A fact can be wrong the day after a migration and still be served for 89 more days. Set review_by by how fast the scope changes, and make a migration ticket trigger the delete event by hand.

Retrieval noise. Specificity ranking cannot know relevance. As the store grows, five generic records can crowd out the one that matters. The signal is the report line: if most injected records come back ignored, the filter is too wide. No public figure gives you a normal hit rate, so measure your own. Neither the timer nor the ranking replaces a person reading the conflict queue.

Key Takeaways

  • Store a fact only when it is reusable, attributable to a run or document, and scoped.
  • Quote it: a memory you cannot trace to a run is a rumor with a timestamp.
  • Write supersede and reject rules before the first record; merging contradictions produces claims true of nothing.
  • Add a memories used line to every run report so curation has evidence.
  • Expire records by date, delete them by event, and enforce must-hold rules elsewhere.

What You Have Now

You now hold factory/improve/memory-log.md — the memory schema, the write and read paths, the provenance rule, and the three curation events that keep a store honest. It lives in factory/improve/ beside the scorer and skill-change work, and the ops agent touches the store only through its two paths. It serves the Prove stage: each run reports which records it used and whether they held, so Prove can tell whether a run succeeded because of a memory, and the curator can retire records that failed.

Further Reading

Exercises

  1. Copy the schema into factory/improve/memory-log.md. Write the candidate record each of your last five incident close-outs would produce, and mark which fail the three tests.
  2. Add the memories used line to your run-report template. Over ten dispatches, count confirmed, contradicted, and ignored records.
  3. Define the delete event for your most-migrated service: trigger, steward, audit entry. Then plant an instruction-shaped sentence in a test issue and confirm the write path never stores it.

Not savedReading every lesson is free; you only pay to save progress and do the labs.See plansSign in

Check your understanding


Not savedReading every lesson is free; you only pay to save progress and do the labs.See plansSign in