Per-run learned-memory seeding for agents on pi, Claude and Codex
Discovered from README cross-links · today
Agents start each run from a git-backed memory folder, injected
What they did
The author built the first half of a learning loop for a multi-harness agent server: the switch, the folder, and the seed every run starts from. Concretely: (1) a `learning: boolean` field on agentic definitions, off unless true, carried on each revision and preserved across edits that omit it, refused on workflow definitions, marked `learning/1` in the served document; (2) a server kill switch `--learning on|off` (default on) or `AGENTRUN_SERVE_LEARNING`, reported as `capabilities.features.learning`; (3) a per-agent folder at `DATA/homes/<agent id>/learning/` in Claude Code's auto-memory format — `MEMORY.md` as a one-line-per-note index plus one `<slug>.md` per note with `name`, `description`, `type` and optional `status`/`superseded_by` — a plain git repo with `main` checked out, created by the agent's first learning run, written only by the server through the existing safe git runner and per-agent queue; (4) a seed step at run start that pins `main`'s commit in `runs/<id>/learning.json`, reads `MEMORY.md` at that commit with git plumbing, and puts its first 200 lines (within 25 KB) into the run's instructions under `# Learned`, plus a copy of the index and active notes into `work/.learning/`; (5) one call site, `withLearned` in `app.ts`, feeding pi's system prompt and claude-agent's and codex's instructions; (6) symlink refusal at the agent home, the learning folder and `learning/.git` via `mkdir` plus `lstat`, and `O_NOFOLLOW` on the `MEMORY.md` write; (7) visible run state `seeded` or `dropped`/`seed_failed`, never blocking a run; (8) format helpers in `packages/learn` (`parseNoteFile`, `activeNotes`, `buildIndex`, `loadedIndex`). They dropped learned skills, the old `learnings/INDEX.md` layout, the 500 on unreadable repositories, and `files.ts` changes. They validated with TAP suites on Node 22 and 24 (154/154 serve, 18/18 recovery, 153/153 learn), a three-harness seed test, a real-adapter claude-agent case against a fake Anthropic endpoint, and eight hand-applied mutants each confirmed to turn the relevant tests red.
How it went
All suites green on Node 22.23.2 and 24.21.0: 154/154 serve, 18/18 recovery, 153/153 learn, with the real-adapter claude-agent case actually running rather than skipping. Eight hand-applied mutants each went red as intended, covering working-tree reads, superseded-note copies, seeding with learning off, throwing on seed failure, the per-try switch race, a leftover pin, `stat` instead of `lstat`, and a missing `O_NOFOLLOW`. Two earlier Node 22 runs timed out on a slow shared disk; the green run used scratch directories on another disk.
Worth knowing
This PR is only the read half: nothing yet writes to the folder, so `main` holds whatever a person or test commits until the end-of-run note-proposing turn lands. Symlink coverage is partial — a link inside a real `.git`, or a swap after the last check and before a git call, is not caught. The run's `work/.learning/` copy is never read back, so the agent cannot edit its own memory mid-run the way Claude Code does. The seed is capped at 200 lines or 25 KB of `MEMORY.md`.
Wiring
Order: definition validation (`validate.ts` `DEFINITION_FIELDS`) → revision build (`agents.ts` `revisionOf`, agentic only, carried in `commit()`'s per-try build beside harness and place, echoed by `definitionOf`) → agent store (`pi-durable-agent/src/agent-store.ts` `AgenticBody` and `NewRevision`, checked in `bodyOf`) → server start (`server.ts` `ServeOptions.learning`, `capabilities(…, learning)`, `features.learning`) → runs (`runs.ts` `RunsOptions.learning`, `RunMeta.learning` read back on load, `seedFor` which never throws, `start()` seeding only when learning is on, `learning` on `view`, `summary`, `summaryOfRow`) → one `withLearned` call in `app.ts` before the harness branch, feeding `systemOf` and `promptOf` → OpenAPI (`openapi.ts` `DefinitionInput.learning`, `Run.learning`, `RunSummary.learning`, `Features.learning`, all `learning/1`). The folder is built by `learning.ts` itself, so `files.ts` and `skills.ts` are untouched. The pin is renamed into place before the run directory is synced, so a dropped run never carries a stale index. Format helpers live in `packages/learn` and are shared with the next slice's checks and merge.
Try it yourself
Add a per-run learned-memory seed to this repo's agent server. Requirements: (1) a `learning: boolean` field on agentic definitions, default off, stored on the revision and preserved when an edit omits it, refused on workflow definitions; (2) a server-level switch `--learning on|off` (default on) or an env var, surfaced in capabilities; (3) a per-agent folder `DATA/homes/<agent id>/learning/` in Claude Code auto-memory format — `MEMORY.md` index with one `- [name](file.md) — description` line per note, and one `<slug>.md` per note with `name`, `description`, `type` (user|feedback|project|reference) and optional `status: superseded|retired` plus `superseded_by` — as a plain git repo with `main` checked out, created on the agent's first learning run, written only by the server through the existing safe git runner and per-agent queue; (4) at run start, pin `main`'s commit in `runs/<id>/learning.json`, read `MEMORY.md` at that commit with git plumbing, and inject its first 200 lines (within 25 KB) into the run's instructions under a `# Learned` heading with one line telling the agent the notes live in `.learning/`; copy the index and only active notes into `work/.learning/`; (5) route that through a single `withLearned` call site that feeds pi's system prompt and claude-agent's and codex's instructions; (6) refuse symlinks at the agent home, the learning folder and `learning/.git` using `mkdir` plus `lstat`, and open `MEMORY.md` with `O_NOFOLLOW`; (7) record run state as `seeded` or `dropped`/`seed_failed` and never let a learning fault block a run — if the copy fails after the pin is written, remove the pin too. Add tests for: three-harness seeding, uncommitted working-tree edits not reaching the run, superseded notes not copied, learning-off and server-switch-off, unreadable folder and unreadable main, the per-try switch race, and symlinks planted at each path.