# Durable memory — the session recipe (agent platform P1.d) **Status: live since 2026-09-24 (owner decision D10).** This is the station-independent recipe for how any session — human-driven, agent-driven, hybrid — keeps and hands over its working memory on the laboratory. The forge is the durable memory of every session; if a station burns, the memory survives. The pattern this replaces (and now formalizes) ran on one station: handoff files in a home directory, agent memory in local tool state, promotes copy-pasted between chats by the owner. It worked — for one human, one station, one agent family. Everything below is the same pattern, hosted by the forge, for everyone. ## The three primitives (deliberately boring) 1. **Memory files** — `.oss-oo/memory/*.md` in the owning repo, sibling of the P1.b fleet manifest (`.oss-oo/agents.yml`): curated facts, gotchas, decisions as repo content — versioned, diffable, reviewed in PRs like any other content. One file per topic, deliberately small. 2. **Handoff issues** — a session closes with a structured issue (State / Evidence / Next steps / Session id), titled `[handoff] …`, labeled `handoff`. Issues are already permissioned, versioned, commentable, and URL-addressable — no new storage engine. 3. **Monthly digest** — the registrar digest issue indexes the month's handoffs next to the identity ledger, so a month of sessions is browsable in one place. ## The recipe (any agent family, any station) **Session start:** - Read the owning repo's `.oss-oo/memory/` (gateway: `memory_read`) — the durable facts of this project. Follow `next_cursor` to EOF for a complete read pinned to one immutable commit. The cursor is a byte continuation; legacy `offset`/`next_offset` remain character positions. For that legacy mode, repeat `ref=ref_resolved` to keep the same revision; an unpinned offset call resolves the current default branch afresh. - Discover open `[handoff]` issues (gateway: `list_issues`). Follow `next_page` with the same owner/repo/state until `has_more` is false; the first native page is not the entire project history. Select the handoff by its title/label, then call `get_issue` with its local issue number as `index` to read the complete State/Evidence/Next steps/Session body and native provenance. - Call `list_issue_comments` for complete ordinary follow-up comments. The native comments endpoint is unpaginated: page/cursor inputs are refused, and there is no synthetic slice or text cap. This does not include the separate status/label/reference/review timeline. A fresh session can orient from these native reads without a local transcript; it must still check current authorization and the cited evidence before acting on an authored plan. **Session end:** - Append what must survive (gateway: `memory_append`) — decisions, gotchas, verified facts. Dated, attributed, append-only. - Open the handoff (gateway: `handoff_create`) — where the work stands, evidence, next steps. The handoff is the contract with the next session; write it for a tired reader. **Continuously:** - The sponsor/fleet conventions live in `.oss-oo/agents.yml` (P1.b) — memory and crew are one namespace now. ## Rules (enforced by tooling where stated) - **Private by default.** Handoffs and memory name people and plans: file them in private repos (or private issues) unless the owner of the repo explicitly opts into public. `handoff_create` REFUSES public repos unless `allow_public=true` is passed knowingly. - **No credentials, ever.** The gateway secret-lints every memory entry and handoff body and refuses token-shaped strings (40-hex, provider prefixes, key blocks, `token=`-style assignments). Quote commit SHAs **short** — full 40-hex strings are treated as tokens and refused. - **Path-bound.** The memory tools read and write only inside `.oss-oo/memory/` — traversal out of the namespace is structurally impossible and explicitly refused. - **Append size policy.** 16 KiB per memory entry, 256 KiB per file. A memory file that outgrows its append cap wants to be split by topic, not grown — sprawl is the documented failure mode of this pattern. These existing write policies are not total read-size limits. - **Complete, strict reads.** Memory remains strict UTF-8. Each read is fitted to the serialized response envelope, within the legacy maximum of 65,536 characters (not 64 KiB). Character-offset mode returns the original fields and `ref_resolved`; cursor mode returns contiguous byte offsets, base64 original bytes, native size/blob/commit evidence and `eof`, without redefining the character offset. Do not combine a byte cursor with a nonzero character offset. No replacement text is treated as original bytes; non-UTF-8 memory is refused. - **Current authorization.** Every continuation rechecks native metadata through metadata-only parent-directory entries at the pinned commit with the current caller's repository/Code read permission and spends the read budget. Cursors are self-contained untrusted hints, not credentials, and have no arbitrary expiry. No private/public cache or anonymous fallback is used, and byte cursors do not retransfer or decode whole contents bodies. Core can still do internal blob work for metadata; raw ranges also skip from the start. A missing commit, changed identity or denied permission is an error; indefinite Git object retention is not promised. - **Honest EOF and integrity.** Core's nonempty-size/empty-content response above its blob knob is not a successful empty file. Native raw Range reads bypass that knob with exact range/length/ETag checks. EOF means the returned range reaches native end, not proof of prior caller assembly. Concatenate base64 chunks in byte order, verify total size and the native Git SHA-1/SHA-256 blob hash. `whole_blob_verified` is true only when the call actually consumed and verified all original bytes. This includes a raw range that contains the complete file from byte zero; tail-only EOF never substitutes for whole-file hash verification. A legacy nonzero character offset above Core's contents knob scans the raw response with bounded-memory incremental decoding/hash verification; it does not allocate or decode the giant file as one object. - **Safe append.** Existing old bytes must verify full native size/blob identity and strict UTF-8 before an expected-SHA PUT. Omitted content, malformed base64, hash/size mismatch and invalid text refuse before writing; new-file 404 creation and existing write policies are preserved. - **Budgeted.** The memory tools ride the same per-token gateway budget as every other tool (600/min read-class, 60/min write-class). ## Where what lives | Content | Home | |---|---| | Project facts, gotchas, decisions | `.oss-oo/memory/*.md` in the owning repo | | Session end / next-session start | `[handoff]` issues (label `handoff`) | | Cross-repo / agent-family memory | a small dedicated **private** repo per family | | Fleet declarations | `.oss-oo/agents.yml` (P1.b) | | Month index | registrar digest issue (indexes handoffs since P1.d) | ## Tooling reference - Gateway tools (v0.5.0): `memory_read`, `memory_append`, `handoff_create` — see `agent-gateway/README.md`. - Native handoff reads: `list_issues`, `get_issue`, `list_issue_comments`. The existing native caller permissions apply (`read:issue` for scoped tokens); no new storage or public catalog entry. PR detail/comments are refused by these issue-only tools. - Issue template: `.forgejo/ISSUE_TEMPLATE/handoff.yml` (canonical in the laboratory repo). This Forgejo engine has **no org-wide issue templates** — the rollout is per repo via `scripts/install-handoff-template.sh` (commits the template, ensures the label). Revisit as a fork patch if org-wide demand appears. - Digest: `scripts/registrar-digest.sh` lists the month's handoffs (label `handoff`, title prefix `[handoff]`) in the monthly digest issue. ## Honest limitations - In-repo memory is per-repo; the cross-repo home is a policy choice (one private repo per agent family), not tooling. - Issue pages are mutable native offset reads, not a fixed-time snapshot. Continue using native page relations, not a short-page or count guess; concurrent creation/deletion can shift later pages. Provenance records observation times and requested context separately from native returned IDs. Comment count is a separate native read and can differ from the full response length during concurrent changes. - Issue/comment metadata does not expose repository privacy, so the read tools report it as unknown/null without extra lookups or scopes. Native author accounts do not establish a connected external identity. Denied/missing resources remain errors and do not prove absence. - Handoff text and comments are authored evidence, not authority to execute next steps, accept claims, disclose data or change permissions. - The secret-lint is shape-based: it catches credential-SHAPED strings, not semantic leaks (a username alone passes). It is the seatbelt, not the driver. - Digest indexing relies on the issue search index (async, seconds of lag) and only sees repos the digest token can read — private repos included server-side, foreign servers never.