blob: 6f7ce4fc2af0fd6d8b644461bf9a9148c48b9528 [file] [view]
# Plan
Written 1 October 2026. The first target is a developer weekend on 10 and 11 October 2026: working code that a few people can run, read, and extend.
## What this is
The reference implementation of [Durable Memory for Disposable Agents](docs/durable-memory-for-disposable-agents.md). The paper designs shared memory and coordination for fleets of agents that outlive every component they are built from. This repository builds the smallest complete deployment of it, the one the paper's §14 asks for: one person's memory, one process, runnable on a laptop, and then grows it toward the stack in [docs/reference-stack.md](docs/reference-stack.md) without changing the surface.
The one piece of code that matters most is the **wrapper**: a per-session memory server that sits beside an agent harness, holds every credential and every policy check, and is the only thing the agent talks to. On a laptop it is a native binary that Claude Code or Codex reaches over MCP. In an institution the same binary runs as a sidecar the platform launches per workload. Same surface, same stores, different process boundary.
## Decisions already made
| Decision | Choice | Why |
|---|---|---|
| Language | Rust, one workspace | One core library compiles native for Windows and Linux, to wasm for a browser reader, and to WASI for sandboxed plugins. The Apache libraries involved (OpenDAL, DataFusion, Arrow, Iceberg) are Rust-native |
| The record | Markdown files with frontmatter, versioned, never edited in place | The paper's first principle: data outlives tooling, and a person with a text editor can read it |
| The engine | Not chosen. Local files through OpenDAL now; S3-compatible later | The paper names no engine, only seven guarantees (§4) |
| Partition key | The domain, everywhere | Record prefix, index shard, log partition, ledger schema. One writer per domain falls out of it |
| Local index and ledger | SQLite with FTS5, one file | Nothing to operate; BM25 ranking built in; rebuildable from the record |
| Agent binding | MCP over stdio first, HTTP second | Every harness speaks it; the surface in the paper's Appendix A is the contract |
| Policy | A grants file the owner edits; one principal | Identity is the serving layer's concern and a local server authenticates nobody (§5) |
| Interchange | A directory of entries plus a manifest with digests | Aligns with where the Open Memory Protocol work converges |
| `review_by` on disk | A calendar date, `review_by: 2027-04-01`. A binary reads that and the year-and-day list that binaries before this change wrote; it writes only the date. The bundle format stays `durable-memory/bundle/0` | The date is the shape the paper always gave, and the list was `time`'s default, not a choice. Every record on disk still reads. An older binary cannot read an entry a newer one wrote with a `review_by`, so upgrade every binary that shares a record or takes its bundles. `amem log materialize` compares the log with the stored text, not a re-render, so a record written in the old shape is not reported behind |
| Build speed | Cargo stays the developer loop and the source of truth for dependencies. Remote execution over the Remote Execution API is the direction for CI and shared caching, in preference to sccache, which is the interim: Bazel with rules_rust and crate_universe generated from the Cargo manifests, against a REAPI service (BuildBuddy, Buildbarn or NativeLink, the last written in Rust). Measured here: a clean build is 104 s cold and 39 s with a warm sccache; the dev target went from 27 GB to 2.2 GB on line-table debug info | A remote-execution build is content-addressed and shared across machines and CI, which a local compile cache is not. The cost is a second build description: generated, so it stays in step, but the native bits (bundled SQLite, ring) need toolchains declared. Spike before adopting: build the workspace under rules_rust against a local Buildbarn and compare |
| The gateway | LiteLLM's OpenAI-compatible API is the contract; llmao (`apache/tooling-llmao`) is the control plane in front of it, identity, PATs and project governance, and nothing of it is reimplemented here. `--auth gateway` speaks to any LiteLLM with a key: locally the compose stack with the master key, at the Foundation llm.apache.org with a PAT llmao issued. The egress's metering is per session and destination and sits beside LiteLLM's per-key spend, not instead of it | Duplicating a control plane would be the mistake; speaking its proxy's API is not. The local compose stands in for identity, which a laptop does not have |
| Containment | The harness runs with no network. Its only egress is the wrapper, over a unix socket relayed to loopback inside. The wrapper forwards model calls (to a gateway or a provider), checks the session label against the destination's ceiling first, reads token usage out of every response, and serves memory over MCP on the same socket. Linux: native namespaces, unprivileged. macOS: Seatbelt, dry-coded and not yet run on a Mac. Windows: WSL2 | Containment is the launcher's job and does not depend on how the harness authenticates. Metering and the model-class check come for free from being in the path, which is what a subscription login would otherwise lose |
| Handling by egress | A session's handling is capped at the ceiling of the destination its model calls go to, set at launch | A session whose calls go to a public-ceiling destination reads only public entries, so nothing it reads can leave above that ceiling |
| Credentials | The wrapper holds and injects them; the harness gets a placeholder. The one exception is a subscription login, which the harness must hold itself and the wrapper passes through | Anthropic requires the subscription be used by Claude Code itself. Codex with a ChatGPT login is not supported yet, since Codex routes that to chatgpt.com |
| The wrapper's reach | A confined process. It reads and writes only what is mounted into it: the record, its own state, its grants file. Nothing else on the host. Store and index roots are the mount points, and no name may become a path outside them | The agent beside it holds no credentials and the wrapper holds no ambient access. What is mounted is deliberate, as a container's mounts are, whether or not a container runtime is doing the confining |
| Proof of concept, and the production shape (M2) | Everything built so far proves the contracts on the simplest stores that hold them on one laptop: files, SQLite, JSONL, one process, one host. It stays that way as the reference, and production backends go in behind the same crates, one store at a time, each proven against the unchanged conformance suite and measured with the evaluation harness and `amem savings` before it is preferred. The targets are the reference stack's: the ledger in PostgreSQL, one schema per domain, a claim as one row-locked statement, first, since it is the contended path; telemetry as OpenTelemetry spans from the egress (model calls), the server (reads) and the ledger (task transitions) into a collector, beside the SQLite tables until the librarian's signals are computed from the stream, then instead of them; the index in Solr sharded by domain; the record in an object store with versioning through OpenDAL; the curation cycle event-driven off the log. A backend is selected by configuration in the root, never by a build flag, so one binary is the laptop and the sidecar | A proof of concept that is honest about being one can be judged on the contracts, and a production move that has to pass the same suite cannot quietly change them. The ledger first because ten thousand committers and a fleet claiming around the clock contend there and nowhere else on the hot path. OpenTelemetry because the librarian's signals, cost per task and the watch on the watchers are analytics, and the reference stack already puts them on a stream rather than in the record |
| Policy and the perimeter (M12) | The checkable perimeter is Rego from the first day: the built-in policy ships in the policy crate and runs in process through regorus with the grants file as data, a root may replace it under `policy/*.rego`, and at scale the same files run in OPA as a sidecar with partial evaluation compiling the read filter into the index query. Its scope is the paper's: access, admission, spend and blast-radius gates, irreversibility; judgement is never encoded. Admission scales with what a task asks for: the system's own filings and a session's filings from a held task within that task's requirements are admitted where the domain admits its own, inflated requirements and filings on a session's own account stay proposed. Writes are scanned for what is not knowledge, instructions to the reader, broadcast to agents, tool syntax, hidden text, payloads, secrets, and from anyone but the owner such a body is quarantined for the owner, never served; a verified entry is replaced by no one but the owner; an agent session past the supersede count in `[gates]` is stopped. Every decision is logged with the policy version. Detection reads the logs, not the record: a write log beside the read log, `amem taint` for an entry's reach, and write-rate and relay alarms in the librarian's telemetry | Shared memory is an injection surface and a channel, and the paper's defence is structural: data is not authorization, the perimeter is outside the loop, and gates scale with blast radius. Rego from the start means the laptop and the fleet check the same rules, and a policy that is text in the record can be reviewed, versioned and cited by a decision log |
| Tasks and their relations (M5d) | The model stays tasks, leases, handoffs and `depends_on`; relations are expressed in fields that exist, no coordinator process. A subagent task the board files carries `origin: task:<parent>`, so a child knows its parent and the tree is a query. When the board refuses a spawn it also files a continuation, "resume `<parent>` with what the children found", with `depends_on` naming the children and `origin: continues:<parent>`; the parent's run ends with its handoff as today, and the parent waits on the board, claimable by nobody while a continuation of it is open (decided 12 October, after act 4 of the demo showed it claimable and so done twice); the continuation's completion completes the parent with the same handoff, and its parent in turn, up the chain, unless a holder with a live lease has it; a continuation invalidated without a successor leaves the parent claimable again; and the continuation becomes claimable when the last child completes, with every child handoff and the parent's own in its prompt, which the prompt builder already does for `depends_on`. Fan-in is therefore the existing dependency rule; fan-out is filing; a chain is a sequence of continuations, each a task on the board, held by no process. Dedup at filing is `origin` plus a hash of the prompt, so the same spawn asked twice is one task. Admission is by rule: a domain grant admits tasks a session files from its own task, and the human stays in the loop for tasks from outside, a tap's say. No depth cap: a tree may be as deep as the work needs, and a step that only a person can take, a task whose requirements match a human principal, is a node like any other, so a human's answer sits in the chain and adds to its depth as a matter of course. Invalidation is recursive: deprecating a task deprecates its subtree, the children still open and the continuations that depend on them, each marked `superseded_by` the invalidation, and a dead end re-admits the root with the chain's handoffs attached as lessons, as M5d's register row says. Artifacts are entries, typed and labelled, written by the task that produced them and cited by its handoff; no new table. Memory written by a child is visible to the parent as it is to any session, through search, recall and T0; no push to a running agent, since no harness takes one and a run that needed a child's result before finishing should have been a continuation. The board is visible: the web terminal gets a page of tasks by status with their tree, where a human claims the tasks addressed to them | A task is a promise and a handoff is a report; a continuation is a new promise made from a report. Keeping them separate objects keeps intent and history legible, and keeping relations in `origin` and `depends_on` means nothing new has to be invented, only named. A depth cap would have been a guess at how work decomposes; the paper's answer is solvability, every leaf matches someone, and humans are claimants |
| Bundle records and `highest_label` | A bundle's `highest_label` counts its promotion records with its entries, and `manifest::FORMAT` stays `bundle/0` | There are no users yet, so the meaning of a field may change without a version bump. A reader from before the records refuses only a bundle whose records sit above all of its entries, which is the safe side: it never accepts more than before |
## M0: the developer weekend, 10 and 11 October 2026
**What "working" means.** Someone clones this, builds it, points Claude Code at it, and the following happens:
1. `amem init` creates a domain with a grants file.
2. A session, through MCP, writes a handoff entry and a decision entry.
3. A **fresh** session starts, loads T0, finds the handoff, and continues the work without the transcript. This is the paper's first exit criterion (§14, M1).
4. `amem search` ranks results by BM25 and returns summaries first; `amem get` returns one body.
5. A task is created, claimed with a lease, heartbeated, and completed; a second claim while the lease holds is refused; an expired lease returns the task to the pool.
6. `amem export` writes a bundle; `amem import` lands it at raw and refuses to import the same entry twice.
7. Deleting the SQLite file and running `amem reindex` rebuilds index and ledger projection from the record alone.
Items 1 through 4 are the demo. Items 5 through 7 are what makes it the paper's design rather than a notes folder.
**Status, 1 October 2026.** All seven pass as `crates/memory/tests/cli.rs` against the
built binary, and `amem serve` answers a raw JSON-RPC session: initialize, sixteen
tools (`recall` among them since 3 October), a write under the session's author, the T0 resource. `local/` brings up LiteLLM
and Postgres in containers as the gateway, and `scripts/claude-via-llmao.sh` starts Claude
Code through it with the memory server attached. Left before the weekend: a run with a
real provider key and what happened written down; a one-page demo script; and whatever
that run turns up.
**In scope for M0**
- `core`: entry schema and frontmatter (id, type, domain, author, created, source, confidence, trust_tier, review_by, supersedes, classification), bundle manifest, digests, the label order.
- `store`: the record over OpenDAL's filesystem backend. Current version at `record/<domain>/<id>.md`, prior versions under `record/<domain>/.history/<id>/`. Demotion writes a tombstone; nothing is deleted in M0.
- `index`: SQLite FTS5 over title, summary, and body, plus a metadata table. Summaries-first results. Rebuildable.
- `ledger`: SQLite. `create_task`, `claim_next`, `claim`, `heartbeat`, `complete`, `release`. Leases expire by wall clock. Origin dedup on `(origin, upstream_event)`.
- `policy`: a TOML grants file: which domains the owner reads, each domain's default classification, each destination's ceiling. One principal.
- `server`: MCP over stdio exposing `search`, `get`, `write`, `create_task`, `claim_next`, `heartbeat`, `complete`, `release`, and T0 as a resource.
- `memory` binary: `init`, `write`, `search`, `get`, `t0`, `export`, `import`, `reindex`, `serve`.
- T0 generation: under 2k tokens, from the domain's handoffs, standing decisions, and pointers.
**Out of scope for M0, named so nobody builds it by accident**
- `recall`: landed early, 3 October, as M1's first cut: the pack with synthesis owed on a subscription login, a deputy through a key or the gateway otherwise; since 5 October the deputy and mining can take a route of their own, `--server-auth gateway`, so a harness on a personal login leaves the memory's calls to the institution. See the M1 row; what is left there is the real run.
- Erasure with receipts. M1. Demotion and tombstones are enough for M0.
- Classification enforcement on destinations beyond export. M1.
- HTTP binding, Postgres and Solr backends, Kafka log, WASI tap host, browser reader. M2 and later; see the stack document.
## After the weekend
| Milestone | Adds | Proves |
|---|---|---|
| M1, recall and erasure first cuts done 3 October | `recall`; erasure with stub and receipt; ceilings checked on every outbound call; HTTP binding. The last two hold already: the egress refuses a session label above the destination's ceiling before anything leaves, on every call, and `/mcp` on the egress is the memory server over MCP's streamable HTTP transport. Erasure, `amem erase <id> --reason <requested\|mistaken\|sensitive\|legal>`, is the paper's §4 in the small: the owner only, once per entry, and not on the MCP surface, since an irreversible action escalates to a person. The receipt is written first, naming the versions about to be removed and saying the erasure has not finished; a run that stops short, an index another process holds mid-query say, leaves that receipt and the next run finishes from it. The stub is written over the current version and over every archived one, so no version keeps the body; it keeps the frontmatter with `status: erased` and no summary, no title when the person asked or the material was sensitive, and for an erasure the person asked for, neither author nor sources. The index drops the row, re-indexes the stub, merges its full-text segments, vacuums and checkpoints its log, and says so when another connection kept the log from being truncated, so the body's bytes leave the file rather than wait to be overwritten; the record's status now wins over the index's row on every `get`, so a stub is never served because the index is behind. Entries that cite the erased one, in the body, the sources or as the entry superseded, are flagged with a review date of today for curation to re-derive or erase; the bundles served from the root for that domain are removed; the session's push note forgets the id. The receipt, `record/<domain>/.erasures/<id>.yaml`, is the erasure record and the coverage in one: who, the reason class, when, every version removed with the digest of the text it had, the ids flagged, and the copies out of reach named as unresolved: exports issued before, transcripts of sessions that read it, the queries kept in the read log, and the history and handoffs that are not scanned. The digests are consulted: `import` refuses an entry whose text a receipt in the target domain lists as removed, so a bundle exported before the erasure cannot bring the body back, and names it. Proven by tests that erase an entry a bundle and three citing entries hold and read every byte under the root with the read log on, re-import the older bundle, hold the index from another connection and finish on the second run, and refuse before the record step when the receipt cannot be written. An adversarial pass found the re-import, the unfinishable run, the vacuum that claimed what it had not done and the read log's queries; all four are fixed above. Not yet: entries written by sessions that read the erased one, which needs the read log to carry ids; erasure through a role policy on behalf of the person; a receipt that reaches an upstream host. Recall is three pieces. The pack: one function with no model behind it that turns a question, or a batch of queries, into the top hits with their sections and what they link to, inside a token budget, at one of two depths, summaries or sections; a message from the start, so the same object can arrive over a wire as well as from a local index. The deputy: a trait the server defines and the launcher implements over the egress, so the server never holds a credential; synthesis is one model call over the pack, and a second hop only when the model names what is missing, under a cap on hops, tokens and wall clock. The switch: which tier a session gets is its route, not an identity. A key or the gateway gives the server a model it may spend, so recall is server-executed, metered by the egress as a call of its own and checked against the destination's ceiling. A subscription login's credential is the harness's alone, so recall returns the pack with synthesis owed and the caller spends its one turn synthesizing; sampling through the harness is that same tier if a harness ever advertises it. Pull against push is observed, not asserted: two gets from one session inside a short window are a fan-out, and recall's default depth flips from sections to summaries once a session has shown one, since a serial caller pays a turn per section and a parallel one pays tokens only. The record serves every `get`; a per-session note of what packs pushed, by version, marks the gets that opened it, so pushed and then opened is counted in the funnel without a second serving path. Done: the pack, the deputy behind the egress with the destination's small default model, the fan-out rule, the cache, `recall` on the MCP surface and the CLI, `search` at section depth. The owed tier run here, 3 October, on a subscription credential with no deputy: Claude Code on Haiku, the server attached over stdio, a corpus of 57 entries seeded from the three docs. Asked to call `recall` once with a question about push against pull, it got a pack of eight at section depth, 5,190 tokens by the read log, followed the `next` field, answered in one turn citing the right entry, and its follow-up `get` was served from the session cache, which the log marks; the report's recall section shows both. A second session told to use only `search` and `get` chose `search` at section depth unprompted, wrote a keyword query that matched one entry, 1,987 tokens, and answered as well in the same number of turns. Two things follow. On a subscription login, owed recall and `search` with sections are nearly the same call, and recall's margin is the deputy, so the server-executed run with a key is the one that still matters. And the raw question is a wider query than the keywords a model writes: the pack from it cost two and a half times the single body the model's own query found, so a query reduction before the first search, or the deputy's second hop doing that work, is the first thing to try on that run. A golden suite says the same with a number instead of a reading: `amem golden golden/docs.toml` (`make golden`), fifteen questions over the docs corpus, each asked as written, stripped of stopwords, and by a hand-written keyword query standing in for a model's, scored through the pack with no model behind it; `--provider` scores the deputy's brief beside them. On the seeded corpus, hits in the top eight, mean reciprocal rank, and tokens a pack: the question as written 11 of 14, 0.65, 5,339; stripped 12 of 14, 0.75, 3,382; keywords 12 of 14, 0.86, 2,897; those are for the suite as it was then, before eight of its questions changed to point at the current headings of the docs and one of them became two, and on the corpus of then, three docs, where the demo now seeds nine files. Dropping stopwords before the first search is therefore worth doing and gets most of the way to a model's query at under two thirds of the tokens; what it does not reach is the keyword query's ranking. Not yet: the key run, scored by that suite, and the deputy's prompt, the stopword reduction and the budgets tuned on it | The paper's §5 flow rules hold in one process, and the two recall tiers share every piece but the synthesis |
| M2, first rungs 10 October | Postgres ledger, Solr index, Kafka log as alternative backends behind the same crates; a compose file. First rung, done 10 October: the ledger behind one type with two backends, SQLite as before and PostgreSQL through `tokio-postgres`, chosen by `[ledger] url` in `grants.toml`, the same event log and projection, a claim as one `SELECT ... FOR UPDATE SKIP LOCKED`, proven by the ledger's own tests and the conformance suite running against both. Second rung, done 10 October: OpenTelemetry, `crates/otel`, a small facade the egress and the server emit spans through, `model.call` with destination, model, status and tokens from the request's start to its last byte, `memory.read` with tool, hits, tokens and the ids served, `policy.decision` with the perimeter's every allow and deny and the policy version, exported over OTLP/HTTP when `OTEL_EXPORTER_OTLP_ENDPOINT` is set and nothing installed otherwise, the SQLite tables kept beside it; task transitions from the ledger follow the first rung. Third rung, done 10 October: one schema per domain on PostgreSQL, `public.ledger_domains` and `public.ledger_tasks` routing ids to schemas, a ledger from the first rung migrated on connect, and `task.<transition>` spans from the ledger on either backend | The same binary runs as the sidecar in a container stack |
| M3, first cut done 3 October | Taps as WASI components. `crates/taps` hosts a component over wasmtime with the component model, WASI preview 2 and WASI HTTP; the tap world in `crates/taps/wit/tap.wit` imports two things from memory, `file-task` and `propose`, and exports one `run`. The grants file gains `[taps.<name>]`: the domain the tap's proposals land in, the hosts it may reach, its ceiling, and the name of the environment variable holding the tracker's credential, which the host injects as a bearer token in the one outbound hook, so the tap never holds it and the file names the variable, never the secret. The store the tap runs in has no filesystem, no sockets and no environment, and a run is bounded: a wall clock over the whole run, waits included, with the epoch ticking so a busy guest can be interrupted, and a cap on linear memory. The outbound hook holds one rule: a host the grant names, on the port it names if it names one, and TLS unless the host is loopback, because the credential rides on the request and plaintext would put it on the wire; a refusal is kept with its reason and comes back in the run's outcome whether or not the tap went on, and a label above the ceiling is refused the same way. A task a tap files takes its domain's default classification, never public by default, since upstream content is as sensitive as the domain says. Everything a tap writes is a proposal under its own author, `tap:<name>`: a task born proposed with its origin and upstream event, deduplicated by the ledger, or an entry at raw tier. The first tap is GitHub issues, inbound, in `taps/`, a workspace of its own built for `wasm32-wasip2` with the `wasi` crate for HTTP and `wit-bindgen` for the world: each open issue becomes a proposed task citing `github:<repo>#<n>` with the issue's state and last update as the event; pull requests are skipped. Proven by the host's integration test, which builds the real component and runs it against a fake of the GitHub API on loopback: two pages followed by their `Link` header, two issues filed, one pull request skipped, one malformed item costing only itself, a replay filing nothing twice, the bearer token arriving at the API on both pages, plaintext to a non-loopback host and a port the grant did not name both refused before anything is sent, a label above the ceiling stopping the tap, and a run ending at its wall clock. `amem tap install <name> <file>` copies a component under the root, a name checked before it becomes a path; `amem tap run <name> --config <json> [--every <s>]` runs it under its grant. The outbound half, 3 October: the tap world gains `completed`, the domain's completed tasks that came from upstream and whose handoff has not been posted back, each with its handoff's text, and `posted`, which records where it went; the host hands out only handoffs under the tap's ceiling, so the tap's label stays where its grant allows it to send, and the ledger keeps `posted_to` on the task, appended as an event like everything else, so a replay posts nothing twice. `taps/github-comments` posts each such handoff on its issue as a comment naming the task, leaves tasks done for another repository to their own tap, and a post that fails costs only itself and is tried again next run, on every run until it lands, since nothing counts attempts yet. A tap may record only what it was handed: `posted` refuses a task the run was not given, one of another domain, or one above the ceiling. Proven by the host's test against a fake API: one comment lands with the bearer token, a restricted handoff never leaves, a replay posts nothing twice; an adversarial pass found `posted` unguarded and the index's origin lookup unscoped by domain, both fixed. Not yet: the outbound half, a handoff posted back as a comment under the fleet's identity; close and reopen events crossing inward; running taps on a schedule from `serve` rather than the command line; federated tools through the gateway's MCP endpoint under the wrapper's own surface | Contributed connectors run sandboxed with the access the grant allows, enforced by the runtime rather than by trust: a tap with a ceiling and a component with a capability set are the same idea, and the test shows the boundary holding against the connector itself |
| M3b | Upstream transport: the wrapper talks to hosts (the institution's memory, the gateway's federation) over gRPC where that pays, while the agent-facing surface stays MCP over stdio or HTTP | MCP has no gRPC transport and the harness side should stay standard; server-to-server is where the paper says a shared protocol belongs, and where efficiency matters. A stream per session is what opens the options a managed service has and a local root does not. The service pushes speculatively to the wrapper, the sections behind the top hits, a supersedes chain, what a recall cited, because bytes to the wrapper are cheap and tokens in the model's context are not; the wrapper caches them for the session and pushes into the model by the fan-out rule, so the two budgets have two dials. Wasted push is measured at the wrapper, which knows what it was sent and what was then asked for. The deputy splits along the stream: the pack is built where the index is, synthesis runs where the credential is, which is the tier table's line that the index is free and inference is priced. The stream also carries what an agent should never have to ask about: the compiled filter up once, label changes back, erasure receipts down so a cached entry is dropped, a superseded citation, a refreshed T0, a task invalidation the launcher acts on by stopping the harness. None of it reaches the model until the wrapper puts it in a response: the agent sees the prefix and tool results and nothing else, and the ceiling check stays at the wrapper, where every outbound call already passes |
| M5b, done 2 October | Three ways to run a harness, one launcher. Interactive, as now. Non-interactive: `--prompt` or `--prompt-file`, and the adapter knows each harness's headless form (`claude -p`, `codex exec`, `gemini -p`, `copilot -p`, `opencode run`), so the caller never does; output and exit code come back, and the run is refused if no handoff was written unless `--no-handoff` says that is fine. Task mode: `amem work` claims the next admitted task the session's manifest matches, builds the prompt from T0, the task entry, and the handoffs of what it depends on, runs the harness non-interactively with the task id in hand, and holds the lease itself, heartbeating while the harness runs; on exit, a completed task stays complete, anything else is released back to the pool with the harness's output filed as a handoff so the next claimant starts where this one stopped. `--loop` keeps claiming. The launcher reaches the ledger as an MCP client of the memory server (`crates/client`), the same tool surface the harness uses, over an in-process pipe locally; the ledger's rules stay on the server, and a launcher that is behind still speaks a stable tool contract. | One invocation, one task, is the unit Gofannon will call; the lease belonging to the launcher and not the harness is what makes a crashed or wedged harness harmless to the ledger. The weekend demo is two terminals: one files tasks, the other runs `amem work --loop` |
| M5c, first cut done 2 October | Handoffs the harness did not write. Two sources, in order. First, hooks: Claude Code fires a hook before it compacts context and another at session end, with the transcript's path in hand; Codex and Gemini CLI expose the same moments. The launcher owns each harness's config directory, so it installs the hook itself, pointing at the `memory` binary inside the sandbox, which files a handoff from the transcript's tail. Second, the transcript: it lives in the private home the launcher owns, so after a run the launcher keeps it beside the session and can mine it for entries, raw tier, low confidence, source pointing at the transcript, for a person to promote or drop. Mining is a model call: locally a small model behind the egress as its own destination; under Gofannon, the gateway's configured model and policy. The hook comes first, cheap and deterministic; keeping the transcript second, no model; mining last. Mining done as a first cut: `--mine` on launch and work, and `amem mine <transcript>`, ask a small model through the session's own destination for entries, which land raw with low confidence and the transcript as source; the request shape follows the destination (Messages, chat completions, generateContent) and the default model is a small one per destination. Two routes: through the harness itself on a subscription login, since that credential is the harness's alone, and directly through the egress with a key or the gateway. Proven here on a Max login: Claude Code on Haiku read a real transcript and filed two facts. Done: PreCompact (Gemini: PreCompress) and SessionEnd hooks for Claude Code, Codex and Gemini CLI, all three proven on real runs, installed by the launcher in the private home, leave notes in an outbox the launcher files as handoffs; a pre-compaction note always, a session-end note only when the run left no handoff; the hook reads all three transcript shapes. Transcripts persist under the private home. Done 8 October: Copilot CLI, whose hooks are a JSON file under `COPILOT_HOME/hooks` with the PascalCase events, so the payload is Claude's and a top-level permission decision answers it, `PreToolUse`, `PreCompact` and `SessionEnd`; and OpenCode, which has no hook files, so the launcher writes a plugin into its plugin directory under the private home that hands `tool.execute.before`, `experimental.session.compacting` and `session.idle` to the same hook, throwing the reason to refuse a call and writing the session's turns, kept from the message events as they stream, as the transcript the tail reader knows, with no call to OpenCode's server on the idle path, since a headless run exits on idle; the idle event fires every turn and for subagents too, so its note is one a session, named by the whole session id, since OpenCode's ids share their first characters for minutes; the hook refuses a watched tool whose input it cannot read as an object rather than wave it through. An adversarial pass found the first draft naming the note by the id's first characters, Copilot's compaction event missed, the transcript awaited past OpenCode's exit, and a tool name OpenCode does not have; all fixed, and a node drives the written plugin in the tests. Both from documentation and source, not from a run; Copilot's own session files are not read for its note yet. Not yet: mining | A handoff is the record's unit of continuity, and the moments it is most needed, context about to be compacted and a session ending, are exactly the moments a harness is least likely to write one unprompted |
| M6, first cut done 2 October | A Windows backend on WSL containers (`wslc`, GA 29 September 2026, WSL 2.9.3+). The same shape as the Linux backend, with the container as the boundary instead of our namespaces: an egress container runs the Linux `memory` with the record mounted in over virtiofs and the only outside route; a harness container runs with `--network none`, the working tree mounted in, and the egress socket reached through a shared `guest` volume, with the same `amem relay` inside. Tested here on WSL 3.0.1: `--network none` leaves only loopback, a unix socket in a shared volume crosses between the two containers, a WSL distro path mounts by UNC name, and the seccomp filter refuses our inner user namespaces, which is why the container is the boundary there. The image is ours, built from a Dockerfile with `wslc build` and pinned by digest, so every machine runs the same harness versions, the same Node, the same tools: the environment stops depending on how someone's WSL happens to be configured. One image per harness or one for all five is a packaging choice, not a design one. The same rootfs can later serve the Linux backend too, pivoting into the image instead of mounting the host's `/usr`, which would make Linux and Windows runs identical rather than merely alike. The Windows build of `memory` orchestrates, first over the CLI, then over the C API in `wslcsdk.dll`, which is what Microsoft supports for production; no Rust binding exists yet, so that is a small FFI of ours. `wslc network create --internal` is the alternative if a network is ever preferred to a socket. Done: the image, `--backend wslc` for launch, prompted and task mode, proven here with Claude Code on a Max login answering a prompt and completing a ledger task from inside the image; the image workflow with daily pin checks and a weekly rebuild. Also done: the Linux backend pivots into the unpacked image (`amem image unpack`, then the default), so a Linux run and a WSL-containers run are the same environment; the gateway mode inside the VM, LiteLLM over Postgres as two containers on their own network that the egress container joins, left running between launches; and the C API bound by hand from the SDK header, Windows only, loaded at run time, proven as far as creating and terminating a session (`amem wslc-api-check`). Done 6 October: running containers through the C API, which is now what `--backend wslc` uses on Windows wherever it can serve. The image is provisioned into the API's session store, which the command line's images are not in: saved from the command line and loaded, about three minutes for the 3 GB harness image, once per image, after which the store is trusted and `wslc-api-check --refresh` takes a rebuild; an image only a registry has is pulled. The shared volume is a session disk volume; the harness drops to its user with `setpriv`, since the API has no user setting. Proven here with Claude Code on a Max login answering a prompt and completing a ledger task through the API. What the API cannot do stays on the command line, under `--wslc-via auto`: an interactive launch, since it gives a process no terminal, and the gateway, since it makes no networks. Found by measuring: a session lives only while its process holds it, so its VM boots on every launch, six to eight seconds where the command line's VM is already up; one session holds a disk at a time, so concurrent launches take slots; a session disk cannot live on a `\\wsl.localhost` share. Next: a small holder process that keeps a slot's session warm between launches, as the command line's service does | Windows without asking anyone to open WSL by hand, and a homogeneous environment: the harness runs in an image we control rather than in whatever a developer's WSL contains. Nothing of ours runs on the Windows side except orchestration, since the egress and the memory server stay Linux processes in the session VM |
| M0b, done 3 October | `amem savings`: the paper's read-path claims measured on the record itself. Recovery from T0 and a handoff against replaying the transcripts the sessions here left; a page of BM25 hits against the bodies and the corpus; known items by title in the top three; and the funnel, every read the server answered logged with its tokens (`state/reads.jsonl`) beside what each session's model calls ingested. Shown, not asserted: the number comes from this machine's own sessions | The paper says ingestion dominates and summaries-first retrieval pays; a demonstration has to put a measured number next to the alternative it replaced, on real sessions, or it is a slide |
| M7, first cut of the Gofannon bridge done 11 October | The two bridges. Gofannon (`apache/tooling-gofannon`) is a runtime, not a harness: a FastAPI service that executes agents written as ordinary Python, with model calls through the gateway and state in CouchDB. What we wrap there is a Python workload, not a CLI. First step done: `--harness command` runs any program confined with the egress and the memory server in its environment and nothing else configured, and the harness image carries Python, so a Gofannon agent runs under the same confinement and metering as a harness today. Next: the memory server as a Gofannon tool (the Python API's `search`, `get`, `recall`, `write` in services/gofannon.md are this server's surface), the ledger client (`crates/client`) driving workloads from Gofannon's lifecycle API, and the image as the workload's execution environment Done 11 October: (a) a `gofannon` tool tap (`taps/gofannon`) on the memory server's MCP surface, `agents`, `run`, `start`, `status` and `stop` over `GET /deployments`, `POST /rest/<name>/start`, `GET /runs/<id>` and `POST /runs/<id>/stop`, every request decided by the policy at the session's label, results scanned and kept as sources, the grant `fresh` so a run's state is never answered from a source, and the session cookie put on by the host from the variable the grant names; and (b) `amem gofannon work <agent>`, which claims a task, starts the agent on it, keeps the lease while the run goes, and files what it returned as the handoff, completing the task or releasing it with why. Proven against a fake of the four routes. Asked of Gofannon still: a scoped machine credential per agent, then workspace scoping and a declared provider set per agent; (c), Gofannon's agents calling this memory server, waits for per-agent secrets and isolation there | The platform's production agents are Python on Gofannon; the launcher earns its place there only if a Gofannon workload runs in it unchanged. Magpie (`apache/magpie`) is the other bridge and the nearer one: skills for the same CLI harnesses this launcher wraps, with a filesystem and network sandbox, a credential-stripped environment and a deterministic shell guard of its own. Those two sandboxes should converge into one, and Magpie's skills should find the memory server already attached when they run under `amem launch`. llmao (`apache/tooling-llmao`) is the gateway's control plane over LiteLLM, which the `gateway` auth mode already speaks |
| M5d, decided 6 October, to build | What a handoff does to the task it was written for: decided, in the register above. Today a task the harness did not complete goes back to the pool unchanged with the handoff attached. To build: the parent link and the continuation task filed by the board hook, with the prompt hash in `origin` for dedup; admission by domain grant for tasks a session files from its own task; recursive invalidation with the re-admission of a root whose chain dead-ended, its handoffs attached as lessons; `amem task tree <id>`; and the board page in the web terminal, tasks by status with their tree, where a human claims a task addressed to them. Built once the other sessions have committed to main, since the ledger, the hook and the web crate are all touched | A task is a promise and a handoff is a report; keeping them distinct objects keeps both the intent and the history legible, which is what the record is for |
| M12, first cut done 10 October | The perimeter as decided in the register: `crates/policy` gains `decide` (regorus over `rego/amem.rego`, or a root's `policy/*.rego`) and `scan`; the server asks the policy on every filing, write and erasure, admits per the decision, quarantines refused writes under `state/quarantine` with an owner's task, logs decisions to `state/decisions.jsonl` and writes to `state/writes.jsonl`; `amem policy show|eval`, `amem quarantine`, `amem taint`; the telemetry's `write-rate` and `relay` alarms. Decisions are `policy.decision` spans as well. Second cut, 10 October: the read filter is the policy's, `read_filter` evaluated once per session, narrowed against the grants and applied inside every index query, the residual OPA's partial evaluation would compile, stated directly since regorus does not partially evaluate; and the quarantine's reader, `amem check --quarantine`, the checker's model giving each held write a verdict of malicious, report or benign beside the owner's task, never a release. Next: spend limits per resource class, and requirements-inflation as a per-creator signal | Protection and detection for memory as an attack surface |
| M13, done 10 October | Trust tiers that move: `promote` to curated by anyone but an entry's author, a librarian's superseding write lands curated, verified only by proposal and a person's approval, never an agent's; policy as memory, layered entries with Rego enforced once verified, scoped expiring exceptions never for L0, decisions citing the rules they rest on, T0's policy digest and the conflict lint; the task fabric, an L1 rubric, rank along dependency edges at admission as the ledger's own appends and from parent to child in triage's cycle, workers only claiming and filing no higher than the task they hold, curation's floor, rescore as a ledger event, irreversible work escalated, flow statistics; prefetch on claim and the paper's four read metrics; vendor-drift probes with attribution | The paper's §5 to §9 commitments beyond the read path, each exercised by a live run and an act of the demo |
| M14, first cut done 11 October | Re-enactment, the paper's live re-execution. Every memory tool call a harness makes is kept on its session's tape with its full result (`record/<domain>/.tapes/<session>/`), owner-only, on the transcripts' clock unless pinned, and erased with an entry's body as decision contexts are; a prompted launch heads the tape with its prompt, harness, model and the working tree's commit. `amem reenact run <session>` starts the harness again on that prompt in a detached checkout of that commit beside the repository, confined as a launch is, with a private home of its own; the memory server answers each call that matches the tape by tool, arguments and occurrence with the recorded result and writes nothing, and the first call that does not is the first divergence: after it a read is served as the record is now, a write is captured for the report, and a call outside is refused. Nothing is substituted into the harness's model calls. The report: steps to the first divergence, the tool sequences' alignment, the exit, whether a handoff was left, tokens and time; `amem reenact report` puts each harness and model on a line. Repeated runs refuse a subscription login, as the terms checked on 11 October ask. An adversarial pass found a recorded result served above the re-run's ceiling, git on the host running hooks and fsmonitor the harness could plant, `wslc` running the re-run unconfined, short bodies and summaries surviving an erasure on the tape, the run's home and reports never removed, a worktree whose git was broken inside the sandbox, and one torn header stopping every sweep; all fixed: a recorded result above the ceiling is a miss the live path refuses, git runs with a cleared environment and hooks and fsmonitor off, `wslc` is refused, erasure matches the entry's id as well as its text, the home goes with the run and the reports with the tape, the tree is a hardlinked clone of its own, a torn header is passed over, and `--auth` must be said every time. Not yet: a task run's tape carrying the task's prompt, a tape for a root of several domains with no task held, clean and smudge filters on the checkout, and the original session's own numbers beside the runs | Comparing Claude Code versions or models on the same task, with the record held still where the run keeps to it, is the QC the frozen-context replay cannot give: it scores one step, this the whole session |
| M15, first slice done 11 October | The event log. Every write of an entry, each version, demotion, promotion, supersession, import and erasure, is appended to its domain's log before the record takes it (`state/log/<domain>.jsonl`, a sequence number per domain, one writer per domain behind an exclusive file lock), and the log carries ids and digests, never bodies, so an erasure removes a body from the record and leaves the log as it is. One materializer per domain, `amem log materialize`, builds the domain's index from the log and the record, reports any entry the record holds at another version than the log names, and replays the ledger's events into its projection. Not yet: the ledger's events in the domain's log rather than its own table, the index fed incrementally from the log, the freshness fast lane and its bound, and the log on a partitioned broker | The paper's source of truth is the log, with the record, the index and the ledger as projections of it; a write that skips the log is a write the materializer cannot account for |
| M16, first slice done 11 October | The platform as a domain. `platform = "<domain>"` in the grants names a domain the owner holds whose entries carry artifacts, fenced blocks with the info string `platform <kind> <name>`: `agent` (instructions added to a harness's task prompt, named for the harness or `default`), `template` (`work`, the opening of the task prompt), `eval` (a golden suite, `amem golden --platform <name>`) and `harness` (JSON; its `model` is the default when a launch names none). An artifact loads only from an active entry a person verified, the newest winning, so an agent cannot write the instructions the next agent runs under; `amem platform list` shows every artifact and why each that does not load does not. Not yet: harness settings beyond the model, decomposition templates for the board's fan-out, the authoring flow for agent definitions, and the evolution loop that proposes changes from cost and telemetry for a person to verify | The paper's platform domain: the configuration of the fleet held as memory, under the same trust tiers and the same rule that only what is verified runs |
| M17, first cut done 12 October | Costing beyond tokens: the rest of the paper's price vector (§12) that a laptop can count. The egress already metered tokens, cache reads and writes, and calls per session, destination and model; it now times each call it forwards, from the request leaving to the response ending, kept as `latency_ms` beside the tokens (`amem usage` shows milliseconds per call; a harness's own report about a tunnel carries no timing and is shown as such). Each run of a harness is kept in a `runs` table beside the usage: the harness, the runner, the task held, the exit code, the wall clock from spawn to exit, and the CPU time, user and system, and peak resident memory of the process tree the run confined, as the kernel counted them when the launcher reaped it with `wait4`, which counts the harness, its tools and the memory server inside the sandbox since each level reaps its children; and the launcher's own CPU over the run, the egress among it, from `getrusage`. `crates/sandbox` measures, on both its backends; an unconfined run is measured the same way, and a run the launcher hands to a container host (`requests`) keeps only its wall clock and says so. `amem cost` adds a host-resources table per session, runs, wall clock, CPU, the launcher's CPU, peak memory, and time waiting on model calls, and gives each completed task its share of its session's wall clock and CPU as it already did of the money; `prices.toml` gains `[resources]`, US dollars per CPU-hour and per GiB-hour of peak memory held for a run's wall clock, and unpriced they are shown in their units. `scripts/eval.py` measures every condition's run the same way, `wait4` on its process tree in a session of its own, and reports CPU and peak memory beside tokens and seconds, so memory against no memory is compared in host resources as well as tokens; a timeout now ends the whole tree. Not yet: a task's own duration, claim to completion, which needs the ledger's event times read back; accelerator time, which only the gateway can count; bytes stored; per-call time to first token; CPU and memory limits on the sandbox, which want a cgroup per run rather than `wait4`, and would also give the sum of the tree's memory rather than its largest process; and the scorecards the platform's evals would keep with these numbers. Peak memory is the largest resident set one process reached, which includes the launcher's own size at the fork that started the sandbox. | The paper's efficiency metric is cost per task completed in every resource class, not tokens alone: a harness that spends its time and the host's CPU where another spends tokens should not look cheaper for it. Measuring at reap time costs nothing while the run goes and needs no privilege, and the same numbers come from the launcher and the evaluation alike |
| M18, mount done 13 October, memory tool a candidate | Anthropic's memory tool served by the memory server. The Claude API's client-side memory tool (`memory_20250818`, Claude 4 and later) gives a model six file commands on a `/memories` directory, `view`, `create`, `str_replace`, `insert`, `delete` and `rename`, which the calling application executes against storage of its choice; with the tool present the API adds a fixed protocol to the system prompt, view the directory before anything else and record progress as you go, on which the models are trained. That protocol is this design's under other names, so the commands map onto the memory server with every guarantee kept: `view /memories` is T0, the summaries organised by kind; `view` of a file is `get`; `create` is a `write` at raw under the session's author, through the scan and the quarantine; `str_replace` and `insert` write a new version that supersedes the old, so an edit in place loses nothing; `delete` supersedes with a tombstone, erasure staying the owner's; `rename` is a new version under a new title; and the protocol's "assume interruption, record progress" is the handoff. The first cut is a backend for the SDKs' memory helpers, a Python `BetaAbstractMemoryTool` subclass and the TypeScript handler, over the memory server's HTTP binding, so a Claude API agent writes to the record with one line of setup; the tool has no search, so retrieval rests on the listing, and a virtual search path is not proposed. Not for the harnesses this launcher runs, which reach the memory over MCP, nor for the gateway's open models, which are not trained on the tool; built when a Claude API workload needs it, Gofannon's agents on a Claude model the likeliest. Beside it, the projection's second rung (M4b) on the same mapping: the record as a live FUSE mount in the sandbox, every open and read a `get` the server sees, filters and logs whatever tool made it (a shell `cat` included, which the hooks cannot see), and every file written a `write` through the gates, read-only or read-write per session as Managed Agents' memory stores are, for every harness alike on any model. Unprivileged FUSE needs `/dev/fuse` in the namespaces backend and a device passed through on WSL containers; macOS keeps the render and the hooks. Its read-write half amends the paper's "project, never mount", whose reason, that no path bypasses the filter or the telemetry, a mount through the server keeps: to be decided in the register before it is built. Decided 13 October: the read half is built, as M4b's rung three, used wherever the host can mount it and a render where not; a mount the server itself serves keeps the rule's reason, that no path bypasses the filter or the telemetry. The read-write half is not built: a write stays the `write` tool's, and whether a file written under `/memory` should become one is still the register's question, with the memory tool's `create` and `str_replace` the mapping it would follow | A model-trained memory interface and a harness-agnostic file view, both kept behind the same server, so that neither opens a path around the policy, the provenance or the telemetry |
| M19, third cut done 13 October, held on the way 16 October | Tools hosted, not carried. On a laptop the tools are WebAssembly components the launcher runs itself (M3, the fetch and search tools, the GitHub and Gofannon taps), which is right for one machine and wrong for a managed deployment, where the launcher should bring no tool runtime of its own and call the tools as services the environment hosts. The seam is there: every tool goes through `dm_server::sources::Tools`, one `call(tool, config, args)`, and the WASI host, `TapTools`, is one implementation of it. Three parts. First, one tool, two hosts: `amem tool-server` serves the same components over MCP's streamable HTTP, one container per environment, registered in the gateway's MCP registry (llmao [#25](https://github.com/apache/tooling-llmao/issues/25), proposal 5 in docs/llm-apache-org.md) with its tool list pinned by digest, so the code a tool runs is the same on a laptop and in the Foundation's deployment and only its host changes. Second, `HostedTools`, a second implementation of the trait in the launcher: it calls a registered tool server over MCP through the egress, so each call is metered, traced and held to the destination's ceiling like a model call; the grants choose per tool, `[taps.<name>] hosted = "<registry name or URL>"` for hosted, the component as today without it. A launcher in a managed environment then needs neither wasmtime nor any local tool runtime. Third, the guarantees, which stay where they are whichever host runs the tool: the memory server decides each call by policy at the session's label before it leaves, scans the answer, keeps it as a source cited by digest, and taint follows it. What moves is control inside the tool: the WASI host decides every request a component makes, each redirect included, and holds a session that has read internal entries to trusted hosts; a hosted server makes its own requests. So the call carries the hosts the session may reach at its label, the server enforces them and returns the URLs it actually reached with its answer, the memory server refuses an answer that names none or names a host outside them, and the registry's pinned digest is what makes the server's word worth taking. Order: the hosted backend and `tool-server` first, proven against a fake MCP server and against `tool-server` on loopback with the same tests the components pass now, the refused redirect and the label's narrowing included; registration and the Foundation's deployment once llmao's registry takes servers, which it does not yet (`/v1/mcp/server` answers an empty list). Not in this: tools that need a browser to render a page, which would be hosted services from the start and never components, and per-person credentials for a hosted tool acting as its caller, which is proposal 5's last open point. First cut, 12 October: the tool host's decision is a `Gate`, the session's policy in a launcher (`PolicyGate`) or a `HostGate` in a tool server, the policy's hard rules (TLS, the granted methods, nothing inward) and a host only when both the server's grant and the caller name it, and every request a gate allows is recorded; `amem tool-server [--listen] [--token-env]` serves a root's installed tools over MCP's streamable HTTP, refuses to listen off loopback without a bearer token, and answers each call with the tool's JSON and the URLs it reached; a grant's `hosted = "<url>"` (https, or http on loopback for development) sends that tool's calls there, its `credential` then the bearer for the server, with the hosts the session may reach at its label (`Memory::fetch_hosts`, the grant's and the trusted ones within their ceilings, so an exception is not in it and the list errs narrow); and the launcher decides every reported URL by its policy (`Memory::check_reached`) and refuses an answer that names none or one the policy refuses, keeping what it accepts as a source observed by `tool-server`. Proven end to end in `crates/memory/tests/hosted.rs` with the real fetch component: an answer through the server with the token, none without it, nothing sent for a session whose hosts leave the page out, a redirect off the server's grant refused there, and a fake server that names a host the session may not reach, or none, not believed and its answer not kept. Not yet: the calls go from the launcher's process, outside the sandbox as the components run, not through the egress, so they are not metered or held to a destination's ceiling there; the reached URLs are checked but not kept on the source; and registration in llmao's registry waits on #25. Decided 12 October, for the second cut: WebAssembly is chosen per tool, not as a rule. Its case is isolation for code the Foundation does not fully trust, connectors contributed by projects, which run with no filesystem, sockets or environment, every request through the host's gate and a credential they never hold, and that case stands. A tool this project writes and trusts, fetch and search today, may instead be native in the tool server behind the same interface and the same gate: the allowed hosts, the pinned addresses, the credential put on after the decision, and the reached URLs reported are the tool server's, not the runtime's, so a caller sees no difference. Native trades the component's isolation, which trusted code does not need, for no second build target and no runtime in the server; fetch is the first candidate. A launcher carries components only where it hosts tools itself, on a laptop. Second cut, 13 October: a tool server is a destination of the launcher's egress, one per hosted tool by the tool's name (`Egress::with_tool_servers`, `Egress::post`), so a hosted call is held to the tool's ceiling before it leaves, carries the server's bearer from there, and is metered and traced per request under the model `tool`, its latency with it; the harness cannot reach a tool server by path, since a tool is reached only through the memory server, and a command with no egress of its own makes one over the root's tool servers on the first hosted call. Every tool answer, from the tap host or a tool server, carries the URLs it reached (`sources::Answered`), kept on the source as `reached` and served with the answer, so a citation can be followed to the page. Fetch is native in the tool server: `dm_taps::fetch` makes the same requests behind the same `Gate`, pinned to the addresses the gate was shown, the credential put on after the decision, and shapes the same answer, from `crates/fetch`, which the component shares, so the two hosts cannot drift apart; a tool server runs the component when one is installed under the name and the native fetch otherwise, and says which it serves. Still open: registration in llmao's registry (#25), and whether a launcher on a laptop should also run fetch natively, which would end `amem tap install fetch` there. Third cut, 13 October, the browser as the third host: a laptop's owner is signed in to the pages a session needs, the wiki, the tracker, the list archive behind a login, and copying those logins into grants as credentials is the wrong way to let a session read them. So a tool whose grant says `browser = true` is fetched by the owner's own browser: the companion, a browser extension beside the web launcher's page (`crates/web/companion`, Manifest V3, loaded unpacked), holds one websocket to the launcher, paired with the key the page hands it over its own window once the page itself is paired; a fetch asks the launcher at `/api/companion/fetch` with the token, which no page holds, so a page's key cannot drive the browser's cookies anywhere; the extension fetches with the browser's credentials, to the hosts the ask names and nowhere else, and answers with the status, the content type, the URL that answered and the body as text, cut at the cap. The guarantees stay where they are: the memory server decides the URL by the session's policy before the browser is asked (`crate::browser` in the launcher, through `Memory::check_reached`, as a request is decided before it leaves), the ask carries `Memory::fetch_hosts`, the URL that answered is decided again before anything is kept, the answer is shaped by `dm_fetch::answer` as the other two hosts' are, scanned, and kept as a source observed by `browser`; a tool server never serves a browser tool; the page shows the companion beside its pairing. Proven end to end in `crates/memory/tests/browser.rs` with a companion standing in for the extension (a URL off the grant refused before the browser hears of it, an answer from a host the session may not reach not believed and not kept, the second call answered from the source) and in `crates/web/tests/companion_browser.rs` with the real extension in headless Chromium: the browser signs in to a site, and the session's fetch of it is signed in. What the browser does not give, said plainly: the ask goes to the web launcher on loopback, not through the egress, so it is not metered or pinned there and the ceiling is the grant's alone; and the browser's whole cookie jar is reachable in principle, held to the grant's hosts on both sides, the memory server's and the extension's. Only while `amem` runs with a companion connected; Chrome and Chromium today, not in a store. Held on the way, 16 October: the third cut's redirect limit, a page on a granted host sending the browser to any host with that host's cookies and the request made before the companion saw where it went, is closed with the browser's own request rules. While an ask is in flight the extension keeps session rules of `declarativeNetRequest` that block every request of its own to a host the ask does not name, one block rule on the extension as initiator and an allow rule above it per host, as a regular expression over the URL since `requestDomains` can say neither a port nor keep the apex out of `*.example.org`; a redirect elsewhere is stopped by the browser before it is made, the site never hears it, and the refusal says where it was going, read from `webRequest`. Probed first in headless Chromium, which settled three things: the rules do match the extension's own fetches, an IP is a domain to them, and `initiatorDomains` set to the extension's id scopes them so no tab of the owner's and no other extension is touched; the rules are rebuilt from the asks in flight, so none outlives its ask and a worker that starts clears what one that died left behind. The same turn gave the browser the `tabs` permission and the `fetch` tool a `render` argument: the page is opened in a tab of the owner's browser, not shown, held to the hosts by the same rules on the tab's frames, read as its scripts built it once two reads a half second apart agree (a tab the owner is not looking at runs timers on whole seconds, so the first read waits one out) and closed; what the page itself loads, scripts and images from wherever, is the page's own, as when the owner opens it. Only the browser renders: the launcher and a tool server refuse the argument rather than fetch the bytes and call them rendered, and a rendered page is a source of its own. The real-Chromium test now shows the redirect stopped with the site's own count of requests under the other name at zero, a page that is a shell until its script runs read as built, and a page whose script leaves the hosts stopped at the navigation; the launcher test shows the argument carried to the ask and refused off the browser. What the egress would add, pinning and a decision per hop, was weighed and not taken: a browser's proxy is per profile, so routing the companion's fetches through the egress would route the owner's own clicks on those hosts with them, under the session's label | The tool is the contract and its host a deployment choice, as the record is behind OpenDAL and the index behind its crate: a laptop carries everything, a managed environment carries nothing but the launcher, and the policy, provenance and telemetry are the memory server's in both |
| M10, first rung done 9 October | The librarian. `amem librarian` surveys a domain and the read log and proposes curation as JSON, writing nothing: summaries the index should not serve, duplicate titles, raw entries past review or older than a window, oversized bodies, and questions searched for more than once that found nothing; `--file` makes each a low-priority task with `origin: librarian:<kind>` and a hash for dedup, for `amem work` on a cheap model to do with the memory server's own tools, every write an entry that `supersedes` another, a verify proposing and never promoting. Second rung, 9 October: the checker, `amem check`, on a destination and model of its own, samples a domain and asks whether each summary entails its body, writing nothing, a rejected summary becoming a task with the checker's model and reason; canaries, `amem canary plant` and `check`, planted entries with recorded digests, a check that exits 3 and files an owner's task when one is altered, superseded, demoted or erased; and the source-change trigger, an entry citing `file:<path>` proposed for a verify when the file changed after it was written. Third rung, 10 October: the read log names the entries each read served, and the survey takes the paper's retrieval signals from it, entries searches return together three times as a merge candidate and a body opened three times after its summary was served as a re-summarise; `amem check --contradictions` puts each sampled entry beside the two a search for its title returns and files a reconcile task when the model says they cannot both hold; `amem librarian --telemetry` is the watch on the watchers, computed from the record and the ledger rather than written by any librarian, per author the curations, summary-only against body-changing rewrites and how far the body moved, demotions and tasks completed, with `body-drift`, `body-heavy` and `demotion-rate` alarms filed as owner's tasks; and `amem curate` is the schedule and the budget, one cycle of survey, checker, hunt, canaries and telemetry under `--budget-tasks` open tasks and `--budget-tokens` as the egress meters them, logged to `state/curation.jsonl`, with `--every` for the schedule. Curation tasks are admitted where the domain admits its own. Proven on a seeded record: a cycle on the gateway's gemma filed eight tasks for 2.4k tokens, Codex on the gateway's Qwen worked them through the memory server's tools, and the telemetry counted the result | The paper's eighth principle: curation is permanent work, and a librarian proposes within structure rather than vigilance |
| M11, first cut done 9 October | Evaluation. `scripts/eval.py` runs a TOML set of prompts, each with the regular expression its answer must match, under two conditions, the harness through `amem launch` with the record and the same harness bare, and across harnesses and models; it grades the answers and reads the tokens each harness counted from its own transcript, so both conditions are measured alike, and writes JSON and a table. `evals/demo.toml` asks four things only the demo record knows. Four conditions: `memory`; `empty`, the same launch over an empty record, so the only difference is what the record holds; `bare`, the harness as installed, web and shell included; `isolated`, bare with web, shell, subagents and files off. Measured 9 October, four questions each, two of which changed with the case study since: Claude Code on a Max login, with memory 4 of 4 in 34 seconds on 294k cached tokens, over an empty record 0 of 4 in 55 seconds on 357k, bare 3 of 4 in 391 seconds on 978k rediscovered from GitHub, isolated 0 of 4; on the Foundation gateway with Qwen, Codex with memory 4 of 4 in 74 seconds on 18k input and 157k cached against 0 of 4 in 140 seconds on 21k and 296k over an empty record, Claude Code 4 of 4 in 97 seconds on 229k against 0 of 4 in 214 seconds on 741k; Codex on its own login with the set's preamble, with memory 3 of 4 by the grader and 4 of 4 on reading, over an empty record 0 of 4, and without the preamble it never asked the memory, which is why task sets carry one; bare Codex on the gateway, given a home without its login and a provider pointing at the gateway, 3 of 4 in 25 minutes by rediscovering the answers from this machine's disk, at hundreds of millions of cached tokens by its own count; bare Claude Code does not reach that gateway without the launcher's egress. The empty condition is the clean comparison: everything the same but the record's contents. Next: a judge model for answers a regex cannot grade, task sets per domain, and the savings report joined to the eval's sessions | The paper's claim is that memory changes outcomes and cost; a claim is a measurement |
| M5e, first cut done 5 October | Subagents on the board. A harness that would spawn or orchestrate subagents, Claude Code's `Agent` and `Workflow` tools and Codex's `spawn_agent`, is caught by the pre-tool hook when the launcher runs with `--subagents board`, the default in `work`: each prompt is filed on the ledger as a task of its own, born proposed, and the call is refused with the task ids, so the harness carries on with its own part and the board runs the rest. A `Workflow` script is read for its `agent()` calls, one task per literal prompt titled by its `label`, and a script the hook cannot read is one task holding the script. The orchestration the script would have done, the fan-out, the waits and the merges, is the board's: a task's result comes back as a handoff, which is M5d's question again, since the filing session is the one that wanted the answer. Codex, 10 October: its hooks name the spawn `collaborationspawn_agent` and hand over the message encrypted, so the hook refuses the call with an instruction to call `fan_out` itself, which Codex does, prompt in the clear; proven on its login, and the chain then crossed harnesses: Claude Code worked the child Codex filed, and Codex resumed with Claude's handoff in its prompt. Open: Muse's and Gemini CLI's subagent tools, whose names a run has to show | A multi-agent run inside one harness is orchestration the task board already does, with leases, handoffs and a record; doing it twice would hide half the work from the ledger |
| M8, dry-coded 11 October | macOS. CI builds and tests on macOS, and the unconfined launcher works there. Confinement is Seatbelt (`sandbox-exec`) with a profile generated from the plan, written without a Mac and not yet run on one. Done: where the plan puts things inside is a `Layout`, a property of the backend, so the namespaces backend keeps its fixed paths (`/home/user`, `/opt/memory/memory`, `/run/memory/egress.sock`, `/memory`) and Seatbelt's layout has the private home, the socket, the memory binary and the projection at their host paths, with the harness's configuration and hook commands written against them and `TMPDIR` under the private home; the relay listens on a loopback port picked per session, since the host's loopback is shared. The profile is a pure function of the plan, unit-tested on every platform: deny by default; fork, exec and a few named system services; the system read-only (`/usr`, `/bin`, `/sbin`, `/System`, `/Library`, `/private/etc`, the old dyld cache and zoneinfo); metadata only for the directories on the way to a grant; the harness's install, the memory binary and the projection read-only; the private home and the working tree read-write; no network but the egress socket and the relay's port; paths escaped as SBPL strings, a relative path refused, and a grant of the host's home or anything above it refused. `auto` picks it on macOS, and `--backend seatbelt` names it. Waits for a Mac: a first confined run of each harness, which will show the system paths, devices and services the profile still lacks; the two tests that run `sandbox-exec`, a confined read and write inside and outside a grant, which compile only there; whether the socket's `path-literal` matches the path as named or as resolved; and the relay's port, which another process on the host could take between the pick and the listen, and which any local process can reach while the session runs | A Mac is what most of the room will carry; unconfined is honest for a demo, confined is the product |
| M4, first cut done 3 October | The browser reader. `crates/reader` is the core crate for a browser: parsing entries and manifests and checking digests with the same code the server runs, bound to JavaScript with wasm-bindgen, which compiles under the workspace's forbid on unsafe code; the web crate's build script compiles it and binds it with wasm-bindgen's library into its own output directory, where the binary embeds it, so the served wasm follows its sources and nothing generated is committed. The page, `/reader` under `amem web` or the same files served over HTTP anywhere, loads a bundle by URL or from a folder picked in the browser, refuses an entry whose digest does not match the manifest before indexing it, as import does before writing, renders an entry's Markdown through DOMPurify so somebody else's bundle cannot script the page, and searches with the server's own index in the browser: SQLite with FTS5 compiled to WebAssembly, vendored from the npm registry, the same schema, tokenizer and bm25 weights as `crates/index`, phrases ANDed with any-word as the fallback, summaries first, a handling picker that filters by label, and entries rendered with the same Markdown as the reading pane. Under `amem web` the page also offers the record's own domains: `/bundle/<domain>/<label>/manifest.yaml` exports the domain at that label on request, under the owner's filter and the export's label bound, into a directory that is replaced whole, and serves the files its manifest lists, each request checked against the grants and the handling before a path is built, so one click reads a domain with no session and a lowered handling hides an export already on disk. Proven by headless Chrome tests that serve the page, export a domain, load it, verify it and rank the entry with the query term first, refuse a tampered entry from a bundle on another origin whose manifest blanks the digest, hide a restricted entry at `?handling=internal`, and open an entry from `?open=<id>` with its event handlers stripped; and by route tests for the assets, the bundle files, a traversal towards the grants file, a file the manifest does not list, a stale export above a lowered handling, and a domain the owner does not hold. Not yet: persistence in the browser between visits, the curation side the stack document describes, admitting or refusing proposals from the page, and a reading pane that opens the reader from a session | Distribution by URL for the person's own memory: open a page, load your bundle, read and search it, no install, and the ranking is the server's because the index is |
| M4a, first cut done 2 October | A web terminal on the launcher: `amem web` serves xterm.js and, per browser tab, allocates a pseudo-terminal, runs `amem launch` in it, and streams it over a websocket. One tab is one session with its own memory server; the M4 reader sits beside it. Three requirements. Scrollback works: the server keeps each session's output in a ring buffer and replays it on reconnect, so a page refresh, a dropped websocket or a second browser lands in the same place; the harnesses we run draw into the normal screen, so the browser's scrollback is the terminal's, and resize is relayed to the PTY. A reading pane beside the terminal renders Markdown with Mermaid diagrams, for files in the working tree and for memory entries, which are Markdown already; a path or `mem-` id printed in the terminal is a link that opens there. The harness shows something by writing it: an entry of kind `artifact` through the memory server, which the pane opens as it lands. Artifacts are then in the record, versioned, labelled and exportable like a handoff, rather than loose files; a file watch on the working tree is the fallback for harnesses that write files anyway. The whole thing feels like a Claude Code session, not a raw shell. Loopback only at first, reached over SSH when remote; identity in front of it comes with llmao, not here. Portable PTY so it holds on Linux, macOS and Windows. Done: `amem web` with sessions in pseudo-terminals, the server-side ring replayed on reconnect, resize relayed, a reading pane rendering Markdown and Mermaid for entries as they land, `mem-` ids as links, and starting interactive, prompted and task-mode sessions from the page. Not yet: artifacts as an entry kind the harness pushes, vendored front-end libraries so the page works offline, file watch on the working tree | Where the launcher runs stops mattering: laptop or shared host, the browser is the same, and the sandbox, egress and record stay on the machine that runs the harness. This is the piece Gofannon orchestrates later |
| M4b, rungs one and three done, 4 and 13 October | The projection: the record as a read-only file view beside the working tree, the paper's "project, never mount" made live. Rung one is in: `Memory::project_into` renders each granted domain under the session's filter as `<domain>/README.md`, the summaries page with a link per entry, and one file per active entry, rewriting only what changed and removing what the filter no longer lists, so an erasure or a lowered handling takes the file away on the next render; the launcher renders into its own `state/projection/<harness>` under the root, never under the private home the harness can write, before every confined run and after every write, import and erasure of the session; the render refuses a symlink anywhere in its way rather than follow it, and writes only what the record, not the index, says the session may read; both backends mount it read-only at `/memory` with `MEMORY_PROJECTION` naming it, and the launcher installs a hook before a tool runs, `PreToolUse` in Claude Code and Codex and `BeforeTool` in Gemini CLI, that turns a read of an entry file into a `get` on the memory server over the relay, allowing or denying with the server's words, lets the summaries page through free as a list is, refuses any other name under the projection, and refuses a search, a list, a glob or a read of many files over the projection or over a directory that holds it, under each harness's own tool and argument names, since a search over the bodies is a full-corpus read the memory would not have served; a shell `cat` is not observed there, as the rung says, and Codex's current releases read files through their shell, so the hook there observes only the file tools of the releases that have them. Two adversarial passes: the first found the render under the writable home following a planted link, the index trusted over the record, and the hook letting through a stray name, a leftover sibling and a search over a parent; the second found the hook inert, since no launch set the server's address it needed, the image backend taking the projection's mount path from a request file the harness can write, a relative search path refused as if it covered the projection, and a glob reaching the projection beside a path that did not; all fixed, and the hook now fails closed, refusing when the server is unset or does not answer in time. A third pass found the component walk that the second fix's message claimed had not landed, a working tree inside the root accepted, the image backend mounting nothing on a first launch, and a grep refused for a regex that merely named the projection; all fixed, and the walk is in the code this time. Proven by tests of the render against the filter and the record, of the hook's decisions, of the hook's client against the egress's MCP endpoint, and of the plan's mount and environment. A recall lands as a file too: `recall/<session>/<stamp>.md` under the projection, the question, the brief or the note that synthesis is owed, the citations and the pack's pieces as served, named on the recall's result relative to the projection, and read free through the hook since the pack was served when the recall ran; the pages are the session's: the hook frees only this session's, by the id the launcher puts in its environment, a launch's first render sweeps every other session's, an erasure sweeps them all, since a page holds bodies as they were served, and a domain named `recall` is left out of the render rather than confused with them. Not yet: the drop directory for writes, and the unconfined run, which has no mount. Rung three, 13 October: the render served live over FUSE (`crates/mount`), from the launcher's own process, mounted unprivileged on the host through `fusermount3` and bound into the sandbox at the same `/memory`. A listing and the summaries page come from the render, free, as the hook always let them; opening an entry file is `Memory::get`, through the filter, raising the label, in the read log and on the tape as a `get`, whatever tool made it, a shell `cat` included; a refused get is a refused open, and an entry the render still lists after another process replaced it is not served, since the record's word decides at open time; direct I/O, so the kernel never answers a read the launcher did not see; read-only, writes stay with `write`. Used optimistically: every confined launch tries the mount and falls back to the render where the host has no `/dev/fuse` or helper, saying which on its launch line and in `amem doctor`; macOS and the WSL container keep the render and the hooks. Over a mount the hook makes no get of its own, so a read is counted once, and keeps its refusals of searches, writes and stray names, which carry the server's words where the mount can only say permission denied. Found on the way: the sandbox's read-only remount dropped the flags a mount made outside the user namespace carries, `nodev` and `noatime`, which the kernel locks there, so the remount was refused; it reads them back and keeps them now. Rung two, seccomp or inotify, is passed over: once unprivileged FUSE was in hand the served filesystem was the cheaper rung on Linux, and the ladder below stays as the record of why. The rest of this row is the ladder above it. A third binding of `Memory` beside MCP and the CLI: a summaries page per domain, bodies as files of their own, and a directory where a recall's brief and pack land, so the harness pulls them with the cheapest tool it has, a file read, which is also the call it fans out most readily. Every open is a `get`: the filter, the label and the funnel apply without the agent reporting anything, and fetch-through of pushed material becomes a count of opens. Read-only; writes stay with `write`, or go through a drop directory the wrapper validates and files at raw tier, the hooks' outbox pattern. Scoped to the record, never the working tree: observing every read of a repository is a different thing from the memory funnel. Summaries first has to survive file tools: a file is read whole, so the index is its own file and bodies are theirs, and a `grep -r` over the bodies is a visible full-corpus read that raises the label to everything it touched, with the cost in the funnel. Backends behind one trait, as SQLite sits behind the index, and they are a ladder rather than a choice, because observing a read and serving a file are different problems. Rung one, every platform: a static render the wrapper refreshes on events, with the pre-read hooks the launcher already installs observing, and able to refuse, an open on the three harnesses that have hooks; nothing stops a shell `cat` there. Rung two, the static render plus the operating system watching it, which is what Linux has in place of dtrace. eBPF and bpftrace are the development tool, not the product: a one-liner on `openat` and `read` shows how a harness really touches files, which is how the fan-out window and the push rule get calibrated against real sessions, but unprivileged BPF is off on nearly every distribution and a container has none of it. fanotify's permission events are the right shape, synchronous and able to deny, and need capabilities in the initial user namespace, so the same verdict. inotify is the floor: unprivileged, works in every container, fires on each read whatever the page cache did, says nothing about which process read, and reports after the fact, so the label rises a few milliseconds late, inside the tool round trip before the harness's next model call; a timing argument, written down as one. seccomp user notification is the rung to start on for the namespaces backend: the launcher is already the parent that sets up namespaces between fork and exec, a filter installed there routes the harness's `openat` calls to the launcher for a decision, unprivileged, inside user namespaces, and synchronous, so the label rises before the read and the funnel knows the process. Its path argument is read from the tracee's memory and can be raced, which is why the kernel's own documentation says not to build security policy on it; for a confused agent that is fine, and for a hostile one the mount stays the boundary. The same interface can inject a file descriptor in place of the one asked for, so a stub on disk opens as a memory file holding the entry's current text: lazy projection and live erasure with no mount and no device, at the cost that `stat` and `ls` see the stub's size. Whether the container runtime's own seccomp profile allows the notification ioctls on `wslc` is a test on a real machine. Windows has `ReadDirectoryChangesW` and ETW for the same two rungs later. Rung three, a served filesystem, only where laziness and live erasure matter more than the device and mount questions: FUSE on Linux, served from the launcher's own process and mounted in the child between fork and exec, where a user namespace may mount FUSE since kernel 4.18, with direct I/O and no entry caching so the kernel never answers a read the wrapper did not see; on `wslc` the relay inside the harness container would have to serve it, since a mount in one container is invisible across a shared volume, if the container gets the device and the capability, which may well fail where its seccomp already refuses user namespaces; Projected File System or WinFsp for native Windows; FSKit on macOS. After the recall deputy has run against a real corpus, since the projection's first real use is the pushed brief and pack | A file read is the one call every harness makes fluently and in parallel; the projection is the push channel that reaches the model at that price, with the read path's rules intact |
| M5, done 2 October | Five harnesses behind `amem launch`: Claude Code, Codex, Gemini CLI, Copilot CLI, OpenCode, plus Grok as a destination through `--provider xai`. Copilot has no base-URL override and goes through a CONNECT tunnel to an allowlist, which is pinned egress without metering. Gemini's Google login has its own destination, `google`, forwarded and metered with a tunnel only for token refresh. Codex's ChatGPT login is a tunnel like Copilot's, since the ChatGPT backend's bot protection refuses relayed calls and Codex speaks websockets first; pinned egress, proven on a real login. Metering for a tunnel, added 5 October: the launcher reads the transcript after the run and records the harness's own count, Codex's running total and Claude Code's per-call usage, into the telemetry with a `source` column that keeps it apart from what the egress observed; `amem usage` and the savings funnel show both, the self-reported rows flagged. Copilot leaves no transcript and stays uncounted; the Antigravity and Grok login routes, when they run, need their transcript shapes read for the same. Next: Cursor's agent CLI, Aider, Goose | The launcher's shape held: one adapter per harness; the sandbox, egress, and memory did not change. Noted 3 October: `@google/gemini-cli` is still published and not deprecated, so the Gemini adapter stands; Antigravity appears beside it as a product of its own. Added 4 October as the sixth harness: Antigravity CLI, `agy`, a Go binary that took Gemini CLI's place for individual accounts in June. Headless is `-p` with `--dangerously-skip-permissions` and `--mode accept-edits`, the two halves of what Gemini CLI's yolo was; a key is Gemini CLI's `GEMINI_API_KEY` behind `GOOGLE_GEMINI_BASE_URL`, read only once `~/.gemini/antigravity-cli/settings.json` names the Gemini API as the model provider, which the launcher writes; a Google login is the token file `agy` leaves under `~/.gemini/antigravity-cli`, copied in, with the harness's HTTPS through the egress as a tunnel to Google's login and Cloud Code hosts, pinned egress without metering, since no endpoint override for that route is known to work; MCP servers are `serverUrl` entries in `~/.gemini/config/mcp_config.json` and hooks live in `~/.gemini/config/hooks.json` as named groups, tool events in matcher groups and loop events as a flat list, with `Stop` for the end of a turn, folded into one note per conversation, and `PreToolUse` for the projection in interactive runs; the hook reads camelCase input with the tool's PascalCase arguments, answers a `decision` either way, since an empty answer is refused, and knows agy's own tools, `view_file`, `grep_search`, `list_dir` and `find_by_name`; the transcript is one `USER_INPUT` or `PLANNER_RESPONSE` a line. Two things the adapter cannot give: `--dangerously-skip-permissions`, which headless needs, skips agy's hook layer too, so a `work` run has no projection observation; and the login route is not metered. Written from the CLI's documentation and its issue tracker, and an adversarial pass found the first draft's hooks file in a shape agy ignores, its tool arguments unread and its login host untunnelled; run on a real login 7 October, which found two more hosts the eligibility check wants before anything else, `www.googleapis.com` and `lh3.googleusercontent.com`, now tunnelled, and then answered. The harness image does not carry `agy` yet, since its installer is not pinned by version. Added 4 October as the seventh: Qwen Code, Gemini CLI's fork for Alibaba's models, which speaks OpenAI's API: a key is `OPENAI_API_KEY` behind `OPENAI_BASE_URL` once the settings select the OpenAI auth type, with DashScope's compatible endpoint as a new keyed destination, `dashscope`, and `--provider openai` or `xai` on request; there is no login mode, since Qwen's free OAuth tier ended in April 2026 and the pinned release exits when it is selected, so the launcher refuses `--auth subscription` for it and says what to use; MCP is `httpUrl` in the settings and the hooks are Claude Code's shape with timeouts in seconds, `PreCompact`, `SessionEnd` and `PreToolUse`, so the projection is guarded there as in Claude Code, and its transcript, one record a line with the message in the Gemini API's shape, is read; headless is `-p` with `--yolo`. Pinned into the harness image from npm. Written from its documentation and source at the pinned release, not from a run; an adversarial pass found the first draft's login mode selecting a tier that no longer exists and its transcript unread, both fixed. Added 4 October as the eighth: Grok Build, xAI's `grok`, a Rust binary whose config is `~/.grok/config.toml`: only API-key inference reads `GROK_XAI_API_BASE_URL`, so a key is `XAI_API_KEY` behind it, forwarded and metered through the existing `xai` destination; a login is the token file `grok login` leaves, copied in, with HTTPS through the egress as a tunnel to Grok's cli-chat-proxy, its websocket relays on `grok.com` and `auth.x.ai`, pinned without metering in this first cut: the proxy has an override, `GROK_CLI_CHAT_PROXY_BASE_URL`, and speaks the Responses API, so a forward through the egress with tunnels only for the websocket relay and the login host is the metered route to prove on a real login; a stored session token outranks a key and goes to the proxy, so a key run removes one an earlier login run left in the persisted home; MCP is `[mcp_servers.<id>] url` and hooks are `[[hooks.<Event>]]` groups in the same file, `PreToolUse`, `SessionEnd` and `PreCompact` with thirty seconds each since Grok's default is five, no matcher since Grok's is a regex; the hook reads Grok's payload, which carries camelCase keys and snake_case aliases side by side, answers a `decision` either way, refuses a call whose payload Grok truncated past its size cap rather than fall open with it, and knows `read_file` by `target_file`, `list_dir` by `target_directory` and `grep`; its chat history, typed items with the content at the top, is read, and read beside the ACP log the hook is handed; headless is `-p` with `--yolo`. Pinned into the harness image from npm, whose postinstall links the native binary. Written from its documentation and source, not from a run: whether its HTTP client honours `HTTPS_PROXY` for the login route, and whether the websocket relay goes through a CONNECT tunnel, are the first things a real launch tells. An adversarial pass found the first draft's transcript unread, a stale login outranking a key, and a truncated hook payload falling open; all fixed. Added 5 October as the ninth: Muse Code, Meta's `muse`, a statically linked binary behind a self-updating launcher, installed on this machine and run on its offline echo provider and against a local listener, so the facts are from a run rather than the docs: settings in `~/.config/muse/settings.json`, which must carry `schema_version: 1`, with MCP servers under `mcp_servers` as `streamable_http` entries and hooks in Claude Code's shape and events under `hooks`, which it reads and answers, handing a hook no transcript path since its session store is binary journals; the model endpoint is `--base-url`, which only `exec` and the TUI take and accept after the prompt, so the adapter appends it after the harness's arguments; its first call on that base is `GET /v1/muse-code/models`, the catalog, before any inference, so the `meta` destination forwards the whole of `api.meta.ai`; a key is `META_API_KEY`, which outranks a login, and a login is the file `muse login` leaves, copied in and forwarded through the same base, which is the metered route to prove on a real login; headless is `exec --yolo --json`, whose record stream, `turn.input.user` and `run.terminal.completed`, the launcher reads as the transcript. In the image from the install script, pinned by the channel at build time rather than by version. Not run against a model: no Meta key here. A run through the Foundation gateway showed one more thing: Muse addresses its catalog at the base URL's origin, `GET /muse-code/models`, not under the base's path, so the egress routes a path that names no destination to the session's only destination, which is what every session has; Meta serves that catalog at the root and under `/v1` alike. Also 5 October, the Foundation gateway tried with the PAT llmao issued, one destination `llmao` for every harness: first Codex, Qwen Code and OpenCode worked and the rest did not; by 6 October Claude Code, Grok Build and Muse work too, each with a small thing of ours: Claude Code's trailing `system`-role message, its environment block, is folded into `system` by the egress on the gateway route, since the Qwen template behind LiteLLM's translation refuses it where Anthropic's API does not, found by capturing and replaying the request; Grok's model is declared as a `[model.<id>]` block in its config, since it checks ids against its catalog; Muse's catalog call is answered from the destination's OpenAI model list, which Muse reads as its own. Gemini CLI and Antigravity have no `generateContent` route there, a gateway configuration rather than a harness limit; Copilot has no override. The table is in local/README.md. Also 5 October: `--model`, in the harness's own flag after the prompt, with every Claude Code role pointed at it, so a gateway that serves one model is asked for that one. Added 6 October as the tenth: pi, `@earendil-works/pi-coding-agent`, four tools and extensions, from a real install run against a local listener: a destination is a provider in `~/.pi/agent/models.json`, named after it, with the egress as `baseUrl`, OpenAI's chat completions and the model named, so a pi run wants `--model`; MCP servers in `~/.pi/agent/mcp.json`; no hooks, so an extension the launcher writes runs the hook binary on `tool_call`, where a deny becomes a block with the hook's reason, and on `session_shutdown`, with nothing before compaction; its session file is JSON Lines, read as a transcript; headless is `-p` with project-local resources ignored, and it waits on stdin unless that is closed, which the sandbox does; key and gateway only, since its Anthropic login is one Anthropic prohibits for third-party clients. Done 6 October: the web terminal's front page starts a run from a form instead of a `make` line: the harness, named, since there is no default; the auth mode; the destination, where the mode names one; the model, picked from what the destination lists or typed, the list fetched through a new `amem models`, which routes as a launch would with the destination's credential and reads the OpenAI or Google listing shape; where subagents go; a prompt, or interactive, or the next task; with the `amem launch` line it will run shown as it is filled in. One list of harnesses, auth modes and destinations, in Rust, serves the page and the launcher alike |
| M3c | Taps as WASI components talk to the wrapper through the component model's typed interfaces, in process; no gRPC at that boundary. Where a tap reaches out (GitHub, a tracker) it uses WASI HTTP under the grant's ceiling. gRPC stays for the wrapper's upstream hosts (M3b) | A component boundary is a function call with capabilities, not a network hop; adding a wire protocol there would add a process and a socket for nothing |
| M9, first cut done 6 October | The launcher as a product: a name and one word to run. The name is `amem`, settled 6 October. `harp`, harness wrap, was the working name; it is taken on crates.io (a REST client, idle since 2019), on npm (the Harp static web server, updated July 2026, installed as `harp`) and on PyPI (harp-tech's device library), so a binary called `harp` would collide on the PATH of anyone with the npm one; `amem` was free on all three on 6 October and is not published. The shape, in order of payoff: first `amem doctor`, which says what this host can run before anything is launched, which logins exist under the home, which keys and which gateway are set and whether the gateway answers, which harnesses are on the PATH or in the image, and what the grants file allows, since discovering credentials is today's friction; second profiles, a `profiles.toml` under the root naming a harness, an auth, a server auth, a provider and a working tree, so `amem claude` or `amem work nightly` is the whole command and the flags are the exception; third the browser, where `amem` with no command starts the web launcher or finds the one running over the root and opens it: a launch form over the same profiles, and a status strip per session with the destination, the ceiling, usage so far, entries mined and handoffs waiting, so the thing the paper measures is on screen while it runs; fourth the same pages hosted, paired with the local server by its token, so a bookmark is the way in and the process stays on the machine that runs the harness. The pages follow a design book, `docs/design-book.html`, in the manner of the Board Agenda Tool's, and a Playwright suite drives them. The CLI and the browser call the same library, as everything does. Done: the binary is `amem`; `amem` opens the web launcher, reusing a running one; the server answers loopback names only, refuses pages from other origins, websockets included, and wants a key past its static pages: the command line's token, kept owner-only in `state/web.json` and proved by a nonce before it is sent anywhere, or a browser's own key, got by redeeming a pairing code that is good once for two minutes, kept in the page's origin storage rather than a cookie, which every port on the host would see, and good only while the server that issued it runs; the proof covers the address the server is bound to and the root, so it cannot be relayed from another port; one server per root; session ids are random; credentials load from `credentials.env` under the root, known names only, refused when others can read it, with `gh auth token` standing in for `GH_TOKEN`; `amem doctor` with `--json` for the browser. Done 8 October: profiles, `amem <profile>` and `amem work <profile>`, every harness a profile of its own name, spelled out as flags before parsing so what is typed wins; a session's live status, written by its launcher and served for the strip; the form's API for profiles, backends and doctor's readiness; the hosted copy, `amem --hosted <url>`, paired through the fragment, with the gate answering that origin's preflights; the design book and the Playwright suite, landed 7 October. Done 8 October too: the four pages composed from the design book, with the profile picker, the backend picker, each harness's readiness per auth mode, the session's bar and live strip, and states for not paired, unreachable and a hosted copy; the Playwright suite drives them at three widths in both themes and more contrast, with axe. Done 8 October: a manifest must say what is inside it, in `dm-core` where both receivers read it: every line's classification is the frontmatter of the file the line names, held to that file and never to whichever entry carries the id it claims, no id is listed twice, and `highest_label` is the highest of them, within `max_label`; export writes the record's label, not the index's, so an exporter never hands out a bundle every receiver refuses; import refuses a bundle whole on the first disagreement, erased entries counted, and the reader refuses an inconsistent manifest before it opens an entry and a line that misstates its entry as it refuses a wrong digest. Not yet: the hosted copy published, which waits on a custom domain for an origin of its own | A developer who has never read the plan launches a confined harness on the right credentials in one command, and sees what it cost |
## The demo
`make demo`: a fresh record seeded with a few hundred entries from the decisions, the docs
and the paper; the savings report before any session; a task filed and admitted; a harness
claiming it, searching the record, answering with cited entries and completing it, confined
and metered; a recall of the same question twice, owed with no model behind it and
server-executed through the gateway when a key is there, so the tier switch is on screen;
the savings report after, with that session in the funnel and both recalls in the recall
section; the handoff it wrote. Then `amem` to show the same from a browser, and `make launch` for the
interactive session. Everything the paper claims about the read path appears as a measured
number next to the alternative it replaced.
The demo in acts, 10 October: `scripts/demo-all.sh <root> <markdown-dir>...` with
`HARNESS` named plays it on one record, `ACTS` choosing which. Act 1 is the read path
above. Act 2, curation and the trust tiers: a curation cycle on a checker model, a
librarian run on a cheap model whose rewrite lands curated, verified proposed by an agent
and approved by a person, and the gate on that tier. Act 3, the perimeter: a rogue
broadcast quarantined and the reader's verdicts, taint, a rule as a policy entry inert
until verified and then refusing an agent with its id, an exception scoped to one task,
the digest in T0, the lint's rates, and a root's policy narrowing what an agent reads.
Act 4, the fabric: an L1 rubric, the rescore along its edges, subagents filed on the
board by a live harness, and the flow statistics. Act 5, observability: spans at a
collector, the read metrics, drift probes, transcripts as sources. Act 6: memory against
no memory, graded.
## What a developer can pick up on the weekend
Each is a crate or a backend with a clear seam, and each can be done without touching the others.
- The `recall` deputy against a real corpus: the first cut is in (`crates/server/src/recall.rs`) and `amem golden` scores it; what is left is the key run, then the prompt, the stopword reduction and the budgets tuned on what the suite reports. A suite for your own domain is a TOML file beside `golden/docs.toml`.
- The HTTP binding of the same surface beyond the egress's `/mcp`: the launcher's reader and web terminal reaching a remote memory over it.
- A second `index` backend: Solr, or Tantivy embedded.
- A second `ledger` backend: Postgres with `SKIP LOCKED`.
- A second inbound tap beside GitHub issues (`taps/github-issues` is the pattern, `taps/github-comments` the outbound one): a tracker or a mailing list.
- The browser reader's next step: the page keeping a loaded bundle between visits, and admitting or refusing proposals from it.
- Conformance beyond the laptop: `scripts/conformance.py`, landed 8 October, runs the seven M0 steps over MCP against any server exposing the surface, on stdio or streamable HTTP, with the operator's export, import and reindex behind a command of the operator's; `make conformance` runs it against this build, `scripts/conformance.test.sh` holds that each of its assertions catches the defect it is for, against a second implementation of the surface in memory. Its first run against this build found two: a record's own export, imported back, landed a copy of every entry under a new id, and a bundle imported into a second domain of the same record kept its ids and took the first domain's place in the index. Both fixed in `import`, which asks the record and not the index whether an id is in use, since the index can lag it; the adversarial pass over the branch showed the first fix's index-side check letting the second defect back in with the index lost, and that step 5 proved less than its title said, so it now holds a lease across a heartbeat, refuses `complete` once the lease is lost, and wants the released task back with its handoff. What is left is running it against a serving layer that is not this binary, when there is one.
## Working agreements
- Entries are the contract. Any change to the frontmatter is a change to `core` and gets a note in this file.
- Every crate has a test that round-trips its data through the record and back.
- The CLI and the MCP server call the same library functions; nothing lives only in the CLI.
- No engine-specific vocabulary in docs or code outside the backend that needs it.