laboratory/docs/DURABLE_MEMORY.md
Claudiu Schuster 1140ffcdf0
All checks were successful
access-validation-contract / contract (push) Successful in 3m21s
ci / ci (push) Successful in 3m55s
Complete native file reads and verify existing memory (#143)
Read native files through pinned, lossless byte continuation and verify existing memory before append. Preserve small text and character-offset compatibility, current caller authorization and the separate public plugin.

Independent source/contracts, exact-tree native E2E and required PR CI passed.
2026-10-18 08:51:50 +00:00

9.2 KiB

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.