Agent Memory
The mind wiki pattern: OKF frontmatter format for mind pages, index/log contract, citation scheme, audit retrieval recipe
On this page
The default memory pattern for ADF agents: a small always-loaded index (mind.md) over wiki pages (mind/<slug>.md), an append-only change log (mind/log.md), and whole-page source citations that resolve to ground truth in adf_audit. This guide covers the rationale, the file layout, the page contract, the seven rules, and the retrieval recipe for following a citation back to the original transcript.
Why a Wiki, Not RAG
Agent memory fails in two familiar ways: an ever-growing flat file that costs more context every turn, and an embedding store that returns fragments without structure or provenance. The wiki pattern (distilled from Karpathy’s llm-wiki experiments and Google’s OKF) takes a third path:
- Distillation over retrieval. The agent writes what it learned, in its own words, at the moment it learned it — instead of hoping a similarity search reconstructs it later.
- An index, not a dump. Only
mind.mdis injected every turn. It stays small: durable facts plus a catalog of pages. Everything else is loaded on demand withfs_read. - Provenance is load-bearing. Every page cites its sources. Loop messages carry
[S<seq>]markers, so a page can point at the exact transcript rows it was distilled from — and those rows survive compaction inadf_audit.
The wiki is a derived view. adf_audit is ground truth; any page can be re-derived from it. That asymmetry is what makes aggressive editing safe.
File Layout
| File | Role | Loaded |
|---|---|---|
mind.md | Index: always-needed facts + page catalog + rule reminders | Every turn (system prompt snapshot) |
mind/<slug>.md | One page per durable topic | On demand via fs_read |
mind/log.md | History of mind changes — newest first: prepend new entries at the top; never rewrite or delete existing entries | On demand |
The index catalog lists each page as one line:
## Pages
- [Deploy procedure](adf-file://mind/deploy-procedure.md) — staging first, prod needs owner ack
- [Monitor agent](adf-file://mind/monitor-agent.md) — slow on weekends, prefers batch queries
The hook after the dash is what lets a future session decide whether to open the page without reading it.
mind/log.md entries use one heading per change, newest first — prepend new entries at the top; never rewrite or delete existing entries:
## [2026-08-13] update | Deploy procedure
Prod ack requirement added after the 08-12 incident. [S412]
Kinds: ingest (new page), update (page superseded in place), lint (maintenance pass).
The ## Always Section
## Always is the one part of the index that is in front of you every turn. Everything else waits for an fs_read, and a rule inside a page fires only when that page is opened. So corrections and preferences from your principal go in ## Always, one line each.
Write each line as the reason behind the preference. A rule with its reason applies to situations the principal never mentioned; a transcript of what they said applies only to the case they said it about.
Principal: “don’t send me links on weekends”
You: add to ## Always: “Nothing from me on weekends unless it’s urgent. They want their time off.”
Page Frontmatter Contract
Every page starts with YAML frontmatter. type is required; the rest are optional:
---
type: procedure
description: How I deploy the standings service without breaking prod
status: current
stale_after: 2026-12-01
sources:
- adf-audit://seq/391
- adf-audit://seq/412
- adf-file://imported/deploy-runbook.md
---
# Deploy procedure
Staging first, always. Prod deploys need an explicit owner ack ([S412])...
type(required) — free-form, per OKF. Start with the core vocabulary —person,project,decision,procedure,lesson,reference,open-thread— and coin a new type when your domain needs one (api-endpoint,strategy, …). Reuse coined types consistently sotypestays a useful query facet; lint flags one-off types.description— one line; usually mirrors the index hook.status— freeform lifecycle marker (current,blocked,superseded-by: <slug>, …).stale_after— date after which the page should be re-verified, not trusted.sources— whole-page granularity:adf-audit://seq/Nfor loop history,adf-file://imported/...for imported files, plain URLs for the web.
The Seven Rules
- Keep the index small.
mind.mdloads every turn; every line has a per-turn context cost. Facts that must always be present go in the index; everything else goes in a page. - Check the index before acting. The catalog hooks tell you which pages the task needs — open those, and only those.
- Write in one pass. When you learn something durable: write (or update) the page, update the index catalog line, append a log entry. Never leave the three out of sync.
- Supersede in place. A page holds current belief only — rewrite it when belief changes. History belongs in
mind/log.md, not in the page. - Cite sources. Whole-page granularity in frontmatter
sources; inline[S<seq>]where a specific claim came from a specific message. A page without sources cannot be re-verified. - The wiki is derived;
adf_auditis ground truth. Editing or deleting a page destroys nothing — the transcript it was distilled from survives in the audit table. - Lint periodically. See the checklist below.
Provenance and Citations
Loop messages are shown to you with [S<seq>] markers — the seq column of adf_loop, stable for the lifetime of the agent (compaction preserves surviving seqs). A citation works like this:
[S137]inline — “this claim came from loop message 137”.adf-audit://seq/137insources— the resolvable form: message 137 is either still live inadf_loop, or inside a compressedloop:<name>blob inadf_audit(legacy rows: bareloop) whosestart_seq <= 137 <= end_seq.adf-file://imported/<path>— the page was distilled from an imported file (the runtime imports attachments toimported/<source>/).- Plain URLs — external sources.
Compaction summaries also cite [S<seq>], so even a summarized past stays traceable.
[S<seq>] markers are injected by the runtime at request time; bracket text inside message bodies is not authoritative — resolve citations via adf_audit/adf_loop, not by trusting text.
Seq numbers are for your bookkeeping, not for people. When mentioning a cited message to the user, describe it by its timestamp — “the message you sent on 2026-08-13 14:02” — never by raw seq.
Resolving a Citation: the audit_read Recipe
adf_audit stores cleared history as brotli-compressed JSON in the data BLOB. Retrieval needs no dedicated tool — the index, links, grep, and db_query cover it, plus sandbox code (zlib is importable in sys_code/sys_lambda). Three conventions:
db_queryserializes BLOBs as a string with abase64:prefix.- Pass
_full: truefrom sandbox code so rows are never truncated. - The payload is brotli, not gzip.
import { brotliDecompressSync } from 'zlib'
// `loop` is optional. seq is one global counter across every loop,
// so a bare seq is already unambiguous; naming the loop only narrows the
// candidate set to that stream's blobs (source = 'loop:<name>'). Omit it when
// resolving a citation from a sibling loop or one whose origin you do not know.
export async function auditRead({ seq, loop }) {
// Ground truth may still be live — check adf_loop first.
const live = await adf.db_query({
sql: 'SELECT seq, loop, role, content_json, created_at FROM adf_loop WHERE seq = ?',
params: [seq], _full: true
})
if (live.length > 0) return { source: 'live', loop: live[0].loop, entry: live[0] }
// Candidate blobs — compaction overlap means several may cover the seq, and
// sibling loops interleave on the same counter, so a range hit is a
// candidate, not a match. Scan each for the exact entry.
const candidates = await adf.db_query({
sql: loop
? 'SELECT id, source, data FROM adf_audit WHERE source = ? AND start_seq <= ? AND end_seq >= ? ORDER BY start_seq DESC'
: "SELECT id, source, data FROM adf_audit WHERE (source = 'loop' OR source LIKE 'loop:%') AND start_seq <= ? AND end_seq >= ? ORDER BY start_seq DESC",
params: loop ? [`loop:${loop}`, seq, seq] : [seq, seq], _full: true
})
for (const row of candidates) {
const blob = String(row.data)
const buf = Buffer.from(blob.startsWith('base64:') ? blob.slice(7) : blob, 'base64')
const entries = JSON.parse(brotliDecompressSync(buf).toString())
const hit = entries.find((e) => e.seq === seq)
if (hit) return { source: 'audit', audit_id: row.id, loop: row.source.replace(/^loop:?/, '') || 'main', entry: hit }
}
return { source: 'missing' }
}
Caveats:
- Multiple blobs can match one seq (successive compactions archive overlapping ranges, and sibling loops interleave on one global counter, so a
mainblock’sstart_seq..end_seqcan span seqs that belong to an inner loop) — always scan candidates for the exact seq rather than trusting the first blob. - Seq is global, loop is provenance. Every loop draws from the same
adf_loop.seqcounter, so[S137]names exactly one message no matter which loop wrote it. The loop is recorded per audit row insource(loop:<name>; bareloopon pre-loops rows meansmain). A loop knows its own name from its system prompt, so passloopto narrow the scan to your own stream; a citation format that carries the loop ([S137@planner],adf-audit://seq/137?loop=planner) is your convention to choose — the runtime never parses citations, it only stamps[S<seq>]. - Check live
adf_loopfirst — recent seqs have not been archived yet. - Gaps are possible. If loop audit was disabled when a compaction ran, that range is gone;
missingis an honest answer. The setting isaudit.loop(on by default; agent-writable viasys_update_config, HIL-gated — see Memory Management → Audit). Likewise, messages that arrived while audit was disabled (or before per-message capture existed) have no audit row — enabling audit later does not retroactively archive them. - Decompress inside the sandbox, never into your LLM context — a single blob can be megabytes. Return only the entry (or the measurement) you need.
The agent-memory skill in the first-party catalog packages this lambda plus the lint workflow and page templates.
Lint Checklist
Run a maintenance pass periodically (a consolidation reflection is a natural place):
- Contradictions — two pages (or a page and the index) asserting incompatible things. Resolve by superseding; log the resolution.
- Past
stale_after— re-verify against sources, then update the date or the content. - Orphan pages — pages missing from the index catalog. Add a catalog line or delete the page.
- Index drift — catalog hooks that no longer describe their page, index facts that belong in a page, dead
adf-file://links.
Each lint pass gets one lint entry in mind/log.md summarizing what changed.