From 248f3143096a9e2169fb9e8d605ac497cbb0672d Mon Sep 17 00:00:00 2001 From: Sander Striker Date: Thu, 29 Oct 2026 22:22:49 +0100 Subject: [PATCH 01/14] docs: correct the facts that the ASD-STE100 rewrite left open #35 listed facts in the documents that were wrong before the rewrite. This change corrects them against the code: - the count of harnesses, and Grok Build as a harness; - the exit code 4 of a launch with no handoff; - the gateway on wslc, which runs in the session VM; - the time and the tokens with the record, with the figures; - the replay flags, and what quarantine drop does; - where the PAT goes, and the default --auth; - the harnesses in the image, and the binary name amem (amem.exe on Windows, /opt/memory/memory inside the sandbox); - the planned crates of the reference stack against the crates today; - dependabot.yml, which #12 added; - what .gitignore ignores under .claude/; - act 11 of the demo, which reads README.md. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bhj4Ck4y5iwZUsjpgQx8A2 --- AGENTS.md | 14 +++++--- CONTRIBUTING.md | 2 +- Makefile | 2 +- README.md | 5 +-- crates/wslc-api/src/dll.rs | 2 +- docs/harness-containers.md | 12 +++---- docs/reference-stack.md | 41 +++++++++++++--------- images/harness/README.md | 5 +-- local/README.md | 66 ++++++++++++++++++++--------------- scripts/docs-only.sh | 6 ++-- scripts/fetch-wslcsdk.sh | 6 ++-- scripts/fetch-wslcsdk.test.sh | 2 +- 12 files changed, 94 insertions(+), 69 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6062748b..197d3241 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,9 +36,11 @@ before you override the instruction. installs `make check` as the pre-commit hook. Some changes touch only Markdown files outside `crates/` and `taps/`. For this rule, an `AGENTS.md` in `crates/` or `taps/` counts as Markdown. The hook and CI do not run `make check` for such a change, because - nothing builds from those files or reads them. `scripts/docs-only.sh` decides which - changes are of this type. Its test holds the premise that nothing builds from those - files or reads them. + nothing builds from those files. `scripts/docs-only.sh` decides which changes are of + this type. Its test holds the premise that no crate or tap embeds those files or reads + them. One script reads one of them: act 11 of the demo (`scripts/demo/11-limits.sh`) + prints the two lists at the top of `README.md`. Thus, if you change those lists, run + that act. - **Show that a new assertion fails before you trust it.** Run it against the code as it was before your change. Put what it reported in the pull request. If a test only ever passed, nobody has shown that it tests anything. Assert the rule, not a bound on the @@ -113,8 +115,10 @@ of the Claude Code harness that belongs to the repository is committed under `.c | `.claude/commands/reader-test.md` | the prompts for a docs-only branch, instead of that pass: a reader that has only the changed files and their diff, and a fact check against the code | If the guard refuses something that this repository asks for, the guard has a bug. Add -the case to the test and fix the guard. Do not work around the bug. All other files in -`.claude/` stay ignored. An agent that is not Claude Code gets none of this. The same +the case to the test and fix the guard. Do not work around the bug. `.gitignore` ignores +`.claude/settings.local.json` and `.claude/worktrees/`. Do not commit other files in +`.claude/` unless they belong to the repository. An agent that is not Claude Code gets +none of this. The same rules apply to that agent, and it learns them when it reads them. ## Project shape diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 351eea9b..db013f23 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,7 +35,7 @@ make check # format, clippy, tests, guard, docs-only and conformance tests | `make conformance` | `scripts/conformance.py`: the seven M0 steps over MCP, against a new record that this build serves. `URL=` points it at a different server. `OPERATOR=` gives it the export, import and reindex of that server, for the last two steps | | `make test` | only `cargo test` | | `make fmt` | `cargo fmt`, which changes the files in place | -| `make run ARGS='...'` | the `memory` binary, for example `ARGS='init /tmp/m --domain dom-me'` | +| `make run ARGS='...'` | the `amem` binary, for example `ARGS='init /tmp/m --domain dom-me'` | | `make verify` | `check`, and also `cargo deny`, `cargo machete`, coverage against `COVERAGE_MIN`, and the browser builds of `dm-core`, `dm-policy` and `dm-reader`. CI runs this target | | `make e2e` | the browser tests in `e2e/`. Chromium in the Playwright image drives the pages. The tests put screenshots in `e2e/screenshots/`. It needs Podman or Docker, not Node. It is not part of `check` or `verify`. Run it for a change to the pages. See [e2e/README.md](e2e/README.md) | | `make reader` | builds again the glue and the wasm of the browser reader into `crates/web/static/reader/`. It needs `wasm-bindgen-cli` 0.2.129 (`cargo install wasm-bindgen-cli --version 0.2.129`). Commit the result | diff --git a/Makefile b/Makefile index d94312c1..f29f9967 100644 --- a/Makefile +++ b/Makefile @@ -230,7 +230,7 @@ windows-target: @rustup target list --installed 2>/dev/null | grep -q '^x86_64-pc-windows-gnu$$' || \ { echo "the C API binding is linted for Windows; run: rustup target add x86_64-pc-windows-gnu" >&2; exit 1; } -# wslcsdk.dll, the WSL containers C API, beside the Windows memory.exe: pinned and checked. +# wslcsdk.dll, the WSL containers C API, beside the Windows amem.exe: pinned and checked. wslcsdk: sh scripts/fetch-wslcsdk.sh "$(or $(DEST),target/release)" diff --git a/README.md b/README.md index 96380f7b..55e20f1f 100644 --- a/README.md +++ b/README.md @@ -278,8 +278,9 @@ its own. Run `amem` from the directory that you want as the working tree, or giv The web launcher listens on loopback only. Without a key, it serves its static pages and answers nothing else. `amem doctor` names each credential that it finds, and where it found it. It never shows their values. `LLMAO_KEY` alone points at a local LiteLLM on -port 4000. A PAT for the Foundation's gateway needs `LLMAO_URL=https://llm.apache.org` -beside it. +port 4000. For the Foundation's gateway, put the PAT in `LLMAO_KEY`, and set +`LLMAO_URL=https://llm.apache.org` beside it. A launch uses the gateway only with +`--auth gateway` (`AUTH=gateway` with `make`). The default is `--auth subscription`. The page has a companion. The companion is a browser extension in `crates/web/companion`. It fetches for a session with the browser's own logins. It is for Chrome. Load it unpacked diff --git a/crates/wslc-api/src/dll.rs b/crates/wslc-api/src/dll.rs index e0982574..9b913e66 100644 --- a/crates/wslc-api/src/dll.rs +++ b/crates/wslc-api/src/dll.rs @@ -110,7 +110,7 @@ impl Api { let lib = unsafe { Library::new("wslcsdk.dll") }.map_err(|e| { Error::other( "wslcsdk.dll", - format!("{e} (it ships in the Microsoft.WSL.Containers NuGet package, not with WSL; put it beside memory.exe)"), + format!("{e} (it ships in the Microsoft.WSL.Containers NuGet package, not with WSL; put it beside amem.exe)"), ) })?; // SAFETY: ole32 is a system DLL; CoTaskMemFree has this signature. diff --git a/docs/harness-containers.md b/docs/harness-containers.md index cc0c1cdb..4c193fb0 100644 --- a/docs/harness-containers.md +++ b/docs/harness-containers.md @@ -416,13 +416,13 @@ entries are platform variants that never install. The large set that is not pinn packages through `npm -g`. pi needs its own lock, because it installs with `--ignore-scripts`, and the postinstall of Claude Code has to run. Both npm builds report five install scripts "not covered by allowScripts". These scripts ran under npm 11.19. Before the next Node bump, check -whether npm 12 blocks them. The proposed `dependabot.yml` is in the lockfile spike directory, -not yet in `.github/`. +whether npm 12 blocks them. The proposed `dependabot.yml` was in the lockfile spike directory. +#12 then added `.github/dependabot.yml`, for the actions of the workflows only. -The repository has GitHub Actions workflows and no `.github/dependabot.yml`. The research -reports that the Dependabot page of ASF Infra requires one for the github-actions ecosystem, on -each repository that uses Actions. The research agent read that page. We did not check it again -here. +Before #12, the repository had GitHub Actions workflows and no `.github/dependabot.yml`. The +research reports that the Dependabot page of ASF Infra requires one for the github-actions +ecosystem, on each repository that uses Actions. The research agent read that page. We did not +check it again here. ## The egress, and who else meters tokens diff --git a/docs/reference-stack.md b/docs/reference-stack.md index b04a7ce6..2e31fda4 100644 --- a/docs/reference-stack.md +++ b/docs/reference-stack.md @@ -91,7 +91,7 @@ Thus the browser gives a person a view of their own memory and of the public sli ## 6. Two deployments from one codebase -**The Foundation's reference deployment** is the compose file above. It has these parts: +**The Foundation's reference deployment** is planned, and it has no compose file yet. It has these parts: - MinIO; - Kafka; @@ -119,22 +119,31 @@ It has the same surface and the same bundle format. It is the conformance check ## 7. Shape of the codebase -One Rust workspace. +One Rust workspace. The table gives the planned shape. The last column says what is in +`crates/` today. -| Crate | Holds | Compiles to | -|---|---|---| -| `core` | Entries, frontmatter, bundles, digest manifests, the filter model, labels and ceilings. No I/O | native, wasm, WASI | -| `store` | The record over OpenDAL: versions, tombstones, erasure | native | -| `index` | Two backends: Tantivy embedded and Solr remote. The results give summaries first | native; Tantivy also wasm for the reader | -| `ledger` | Two backends: SQLite and Postgres. Claims, leases, origin dedup | native | -| `log` | Kafka producer and consumer groups. A file-backed log for the laptop | native | -| `policy` | OPA sidecar client. Regorus for the local grants file | native, wasm | -| `server` | MCP and HTTP surface, session cache, the read filter and write checks, T0 assembly | native | -| `sandbox` | Confinement: the plan, namespaces on Linux, Seatbelt on macOS. The WSL container backend is in `amem` (`crates/memory`) | native | -| `egress` | The one network path out of the confined harness: the MCP of the memory server over HTTP, and model calls with ceilings, credentials, spend and metering | native | -| `plugins` | WASI host for taps and skill bundles | native | -| `ui` | The browser reader and curation page | wasm | -| `memoryd`, `memory-local` | The two binaries | native, Windows and Linux | +| Crate | Holds | Compiles to | Today | +|---|---|---|---| +| `core` | Entries, frontmatter, bundles, digest manifests, the filter model, labels and ceilings. No I/O | native, wasm, WASI | `crates/core` | +| `store` | The record over OpenDAL: versions, tombstones, erasure | native | `crates/store`, with the file system and in-memory services | +| `index` | Two backends: Tantivy embedded and Solr remote. The results give summaries first | native; Tantivy also wasm for the reader | `crates/index`, with one backend: SQLite with FTS5 | +| `ledger` | Two backends: SQLite and Postgres. Claims, leases, origin dedup | native | `crates/ledger`, with both backends | +| `log` | Kafka producer and consumer groups. A file-backed log for the laptop | native | No crate. The event log is in `crates/server`, with no Kafka | +| `policy` | OPA sidecar client. Regorus for the local grants file | native, wasm | `crates/policy`, with regorus in the process and no OPA client | +| `server` | MCP and HTTP surface, session cache, the read filter and write checks, T0 assembly | native | `crates/server` | +| `sandbox` | Confinement: the plan, namespaces on Linux, Seatbelt on macOS. The WSL container backend is in `amem` (`crates/memory`) | native | `crates/sandbox` | +| `egress` | The one network path out of the confined harness: the MCP of the memory server over HTTP, and model calls with ceilings, credentials, spend and metering | native | `crates/egress` | +| `plugins` | WASI host for taps and skill bundles | native | `crates/taps`, a WASI host for taps | +| `ui` | The browser reader and curation page | wasm | `crates/reader`, the core crate for a browser, and `crates/web`, the pages of the web launcher | +| `memoryd`, `memory-local` | The two binaries | native, Windows and Linux | One binary, `amem` (`crates/memory`) | + +These crates are in `crates/` too, and the plan does not name them: + +- `client`: a typed MCP client for the memory server; +- `fetch`: what the fetch tool does with a page; +- `mount`: the projection as a live mount; +- `otel`: OpenTelemetry traces; +- `wslc-api`: a binding to the WSL containers C API. ## 8. Portability notes diff --git a/images/harness/README.md b/images/harness/README.md index 3def792f..d20ff60b 100644 --- a/images/harness/README.md +++ b/images/harness/README.md @@ -1,7 +1,8 @@ # The harness image -The harness image holds seven of the eight harnesses that `amem launch` knows, Node, and -the `memory` binary. `make image` builds it from the Dockerfile in this directory. It +The harness image holds nine of the ten harnesses that `amem launch` knows, Node, and +the `amem` binary as `/opt/memory/memory`. Antigravity is not in it yet. `make image` +builds it from the Dockerfile in this directory. It copies the release binary into the image. Then it builds with `wslc build` (Windows, WSL containers), or with `podman build` where that is present. The tag is `durable-memory/harness:dev`. At launch, `MEMORY_IMAGE` or `--image` overrides it. diff --git a/local/README.md b/local/README.md index bc04a809..8d38e5f1 100644 --- a/local/README.md +++ b/local/README.md @@ -2,14 +2,14 @@ ## Confined: `amem launch` -This is the way to run a harness. It needs no containers. The `memory` binary makes the -sandbox natively, with no privilege. The harness gets its own user, mount, network, pid, +This is the way to run a harness. It needs no containers. `amem` makes the sandbox +natively, with no privilege. The harness gets its own user, mount, network, pid, uts and ipc namespaces. The sandbox contains: - the system's binaries, read-only - a private home - the working tree, read-write -- the `memory` binary +- `amem`, at `/opt/memory/memory` - the egress socket. The sandbox has no network interface except loopback. In the sandbox, a relay forwards one @@ -71,9 +71,10 @@ you know it. `PROMPT` runs the headless form of the harness, with the harness's permission prompts off. Nobody is there to answer them, and a task needs edits and commands. The sandbox is the boundary, and the working tree is what the harness can change. The harness prints the answer and exits with its exit code. A launch that leaves -no handoff exits with code 4. The reason: a non-interactive launch whose outcome is only -in its transcript leaves nothing for the next session. If you intend this, use -`--no-handoff`. +no handoff exits with code 4, unless the harness itself failed: then the launch exits with +the code of the harness. A note that a hook files at session end counts as a handoff. The +reason: a non-interactive launch whose outcome is only in its transcript leaves nothing +for the next session. If you intend this, use `--no-handoff`. `make work` claims the next admitted task that the session may take. It makes the prompt from: @@ -99,7 +100,7 @@ amem task admit task-me-xxxxxxxx make work HARNESS=codex AUTH=key ``` -One launcher serves eight harnesses. Each harness needed the same three things. The table +One launcher serves ten harnesses. Each harness needed the same three things. The table shows all that is different: | Harness | Model calls reach the egress by | Memory over MCP by | `AUTH` modes | @@ -362,8 +363,9 @@ If the body is not from the owner and the scan finds something, the body does no the record. It goes to `state/quarantine/.md`. It is never indexed or served. The owner gets a task that requires `human`, with the scan's excerpts and the commands to act. `amem quarantine list|show|release|drop` are the owner's commands. Release writes the -entry into the record as it was written, with its author and all. There are two more -gates: +entry into the record as it was written, with its author and all. Drop deletes the held +write and its review from `state/quarantine`, and writes nothing to the record. There are +two more gates: - This gate counts the entries that an agent session supersedes. The limit is `max_supersedes_per_session` (`[gates]` in `grants.toml`, twenty by default). When a @@ -855,6 +857,9 @@ It sends each recorded request again, up to `--limit`, or one `--turn`: - with only the model changed, and the answer not streamed; - through the egress, metered and under the spend limits. +`--model` names the model of the candidate. `--provider` names the destination. `--auth` +selects the route, and is `gateway` by default. + It compares the tools that the candidate asks for with the tools that the upstream returned. Where both answered in text, the overlap of the texts decides. `amem replay` logs the run in `state/replay/.jsonl`, with a digest of the decision contexts. @@ -1110,15 +1115,18 @@ hundreds of millions of cached tokens. Bare Claude Code does not reach that gate all, because the launcher's egress is what folds its system message. That is the claim of the paper in one table. With the record, the harness gives every -answer in a third of the time and tokens of a search for it. Without the record, it gives -nothing, or it rediscovers the answers at a cost, and only where a public source exists. +answer in much less time, and on fewer tokens, than a search for it. Claude Code on a Max +login used less than a tenth of the time (34 against 391 seconds) and a third of the +tokens (294k against 978k). Without the record, the harness gives nothing, or it +rediscovers the answers at a cost, and only where a public source exists. ```sh scripts/eval.py evals/demo.toml --root ~/memory-demo2 --harness claude --auth subscription scripts/eval.py evals/demo.toml --root ~/memory-demo2 --harness codex,claude --auth gateway --model qwen3.8-27b ``` -Grok is a destination, not a harness. Any OpenAI-shaped harness reaches it with -`--provider xai` and `XAI_API_KEY`. `command` is not a harness either. It is a program: +xAI is also a destination. Any OpenAI-shaped harness reaches the Grok models with +`--provider xai` and `XAI_API_KEY`. Grok Build (`grok`), the harness of xAI, is a harness +of its own in the table above. `command` is not a harness. It is a program: the first argument after `--` runs as given, in the sandbox. Its environment has the egress and the memory server, and nothing else is configured. Thus a Gofannon agent, which is ordinary Python that calls a gateway, runs with the same confinement and metering as a @@ -1370,15 +1378,15 @@ read of an entry file becomes a `get`. The hook refuses a search over the projec workspace. Thus the launcher includes the projection in their workspace. If it did not, the model would use a shell instead, which the hook does not see. -Each hook runs the `memory` binary in the sandbox. The binary reads the event and the tail -of the transcript, and leaves a note in an outbox in the private home. When the session -ends, the launcher files the notes as handoffs. Each has a mark that says that the hook -left it, not the agent, and it points to the full transcript. +Each hook runs `amem` in the sandbox, at `/opt/memory/memory`. The binary reads the event +and the tail of the transcript, and leaves a note in an outbox in the private home. When +the session ends, the launcher files the notes as handoffs. Each has a mark that says +that the hook left it, not the agent, and it points to the full transcript. The launcher always files a note from before compaction, because its content is no longer in the context of the harness. It files a note from session end only when the session left -no handoff of its own. This note is the handoff of last resort. It is also the reason why -a prompted launch that did not write a handoff no longer exits 4. +no handoff of its own. This note is the handoff of last resort. Thus a prompted launch +where the agent wrote no handoff does not exit 4 when a hook filed this note. Transcripts stay where the harness keeps them, under the private home in the state of the record. Thus nothing is lost, also when the launcher files no note. The hook reads all @@ -1426,7 +1434,7 @@ for the Foundation, where the front desk mints a virtual key for each principal. ## The WSL containers C API Microsoft supports `wslcsdk.dll` for production. It does not come with WSL. It comes in -the `Microsoft.WSL.Containers` NuGet package, version 3.0.1, and goes beside `memory.exe`. +the `Microsoft.WSL.Containers` NuGet package, version 3.0.1, and goes beside `amem.exe`. `make wslcsdk DEST=` gets it. It checks the package and the DLL against pinned sums, and writes nothing when one of them is different. @@ -1436,8 +1444,8 @@ The binding is `crates/wslc-api`. It is for Windows only, and it loads at run ti as over the command line. `--wslc-via cli` or `api` selects the route explicitly: ```sh -memory.exe launch --harness claude --backend wslc --prompt 'say pong' # over the API -memory.exe work --harness claude --backend wslc # a task, over the API +amem.exe launch --harness claude --backend wslc --prompt 'say pong' # over the API +amem.exe work --harness claude --backend wslc # a task, over the API ``` What the API cannot do stays on the command line: @@ -1458,7 +1466,7 @@ image. An image that the command line does not have comes from its registry. `amem wslc-api-check` alone provisions if necessary and runs one container. It shows the time that each step took. -The session lives while `memory.exe` holds it. Thus its VM boots on each launch. This +The session lives while `amem.exe` holds it. Thus its VM boots on each launch. This takes six to eight seconds here, where the long-lived VM of the command line is already up. The container itself takes about two seconds. Ctrl-C ends the session and everything in it. @@ -1482,8 +1490,8 @@ launch, because it is only bind mounts. Unpack again when the harness image changes. A launch has the harnesses, Node and `/etc` of the harness image, read-only. It adds only the private home, the working tree and the -egress socket. The `memory` binary inside, which is the relay and the hooks, is the -launcher's own binary. The launcher mounts it over the copy in the harness image. Thus a +egress socket. The binary inside, `/opt/memory/memory`, is the relay and the hooks. It is +the launcher's own binary. The launcher mounts it over the copy in the harness image. Thus a change to the hooks is live on the next Linux launch, with no rebuild. Under WSL containers, there is no such mount, and the copy in the harness image runs. Thus, there, a change to a hook needs `make image` first. @@ -1507,7 +1515,7 @@ make work BACKEND=wslc # a task from the ledger The session VM has two containers: -- The egress container runs the Linux `memory`, with your record mounted in. It has the +- The egress container runs the Linux `amem`, with your record mounted in. It has the only route outside. It opens the memory, serves the egress on a socket in a shared volume, and runs the launcher logic, task mode included. - The harness container has no network at all. It has its private home and the working @@ -1517,9 +1525,9 @@ The session VM has two containers: The host side only changes run requests into `wslc run`, and reports the exit code. Your login directories (`~/.claude`, `~/.codex`, `~/.gemini`) go into the egress container read-only. The egress container copies the one file that the harness needs into the -private home. Keys and `GH_TOKEN` go in as environment. The `gateway` auth is not -connected through yet, because the session VM cannot reach LiteLLM on the loopback of this -machine. +private home. Keys and `GH_TOKEN` go in as environment. The session VM cannot reach +LiteLLM on the loopback of this machine. Thus `--auth gateway` runs a gateway of its own in +the VM, as [The gateway inside the VM](#the-gateway-inside-the-vm) says. `--unconfined` runs the same configuration without namespaces, with the egress on a TCP port. It is for platforms that have no sandbox backend. macOS gets Seatbelt later. diff --git a/scripts/docs-only.sh b/scripts/docs-only.sh index 72c81c82..b0aa5bb1 100755 --- a/scripts/docs-only.sh +++ b/scripts/docs-only.sh @@ -4,8 +4,10 @@ # that touches only such files cannot change what builds or what the tests see. Under # `crates/` and `taps/` a `.md` file counts as code, since a fixture or a crate's README is # what a test or a doc attribute would read; an `AGENTS.md` there is instructions, not -# input. Nothing reads a Markdown file from anywhere else (scripts/docs-only.test.sh holds -# that). An empty list is not docs-only, so a caller that failed to list its changes runs +# input. No crate or tap embeds a Markdown file from anywhere else or reads one +# (scripts/docs-only.test.sh holds that). One script does read one: act 11 of the demo +# prints the lists at the top of README.md, and no check runs it. An empty list is not +# docs-only, so a caller that failed to list its changes runs # everything. # # git diff --cached --name-only --no-renames | scripts/docs-only.sh diff --git a/scripts/fetch-wslcsdk.sh b/scripts/fetch-wslcsdk.sh index a5228cc3..a47ed951 100644 --- a/scripts/fetch-wslcsdk.sh +++ b/scripts/fetch-wslcsdk.sh @@ -1,10 +1,10 @@ #!/bin/sh # Fetch wslcsdk.dll, the WSL containers C API, which does not ship with WSL: it comes in -# the Microsoft.WSL.Containers NuGet package, and the Windows `memory.exe` loads it from +# the Microsoft.WSL.Containers NuGet package, and the Windows `amem.exe` loads it from # beside itself. The package and the DLL are pinned by version and sha256, and nothing is # written unless both match. # -# scripts/fetch-wslcsdk.sh [x64|arm64] +# scripts/fetch-wslcsdk.sh [x64|arm64] # # WSLCSDK_NUPKG names a package already on disk instead of downloading it. To move to a # new SDK, change the version and all three sums together, from a package you checked. @@ -17,7 +17,7 @@ dll_sha256_x64=f3528a5b69b777d2bf606edc1629d13ca549f1b87c9f57380dd2f2c611b30b4f dll_sha256_arm64=7afecfc4fc5d3133172e8c0fba763ff8266473d4f78b4cfb20386eb2865b1ab4 if [ $# -lt 1 ] || [ -z "$1" ]; then - echo "usage: $0 [x64|arm64]" >&2 + echo "usage: $0 [x64|arm64]" >&2 exit 2 fi dest=$1 diff --git a/scripts/fetch-wslcsdk.test.sh b/scripts/fetch-wslcsdk.test.sh index 263ef92c..8d025c2b 100644 --- a/scripts/fetch-wslcsdk.test.sh +++ b/scripts/fetch-wslcsdk.test.sh @@ -1,7 +1,7 @@ #!/bin/sh # What fetch-wslcsdk.sh refuses: a package that is not the pinned one, a package that # cannot be read, an architecture the SDK has no DLL for, and no destination. In each case -# nothing may be written, since a DLL beside memory.exe is code it loads. +# nothing may be written, since a DLL beside amem.exe is code it loads. # # Then the real package, downloaded once into target/ and checked by its sum like any # other: both architectures are accepted, and with the two DLL pins swapped each is From 1df7e506d7184ff0ef8149fbce0b47a9e1337300 Mon Sep 17 00:00:00 2001 From: Sander Striker Date: Thu, 29 Oct 2026 22:37:31 +0100 Subject: [PATCH 02/14] fix: answer the adversarial pass on the corrected facts The pass found these errors in the first commit, and this commit fixes them: - Other scripts read the Markdown outside crates/ and taps/: seed-corpus.py seeds the demo record from docs/ and examples/decisions/, and the golden suite and the eval score against that text. - On wslc, --auth gateway always uses the LiteLLM in the session VM, and it replaces LLMAO_URL and LLMAO_KEY. --server-auth gateway also uses the gateway. - amem replay sends to the gateway by default. With --auth key, --provider must name the recorded destination. - A note from before compaction also prevents exit 4. - Under Seatbelt, the binary is at its host path. - Messages, help text and comments named a "memory" command. The command is amem. - The sandbox crate said eight harnesses. It knows ten. - Wording: the timing figures, which table, which plan, and the spike file that is still there. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bhj4Ck4y5iwZUsjpgQx8A2 --- .github/workflows/image.yml | 4 +-- AGENTS.md | 16 ++++++++---- README.md | 5 +++- crates/memory/src/main.rs | 6 ++--- crates/memory/src/savings.rs | 2 +- crates/memory/src/wslc_api.rs | 2 +- crates/sandbox/src/lib.rs | 6 +++-- crates/web/src/lib.rs | 4 +-- docs/harness-containers.md | 2 +- docs/reference-stack.md | 2 +- golden/docs.toml | 2 +- images/harness/Dockerfile | 9 ++++--- images/harness/README.md | 12 ++++----- local/README.md | 49 +++++++++++++++++++---------------- scripts/docs-only.sh | 9 ++++--- 15 files changed, 73 insertions(+), 57 deletions(-) diff --git a/.github/workflows/image.yml b/.github/workflows/image.yml index 2ffc3ffd..0e5bb051 100644 --- a/.github/workflows/image.yml +++ b/.github/workflows/image.yml @@ -6,7 +6,7 @@ # image to a registry, and `packages: none` below says so in this file (with other # permissions named, GitHub gives none for packages anyway). Pins are bumped by a daily # check that opens a pull request, and a pull request that touches them has its lockfile -# checked against package.json. Never on a run: `memory launch` uses the tag it is given. +# checked against package.json. Never on a run: `amem launch` uses the tag it is given. name: image on: @@ -83,7 +83,7 @@ jobs: with: toolchain: stable - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - - name: Build the memory binary + - name: Build the amem binary run: cargo build --release --locked -p amem && cp target/release/amem images/harness/memory - name: Build the image run: docker build --pull -t harness:${{ github.sha }} images/harness diff --git a/AGENTS.md b/AGENTS.md index 197d3241..843b0950 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,9 +38,16 @@ before you override the instruction. counts as Markdown. The hook and CI do not run `make check` for such a change, because nothing builds from those files. `scripts/docs-only.sh` decides which changes are of this type. Its test holds the premise that no crate or tap embeds those files or reads - them. One script reads one of them: act 11 of the demo (`scripts/demo/11-limits.sh`) - prints the two lists at the top of `README.md`. Thus, if you change those lists, run - that act. + them. Some scripts read them, and no check runs these scripts: + + - `scripts/seed-corpus.py` seeds the demo record from `docs/` and + `examples/decisions/` (`make demo`). `golden/docs.toml` and `evals/demo.toml` score + answers against that text. + - Act 11 of the demo (`scripts/demo/11-limits.sh`) prints the two lists at the top of + `README.md`. + + If you change a heading in `docs/`, a decision in `examples/decisions/` or those two + lists, run the script that reads it. - **Show that a new assertion fails before you trust it.** Run it against the code as it was before your change. Put what it reported in the pull request. If a test only ever passed, nobody has shown that it tests anything. Assert the rule, not a bound on the @@ -118,8 +125,7 @@ If the guard refuses something that this repository asks for, the guard has a bu the case to the test and fix the guard. Do not work around the bug. `.gitignore` ignores `.claude/settings.local.json` and `.claude/worktrees/`. Do not commit other files in `.claude/` unless they belong to the repository. An agent that is not Claude Code gets -none of this. The same -rules apply to that agent, and it learns them when it reads them. +none of this. The same rules apply to that agent, and it learns them when it reads them. ## Project shape diff --git a/README.md b/README.md index 55e20f1f..77c06227 100644 --- a/README.md +++ b/README.md @@ -279,8 +279,11 @@ The web launcher listens on loopback only. Without a key, it serves its static p answers nothing else. `amem doctor` names each credential that it finds, and where it found it. It never shows their values. `LLMAO_KEY` alone points at a local LiteLLM on port 4000. For the Foundation's gateway, put the PAT in `LLMAO_KEY`, and set -`LLMAO_URL=https://llm.apache.org` beside it. A launch uses the gateway only with +`LLMAO_URL=https://llm.apache.org` beside it. A launch uses the gateway with `--auth gateway` (`AUTH=gateway` with `make`). The default is `--auth subscription`. +With `--server-auth gateway`, only the memory's own model calls use the gateway. On the +`wslc` backend, the gateway is a LiteLLM in the session VM, with `LITELLM_MASTER_KEY` +from `local/.env`. That backend does not use `LLMAO_URL` or `LLMAO_KEY`. The page has a companion. The companion is a browser extension in `crates/web/companion`. It fetches for a session with the browser's own logins. It is for Chrome. Load it unpacked diff --git a/crates/memory/src/main.rs b/crates/memory/src/main.rs index 0796de3e..e61c5539 100644 --- a/crates/memory/src/main.rs +++ b/crates/memory/src/main.rs @@ -254,7 +254,7 @@ enum BackendArg { Auto, /// Native Linux namespaces, from this host's binaries. Namespaces, - /// Native Linux namespaces, pivoted into the unpacked harness image (`memory image + /// Native Linux namespaces, pivoted into the unpacked harness image (`amem image /// unpack` first). What `auto` picks on Linux once the image is unpacked. Image, /// WSL containers: the harness image in a session VM, through `wslc`. @@ -1465,7 +1465,7 @@ async fn run_cli(cli: Cli, root: PathBuf, at_start: Vec) -> Fallible { println!("{}", p.display()); Ok(()) } - None => Err("no unpacked image; run `memory image unpack`".into()), + None => Err("no unpacked image; run `amem image unpack`".into()), }, }, Command::Relay { .. } => unreachable!("handled in main"), @@ -3385,7 +3385,7 @@ fn runner_for(backend: Backend, root: &Path) -> Result Runner::Namespaces, Backend::Seatbelt => Runner::Seatbelt, Backend::Image => Runner::Image( - image::present(root).ok_or("no unpacked image; run `memory image unpack` first")?, + image::present(root).ok_or("no unpacked image; run `amem image unpack` first")?, ), Backend::None | Backend::Wslc => Runner::Unconfined(None), }) diff --git a/crates/memory/src/savings.rs b/crates/memory/src/savings.rs index 128082ac..5b86b230 100644 --- a/crates/memory/src/savings.rs +++ b/crates/memory/src/savings.rs @@ -472,7 +472,7 @@ fn render_recall(r: &RecallRow) -> String { let mut s = String::new(); s.push_str("\nRecall: one turn either way; the brief against the pack it is written from\n"); if r.briefs + r.packs == 0 { - s.push_str(" none yet; `memory recall ` on this record\n"); + s.push_str(" none yet; `amem recall ` on this record\n"); return s; } let each = |tokens: usize, n: usize| tokens.checked_div(n).unwrap_or(0); diff --git a/crates/memory/src/wslc_api.rs b/crates/memory/src/wslc_api.rs index 5744d4fb..bad1e33a 100644 --- a/crates/memory/src/wslc_api.rs +++ b/crates/memory/src/wslc_api.rs @@ -430,7 +430,7 @@ mod on_windows { Provision::Present { id } => { eprintln!( "memory: {image} {} from this session's store; after a rebuild, \ - `memory wslc-api-check --refresh`", + `amem wslc-api-check --refresh`", id.get(..12).unwrap_or(&id) ); Ok(()) diff --git a/crates/sandbox/src/lib.rs b/crates/sandbox/src/lib.rs index 1497ffce..31fa155a 100644 --- a/crates/sandbox/src/lib.rs +++ b/crates/sandbox/src/lib.rs @@ -6,7 +6,7 @@ //! on loopback that forwards to the socket, and the egress is the only process with the //! network. //! -//! Eight harnesses are known. Each needs three things from the launcher, and the adapter +//! Ten harnesses are known. Each needs three things from the launcher, and the adapter //! for each is the `setup_*` function below: //! //! | Harness | Model calls | Memory over MCP | Config the launcher owns | @@ -17,6 +17,8 @@ //! | Antigravity CLI | `GOOGLE_GEMINI_BASE_URL`; a login tunnels | `mcpServers..serverUrl` | `~/.gemini/antigravity-cli/settings.json`, `~/.gemini/config/` | //! | Qwen Code | `OPENAI_BASE_URL`; key only | `mcpServers..httpUrl` | `~/.qwen/settings.json` | //! | Grok Build | `GROK_XAI_API_BASE_URL`; a login tunnels | `mcp_servers..url` | `~/.grok/config.toml` | +//! | Muse Code | `--base-url` on `exec` | `mcp_servers.`, `streamable_http` | `~/.config/muse/settings.json` | +//! | pi | `baseUrl` of a provider of its own | `mcpServers..url` | `~/.pi/agent/models.json`, `mcp.json`, `extensions/` | //! | Copilot CLI | no override; `HTTPS_PROXY` to the egress, which tunnels to allowed hosts | `mcp-config.json` | `COPILOT_HOME` | //! | OpenCode | `provider..options.baseURL` | `mcp..url` | `XDG_CONFIG_HOME/opencode/opencode.json` | //! @@ -469,7 +471,7 @@ pub struct Request<'a> { pub home: &'a Path, /// The egress unix socket on the host. pub egress_socket: &'a Path, - /// The `memory` binary on the host, mounted in for the relay. + /// The `amem` binary on the host, mounted in for the relay. pub memory_binary: &'a Path, /// The destination's ceiling, which caps the session's handling. pub destination_ceiling: Label, diff --git a/crates/web/src/lib.rs b/crates/web/src/lib.rs index 969e4788..cadf3d0a 100644 --- a/crates/web/src/lib.rs +++ b/crates/web/src/lib.rs @@ -1,6 +1,6 @@ //! A web terminal on the launcher. //! -//! Each session is a pseudo-terminal running `memory launch`, kept alive by this server +//! Each session is a pseudo-terminal running `amem launch`, kept alive by this server //! whether or not a browser is attached. Its output goes into a ring buffer and to every //! attached websocket; a browser that connects, or reconnects, gets the ring first, so a //! refresh or a dropped connection lands in the same place. Beside the terminal a reading @@ -58,7 +58,7 @@ const RING_BYTES: usize = 2 * 1024 * 1024; pub struct Config { /// The record. pub root: PathBuf, - /// The `memory` binary; sessions run ` --root launch ...`. + /// The `amem` binary; sessions run ` --root launch ...`. pub memory_binary: PathBuf, /// The working tree sessions start in. pub workdir: PathBuf, diff --git a/docs/harness-containers.md b/docs/harness-containers.md index 4c193fb0..61844e87 100644 --- a/docs/harness-containers.md +++ b/docs/harness-containers.md @@ -416,7 +416,7 @@ entries are platform variants that never install. The large set that is not pinn packages through `npm -g`. pi needs its own lock, because it installs with `--ignore-scripts`, and the postinstall of Claude Code has to run. Both npm builds report five install scripts "not covered by allowScripts". These scripts ran under npm 11.19. Before the next Node bump, check -whether npm 12 blocks them. The proposed `dependabot.yml` was in the lockfile spike directory. +whether npm 12 blocks them. The proposed `dependabot.yml` is in the lockfile spike directory. #12 then added `.github/dependabot.yml`, for the actions of the workflows only. Before #12, the repository had GitHub Actions workflows and no `.github/dependabot.yml`. The diff --git a/docs/reference-stack.md b/docs/reference-stack.md index 2e31fda4..d2087b8e 100644 --- a/docs/reference-stack.md +++ b/docs/reference-stack.md @@ -137,7 +137,7 @@ One Rust workspace. The table gives the planned shape. The last column says what | `ui` | The browser reader and curation page | wasm | `crates/reader`, the core crate for a browser, and `crates/web`, the pages of the web launcher | | `memoryd`, `memory-local` | The two binaries | native, Windows and Linux | One binary, `amem` (`crates/memory`) | -These crates are in `crates/` too, and the plan does not name them: +These crates are in `crates/` too, and the table above does not name them: - `client`: a typed MCP client for the memory server; - `fetch`: what the fetch tool does with a page; diff --git a/golden/docs.toml b/golden/docs.toml index 11e6cd06..28102321 100644 --- a/golden/docs.toml +++ b/golden/docs.toml @@ -1,7 +1,7 @@ # Golden questions over the three documents in docs/, seeded as the demo corpus by # scripts/seed-corpus.py, one entry per section. Answers name section titles, or parts # of them; any one is a hit. `keywords` is the query a person would write, standing in -# for the one a model writes. Run: memory golden golden/docs.toml +# for the one a model writes. Run: amem golden golden/docs.toml # # Add your own domain's suite beside this one; the format is the same. diff --git a/images/harness/Dockerfile b/images/harness/Dockerfile index 1fd0f064..e03a6c69 100644 --- a/images/harness/Dockerfile +++ b/images/harness/Dockerfile @@ -1,7 +1,8 @@ -# The harness image: every harness `memory launch` knows, at pinned versions, on one base, -# with the memory binary where the sandbox expects it, and Python for workloads that are -# programs rather than harnesses, such as a Gofannon agent. The same image on every machine is -# the point; nothing here depends on what a developer's WSL or laptop contains. +# The harness image: every harness `amem launch` knows but Antigravity, at pinned +# versions, on one base, with the memory binary where the sandbox expects it, and Python +# for workloads that are programs rather than harnesses, such as a Gofannon agent. The same +# image on every machine is the point; nothing here depends on what a developer's WSL or +# laptop contains. # # Build with `make image`, which copies the release binary in beside this file. FROM ubuntu:24.04 diff --git a/images/harness/README.md b/images/harness/README.md index d20ff60b..3c7a0ee3 100644 --- a/images/harness/README.md +++ b/images/harness/README.md @@ -1,11 +1,11 @@ # The harness image -The harness image holds nine of the ten harnesses that `amem launch` knows, Node, and -the `amem` binary as `/opt/memory/memory`. Antigravity is not in it yet. `make image` -builds it from the Dockerfile in this directory. It -copies the release binary into the image. Then it builds with `wslc build` (Windows, WSL -containers), or with `podman build` where that is present. The tag is -`durable-memory/harness:dev`. At launch, `MEMORY_IMAGE` or `--image` overrides it. +The harness image holds nine of the ten harnesses that `amem launch` knows, Node, Python +and the `amem` binary as `/opt/memory/memory`. Antigravity is not in it yet. `make image` +builds it from the Dockerfile in this directory. It copies the release binary into the +image. Then it builds with `wslc build` (Windows, WSL containers), or with `podman build` +where that is present. The tag is `durable-memory/harness:dev`. At launch, `MEMORY_IMAGE` +or `--image` overrides it. The harness image gives the same environment on each machine: the same harness versions and tools. It does not depend on the contents of the WSL or the laptop of a developer. diff --git a/local/README.md b/local/README.md index 8d38e5f1..4f11d564 100644 --- a/local/README.md +++ b/local/README.md @@ -72,9 +72,9 @@ permission prompts off. Nobody is there to answer them, and a task needs edits a commands. The sandbox is the boundary, and the working tree is what the harness can change. The harness prints the answer and exits with its exit code. A launch that leaves no handoff exits with code 4, unless the harness itself failed: then the launch exits with -the code of the harness. A note that a hook files at session end counts as a handoff. The -reason: a non-interactive launch whose outcome is only in its transcript leaves nothing -for the next session. If you intend this, use `--no-handoff`. +the code of the harness. A note that a hook files, before compaction or at session end, +counts as a handoff. The reason: a non-interactive launch whose outcome is only in its +transcript leaves nothing for the next session. If you intend this, use `--no-handoff`. `make work` claims the next admitted task that the session may take. It makes the prompt from: @@ -364,8 +364,8 @@ the record. It goes to `state/quarantine/.md`. It is never indexed or served owner gets a task that requires `human`, with the scan's excerpts and the commands to act. `amem quarantine list|show|release|drop` are the owner's commands. Release writes the entry into the record as it was written, with its author and all. Drop deletes the held -write and its review from `state/quarantine`, and writes nothing to the record. There are -two more gates: +write from `state/quarantine`, and the reviewer's notes beside it. It writes nothing to +the record. There are two more gates: - This gate counts the entries that an agent session supersedes. The limit is `max_supersedes_per_session` (`[gates]` in `grants.toml`, twenty by default). When a @@ -857,8 +857,9 @@ It sends each recorded request again, up to `--limit`, or one `--turn`: - with only the model changed, and the answer not streamed; - through the egress, metered and under the spend limits. -`--model` names the model of the candidate. `--provider` names the destination. `--auth` -selects the route, and is `gateway` by default. +`--model` names the model of the candidate. `--auth` is `gateway` by default. Then the +calls go to the gateway, as the text below says. With `--auth key`, `--provider` must name +the destination that the session used. It compares the tools that the candidate asks for with the tools that the upstream returned. Where both answered in text, the overlap of the texts decides. `amem replay` @@ -1115,10 +1116,10 @@ hundreds of millions of cached tokens. Bare Claude Code does not reach that gate all, because the launcher's egress is what folds its system message. That is the claim of the paper in one table. With the record, the harness gives every -answer in much less time, and on fewer tokens, than a search for it. Claude Code on a Max -login used less than a tenth of the time (34 against 391 seconds) and a third of the -tokens (294k against 978k). Without the record, the harness gives nothing, or it -rediscovers the answers at a cost, and only where a public source exists. +answer in much less time, and on fewer tokens, than a search for it. On a Max login, +Claude Code used less than a tenth of the time: 34 against 391 seconds. It used less than +a third of the tokens: 294k against 978k. Without the record, the harness gives nothing, +or it rediscovers the answers at a cost, and only where a public source exists. ```sh scripts/eval.py evals/demo.toml --root ~/memory-demo2 --harness claude --auth subscription @@ -1126,11 +1127,11 @@ scripts/eval.py evals/demo.toml --root ~/memory-demo2 --harness codex,claude --a ``` xAI is also a destination. Any OpenAI-shaped harness reaches the Grok models with `--provider xai` and `XAI_API_KEY`. Grok Build (`grok`), the harness of xAI, is a harness -of its own in the table above. `command` is not a harness. It is a program: -the first argument after `--` runs as given, in the sandbox. Its environment has the -egress and the memory server, and nothing else is configured. Thus a Gofannon agent, which -is ordinary Python that calls a gateway, runs with the same confinement and metering as a -harness: +of its own, in the table under [Confined: `amem launch`](#confined-amem-launch). `command` +is not a harness. It is a program: the first argument after `--` runs as given, in the +sandbox. Its environment has the egress and the memory server, and nothing else is +configured. Thus a Gofannon agent, which is ordinary Python that calls a gateway, runs +with the same confinement and metering as a harness: ```sh make launch HARNESS=command AUTH=gateway ARGS='python3 agent.py' @@ -1378,15 +1379,16 @@ read of an entry file becomes a `get`. The hook refuses a search over the projec workspace. Thus the launcher includes the projection in their workspace. If it did not, the model would use a shell instead, which the hook does not see. -Each hook runs `amem` in the sandbox, at `/opt/memory/memory`. The binary reads the event -and the tail of the transcript, and leaves a note in an outbox in the private home. When -the session ends, the launcher files the notes as handoffs. Each has a mark that says -that the hook left it, not the agent, and it points to the full transcript. +Each hook runs `amem` in the sandbox: at `/opt/memory/memory` under namespaces and in the +harness image, and at its host path under Seatbelt. The binary reads the event and the +tail of the transcript, and leaves a note in an outbox in the private home. When the +session ends, the launcher files the notes as handoffs. Each has a mark that says that the +hook left it, not the agent, and it points to the full transcript. The launcher always files a note from before compaction, because its content is no longer in the context of the harness. It files a note from session end only when the session left no handoff of its own. This note is the handoff of last resort. Thus a prompted launch -where the agent wrote no handoff does not exit 4 when a hook filed this note. +where the agent wrote no handoff does not exit 4 when a hook filed a note. Transcripts stay where the harness keeps them, under the private home in the state of the record. Thus nothing is lost, also when the launcher files no note. The hook reads all @@ -1513,7 +1515,7 @@ make launch BACKEND=wslc PROMPT='say pong' # prompted make work BACKEND=wslc # a task from the ledger ``` -The session VM has two containers: +The session VM has two containers, and two more for the gateway with `--auth gateway`: - The egress container runs the Linux `amem`, with your record mounted in. It has the only route outside. It opens the memory, serves the egress on a socket in a shared @@ -1527,7 +1529,8 @@ login directories (`~/.claude`, `~/.codex`, `~/.gemini`) go into the egress cont read-only. The egress container copies the one file that the harness needs into the private home. Keys and `GH_TOKEN` go in as environment. The session VM cannot reach LiteLLM on the loopback of this machine. Thus `--auth gateway` runs a gateway of its own in -the VM, as [The gateway inside the VM](#the-gateway-inside-the-vm) says. +the VM, as [The gateway inside the VM](#the-gateway-inside-the-vm) says. That gateway +replaces `LLMAO_URL` and `LLMAO_KEY`, also when they name a remote gateway. `--unconfined` runs the same configuration without namespaces, with the egress on a TCP port. It is for platforms that have no sandbox backend. macOS gets Seatbelt later. diff --git a/scripts/docs-only.sh b/scripts/docs-only.sh index b0aa5bb1..aa03d111 100755 --- a/scripts/docs-only.sh +++ b/scripts/docs-only.sh @@ -5,10 +5,11 @@ # `crates/` and `taps/` a `.md` file counts as code, since a fixture or a crate's README is # what a test or a doc attribute would read; an `AGENTS.md` there is instructions, not # input. No crate or tap embeds a Markdown file from anywhere else or reads one -# (scripts/docs-only.test.sh holds that). One script does read one: act 11 of the demo -# prints the lists at the top of README.md, and no check runs it. An empty list is not -# docs-only, so a caller that failed to list its changes runs -# everything. +# (scripts/docs-only.test.sh holds that). Some scripts do, and no check runs them: +# scripts/seed-corpus.py seeds the demo record from docs/ and examples/decisions/, which +# golden/docs.toml and evals/demo.toml score against, and act 11 of the demo prints the +# lists at the top of README.md. An empty list is not docs-only, so a caller that failed +# to list its changes runs everything. # # git diff --cached --name-only --no-renames | scripts/docs-only.sh # From c6f7e33bfb78b20a3bca239c025765805a10fb87 Mon Sep 17 00:00:00 2001 From: Sander Striker Date: Thu, 29 Oct 2026 22:51:15 +0100 Subject: [PATCH 03/14] fix: answer the second adversarial pass on the corrected facts The second pass found these errors, and this commit fixes them: - The replay bullet said that calls go to the same destination. With --auth gateway, they go to the gateway. - seed-corpus.py ran a "memory" command by default. It now runs amem. - AGENTS.md said to run the script that reads a changed document. The demo seeds its record only once, so the text now says to seed a new record and run the golden suite and act 6. - On wslc, --server-auth gateway also starts the gateway in the VM. The text and two messages now say so. - Doc comments and the headers of generated config files named a "memory" command. The command is amem. - Wording: the verdict of a held write, the Muse flag, the web sessions, the Muse pin, the documents of the golden suite, and the key of the gateway on wslc. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bhj4Ck4y5iwZUsjpgQx8A2 --- AGENTS.md | 6 ++++-- README.md | 7 ++++--- crates/client/src/lib.rs | 2 +- crates/egress/src/lib.rs | 2 +- crates/memory/src/main.rs | 10 +++++----- crates/memory/src/wslc.rs | 10 +++++----- crates/memory/src/wslc_api.rs | 2 +- crates/policy/src/lib.rs | 2 +- crates/sandbox/src/lib.rs | 10 +++++----- crates/sandbox/src/linux.rs | 4 ++-- crates/web/src/lib.rs | 16 ++++++++-------- golden/docs.toml | 2 +- images/harness/Dockerfile | 10 +++++----- local/README.md | 26 +++++++++++++++----------- scripts/seed-corpus.py | 8 ++++---- 15 files changed, 62 insertions(+), 55 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 843b0950..0b63d218 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,8 +46,10 @@ before you override the instruction. - Act 11 of the demo (`scripts/demo/11-limits.sh`) prints the two lists at the top of `README.md`. - If you change a heading in `docs/`, a decision in `examples/decisions/` or those two - lists, run the script that reads it. + The demo seeds its record only once. So, after you change a file in `docs/` or + `examples/decisions/`, seed a new record with `make demo-ready RESET=1`. Then run + `make golden` and act 6 (`make demo-all ACTS=6`). After you change those two lists, run + act 11. - **Show that a new assertion fails before you trust it.** Run it against the code as it was before your change. Put what it reported in the pull request. If a test only ever passed, nobody has shown that it tests anything. Assert the rule, not a bound on the diff --git a/README.md b/README.md index 77c06227..ef3da21e 100644 --- a/README.md +++ b/README.md @@ -281,9 +281,10 @@ found it. It never shows their values. `LLMAO_KEY` alone points at a local LiteL port 4000. For the Foundation's gateway, put the PAT in `LLMAO_KEY`, and set `LLMAO_URL=https://llm.apache.org` beside it. A launch uses the gateway with `--auth gateway` (`AUTH=gateway` with `make`). The default is `--auth subscription`. -With `--server-auth gateway`, only the memory's own model calls use the gateway. On the -`wslc` backend, the gateway is a LiteLLM in the session VM, with `LITELLM_MASTER_KEY` -from `local/.env`. That backend does not use `LLMAO_URL` or `LLMAO_KEY`. +With `--server-auth gateway` (`SERVER_AUTH=gateway`), only the memory server's own model +calls use the gateway. On the `wslc` backend, the gateway is a LiteLLM in the session VM, +with `LITELLM_MASTER_KEY` from the environment or `credentials.env` (`make` reads +`local/.env`). That backend does not use `LLMAO_URL` or `LLMAO_KEY`. The page has a companion. The companion is a browser extension in `crates/web/companion`. It fetches for a session with the browser's own logins. It is for Chrome. Load it unpacked diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index 9d9b544f..0e13b37c 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -4,7 +4,7 @@ //! through this, over the same tool surface a harness uses. The rules of the ledger live //! on the server; this crate knows the tool names and the shape of their answers, and //! nothing else. It is transport-agnostic: in process over a byte pipe, over standard -//! input and output to a `memory serve`, or over HTTP to a remote server, the calls are +//! input and output to an `amem serve`, or over HTTP to a remote server, the calls are //! the same. use rmcp::model::CallToolRequestParams; diff --git a/crates/egress/src/lib.rs b/crates/egress/src/lib.rs index c8835bf3..80ac3ae7 100644 --- a/crates/egress/src/lib.rs +++ b/crates/egress/src/lib.rs @@ -1534,7 +1534,7 @@ async fn debugged( /// Record the call once its usage is known, when the body has streamed through. A call /// that reported usage is recorded as such; a call the upstream refused is recorded by -/// its status, so a refused key or an expired login shows up in `memory usage` rather +/// its status, so a refused key or an expired login shows up in `amem usage` rather /// than vanishing; anything else, a listing say, is not a model call. /// A copy of a response as it streams through, up to a cap, for its decision context. fn tap( diff --git a/crates/memory/src/main.rs b/crates/memory/src/main.rs index e61c5539..9f3df62b 100644 --- a/crates/memory/src/main.rs +++ b/crates/memory/src/main.rs @@ -2032,7 +2032,7 @@ fn record_reported_usage(p: &Prepared) { } } -/// `memory usage`: the telemetry by destination, model and who counted. +/// `amem usage`: the telemetry by destination, model and who counted. fn print_usage(rows: &[dm_egress::telemetry::Totals]) { if rows.is_empty() { println!("no model calls recorded"); @@ -2465,7 +2465,7 @@ async fn tap_cmd(root: &Path, op: TapOp) -> Fallible { } /// `mine` and the C API check. -/// `memory mine`: a transcript read by a small model through the session's destination. +/// `amem mine`: a transcript read by a small model through the session's destination. async fn mine_cmd( root: &Path, transcript: &Path, @@ -2498,7 +2498,7 @@ async fn mine_cmd( Ok(()) } -/// `memory models`: the destination's own model list, as the web form's picker reads it. +/// `amem models`: the destination's own model list, as the web form's picker reads it. async fn models_cmd(root: &Path, auth: AuthArg, provider: Option) -> Fallible { let auth = AuthMode::from(auth); if auth == AuthMode::Subscription { @@ -2992,7 +2992,7 @@ async fn cmd_launch(root: &Path, command: Command) -> Fallible { std::process::exit(code); } -/// The `memory inside` flags for a task run, after the run options. +/// The `amem inside` flags for a task run, after the run options. fn work_inside_args( mut args: Vec, domain: Option<&str>, @@ -3277,7 +3277,7 @@ fn split_program(harness: Harness, args: Vec) -> (Vec, Vec, prompt_file: Option<&Path>, diff --git a/crates/memory/src/wslc.rs b/crates/memory/src/wslc.rs index 723000e0..36a57f44 100644 --- a/crates/memory/src/wslc.rs +++ b/crates/memory/src/wslc.rs @@ -1,7 +1,7 @@ //! The `wslc` backend: the harness in the harness image, under WSL containers. //! //! Two containers in the session VM. The egress container runs this same binary as -//! `memory inside`, with the record mounted in and the only route outside: it opens the +//! `amem inside`, with the record mounted in and the only route outside: it opens the //! memory, serves the egress on a socket in a shared volume, and does everything the //! launcher does on Linux, including task mode. The harness container has no network at //! all, the working tree and its private home mounted in, and reaches the egress through @@ -81,7 +81,7 @@ pub struct Launch { pub workdir: PathBuf, /// The harness image. pub image: String, - /// The flags for `memory inside` that say what to run: `--prompt`, `--work`, and so + /// The flags for `amem inside` that say what to run: `--prompt`, `--work`, and so /// on, followed by `--` and the harness's own arguments. pub inside_args: Vec, /// Attached to the terminal: no prompt and no task. @@ -118,7 +118,7 @@ pub(crate) fn via_for(l: &Launch, windows: bool) -> Result { } else if l.interactive { Some("the C API gives a process no terminal, so an interactive launch goes through the command line") } else if wants_gateway(l) { - Some("the gateway inside the VM needs a network of its own, which the C API cannot make, so --auth gateway goes through the command line") + Some("the gateway inside the VM needs a network of its own, which the C API cannot make, so a gateway route goes through the command line") } else { None }; @@ -276,7 +276,7 @@ pub(crate) fn egress_env(taps: &[String], extra: &[(String, String)]) -> Vec<(St env } -/// The egress container's command: this binary as `memory inside`, told what to run. +/// The egress container's command: this binary as `amem inside`, told what to run. pub(crate) fn inside_command(l: &Launch) -> Vec { let mut a = s(&[ "/opt/memory/memory", @@ -565,7 +565,7 @@ fn gateway_up(root: &Path) -> Fallible { let master = std::env::var("LITELLM_MASTER_KEY") .ok() .filter(|k| !k.is_empty()) - .ok_or("--auth gateway inside the VM needs LITELLM_MASTER_KEY (from local/.env)")?; + .ok_or("the gateway inside the VM (--auth or --server-auth gateway) needs LITELLM_MASTER_KEY (from local/.env)")?; let pg_password = std::env::var("POSTGRES_PASSWORD").unwrap_or_else(|_| "litellm".into()); if run_ok(&s(&["network", "inspect", GATEWAY_NETWORK])).is_err() { run_ok(&s(&["network", "create", GATEWAY_NETWORK]))?; diff --git a/crates/memory/src/wslc_api.rs b/crates/memory/src/wslc_api.rs index bad1e33a..ae53e7df 100644 --- a/crates/memory/src/wslc_api.rs +++ b/crates/memory/src/wslc_api.rs @@ -49,7 +49,7 @@ pub(crate) struct Mounts { pub host_home: Option<(String, Vec)>, } -/// The egress container: this binary as `memory inside`, the record, the shared volume, +/// The egress container: this binary as `amem inside`, the record, the shared volume, /// the logins read-only, and the session's bridge as its only way out. pub(crate) fn egress_spec( l: &Launch, diff --git a/crates/policy/src/lib.rs b/crates/policy/src/lib.rs index 18a686de..84c09f3c 100644 --- a/crates/policy/src/lib.rs +++ b/crates/policy/src/lib.rs @@ -372,7 +372,7 @@ impl Grants { Grants::parse(&text) } - /// Render as TOML, for `memory init` to write. + /// Render as TOML, for `amem init` to write. /// /// # Errors /// diff --git a/crates/sandbox/src/lib.rs b/crates/sandbox/src/lib.rs index 31fa155a..425da01e 100644 --- a/crates/sandbox/src/lib.rs +++ b/crates/sandbox/src/lib.rs @@ -17,7 +17,7 @@ //! | Antigravity CLI | `GOOGLE_GEMINI_BASE_URL`; a login tunnels | `mcpServers..serverUrl` | `~/.gemini/antigravity-cli/settings.json`, `~/.gemini/config/` | //! | Qwen Code | `OPENAI_BASE_URL`; key only | `mcpServers..httpUrl` | `~/.qwen/settings.json` | //! | Grok Build | `GROK_XAI_API_BASE_URL`; a login tunnels | `mcp_servers..url` | `~/.grok/config.toml` | -//! | Muse Code | `--base-url` on `exec` | `mcp_servers.`, `streamable_http` | `~/.config/muse/settings.json` | +//! | Muse Code | `--base-url`, after the harness's arguments | `mcp_servers.`, `streamable_http` | `~/.config/muse/settings.json` | //! | pi | `baseUrl` of a provider of its own | `mcpServers..url` | `~/.pi/agent/models.json`, `mcp.json`, `extensions/` | //! | Copilot CLI | no override; `HTTPS_PROXY` to the egress, which tunnels to allowed hosts | `mcp-config.json` | `COPILOT_HOME` | //! | OpenCode | `provider..options.baseURL` | `mcp..url` | `XDG_CONFIG_HOME/opencode/opencode.json` | @@ -1151,7 +1151,7 @@ fn setup_codex( String::new() }; format!( - r#"# written by memory launch; edits are overwritten + r#"# written by amem launch; edits are overwritten {base}show_raw_agent_reasoning = true [features] @@ -1164,7 +1164,7 @@ url = "{relay}/mcp" } else { env.insert("DM_EGRESS_TOKEN".into(), "egress".into()); format!( - r#"# written by memory launch; edits are overwritten + r#"# written by amem launch; edits are overwritten model_provider = "egress" # the reasoning an open model returns is kept in the rollout, so the record has the why show_raw_agent_reasoning = true @@ -1456,7 +1456,7 @@ context_window = 131072 } }; let config = format!( - r#"# written by memory launch; edits are overwritten + r#"# written by amem launch; edits are overwritten [cli] auto_update = false @@ -1594,7 +1594,7 @@ fn setup_pi( )?; let memory = at.memory_literal(); let extension = format!( - r#"// written by memory launch; edits are overwritten. The launcher's hooks, as a pi + r#"// written by amem launch; edits are overwritten. The launcher's hooks, as a pi // extension: pi has none of its own, so the hook binary is run from here. import {{ spawnSync }} from "node:child_process"; diff --git a/crates/sandbox/src/linux.rs b/crates/sandbox/src/linux.rs index 0ca89e3e..4b9ec176 100644 --- a/crates/sandbox/src/linux.rs +++ b/crates/sandbox/src/linux.rs @@ -2,7 +2,7 @@ //! //! The child unshares user, mount, network, uts and ipc namespaces, maps itself to the //! caller's uid, builds a new root on a tmpfs from the plan's bind mounts, pivots into it, -//! and execs the relay. The relay (`memory relay`) then unshares a pid namespace and +//! and execs the relay. The relay (`amem relay`) then unshares a pid namespace and //! spawns the harness, mounting `/proc` from inside it, so the harness is pid 1 of its //! own world and sees no other process. //! @@ -731,7 +731,7 @@ fn die_with_intermediate(pidfd: libc::c_int) -> Result<(), Error> { } } -/// What `memory relay` does inside: spawn the harness and wait for it. The relay is +/// What `amem relay` does inside: spawn the harness and wait for it. The relay is /// already pid 1 of the harness's namespace, courtesy of [`run`]. /// /// # Errors diff --git a/crates/web/src/lib.rs b/crates/web/src/lib.rs index cadf3d0a..1f8e03a7 100644 --- a/crates/web/src/lib.rs +++ b/crates/web/src/lib.rs @@ -1,11 +1,11 @@ //! A web terminal on the launcher. //! -//! Each session is a pseudo-terminal running `amem launch`, kept alive by this server -//! whether or not a browser is attached. Its output goes into a ring buffer and to every -//! attached websocket; a browser that connects, or reconnects, gets the ring first, so a -//! refresh or a dropped connection lands in the same place. Beside the terminal a reading -//! pane renders entries from the record as they land. Loopback only, and behind a token: -//! see [`access`]. +//! Each session is a pseudo-terminal running `amem launch` or `amem work`, kept alive by +//! this server whether or not a browser is attached. Its output goes into a ring buffer and +//! to every attached websocket; a browser that connects, or reconnects, gets the ring +//! first, so a refresh or a dropped connection lands in the same place. Beside the terminal +//! a reading pane renders entries from the record as they land. Loopback only, and behind a +//! token: see [`access`]. use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade}; use axum::extract::{Path, State}; @@ -58,7 +58,7 @@ const RING_BYTES: usize = 2 * 1024 * 1024; pub struct Config { /// The record. pub root: PathBuf, - /// The `amem` binary; sessions run ` --root launch ...`. + /// The `amem` binary; sessions run ` --root launch ...` or `work ...`. pub memory_binary: PathBuf, /// The working tree sessions start in. pub workdir: PathBuf, @@ -374,7 +374,7 @@ fn options(config: &Config) -> Value { }) } -/// The models a destination lists, asked of the launcher itself: `memory models` opens +/// The models a destination lists, asked of the launcher itself: `amem models` opens /// the record, routes as a launch would, and prints what the destination's own model /// list says. A login mode has no list, since the login is the harness's. async fn models_via_launcher( diff --git a/golden/docs.toml b/golden/docs.toml index 28102321..d5c42cb5 100644 --- a/golden/docs.toml +++ b/golden/docs.toml @@ -1,4 +1,4 @@ -# Golden questions over the three documents in docs/, seeded as the demo corpus by +# Golden questions over the documents in docs/, seeded as the demo corpus by # scripts/seed-corpus.py, one entry per section. Answers name section titles, or parts # of them; any one is a hit. `keywords` is the query a person would write, standing in # for the one a model writes. Run: amem golden golden/docs.toml diff --git a/images/harness/Dockerfile b/images/harness/Dockerfile index e03a6c69..d4bfd0c2 100644 --- a/images/harness/Dockerfile +++ b/images/harness/Dockerfile @@ -1,8 +1,8 @@ -# The harness image: every harness `amem launch` knows but Antigravity, at pinned -# versions, on one base, with the memory binary where the sandbox expects it, and Python -# for workloads that are programs rather than harnesses, such as a Gofannon agent. The same -# image on every machine is the point; nothing here depends on what a developer's WSL or -# laptop contains. +# The harness image: every harness `amem launch` knows but Antigravity, at pinned versions +# (Muse Code by its release channel), on one base, with the memory binary where the sandbox +# expects it, and Python for workloads that are programs rather than harnesses, such as a +# Gofannon agent. The same image on every machine is the point; nothing here depends on what +# a developer's WSL or laptop contains. # # Build with `make image`, which copies the release binary in beside this file. FROM ubuntu:24.04 diff --git a/local/README.md b/local/README.md index 4f11d564..b2325303 100644 --- a/local/README.md +++ b/local/README.md @@ -72,9 +72,10 @@ permission prompts off. Nobody is there to answer them, and a task needs edits a commands. The sandbox is the boundary, and the working tree is what the harness can change. The harness prints the answer and exits with its exit code. A launch that leaves no handoff exits with code 4, unless the harness itself failed: then the launch exits with -the code of the harness. A note that a hook files, before compaction or at session end, -counts as a handoff. The reason: a non-interactive launch whose outcome is only in its -transcript leaves nothing for the next session. If you intend this, use `--no-handoff`. +the code of the harness. A note that a hook leaves, before compaction or at session end, +counts as a handoff when the launcher files it. The reason: a non-interactive launch whose +outcome is only in its transcript leaves nothing for the next session. If you intend this, +use `--no-handoff`. `make work` claims the next admitted task that the session may take. It makes the prompt from: @@ -364,8 +365,8 @@ the record. It goes to `state/quarantine/.md`. It is never indexed or served owner gets a task that requires `human`, with the scan's excerpts and the commands to act. `amem quarantine list|show|release|drop` are the owner's commands. Release writes the entry into the record as it was written, with its author and all. Drop deletes the held -write from `state/quarantine`, and the reviewer's notes beside it. It writes nothing to -the record. There are two more gates: +write from `state/quarantine`, and the verdict beside it. It writes nothing to the record. +There are two more gates: - This gate counts the entries that an agent session supersedes. The limit is `max_supersedes_per_session` (`[gates]` in `grants.toml`, twenty by default). When a @@ -853,7 +854,8 @@ decision contexts that carry its text, and the receipt lists them. `amem replay ` is the paper's frozen-context replay on those decision contexts. It sends each recorded request again, up to `--limit`, or one `--turn`: -- to the same destination and path; +- to the same path, at the destination that the session used, or at the gateway with + `--auth gateway`; - with only the model changed, and the answer not streamed; - through the egress, metered and under the spend limits. @@ -1388,7 +1390,7 @@ hook left it, not the agent, and it points to the full transcript. The launcher always files a note from before compaction, because its content is no longer in the context of the harness. It files a note from session end only when the session left no handoff of its own. This note is the handoff of last resort. Thus a prompted launch -where the agent wrote no handoff does not exit 4 when a hook filed a note. +where the agent wrote no handoff does not exit 4 when the launcher filed a note. Transcripts stay where the harness keeps them, under the private home in the state of the record. Thus nothing is lost, also when the launcher files no note. The hook reads all @@ -1515,7 +1517,8 @@ make launch BACKEND=wslc PROMPT='say pong' # prompted make work BACKEND=wslc # a task from the ledger ``` -The session VM has two containers, and two more for the gateway with `--auth gateway`: +The session VM has two containers. It has two more for the gateway, with `--auth gateway` +or `--server-auth gateway`: - The egress container runs the Linux `amem`, with your record mounted in. It has the only route outside. It opens the memory, serves the egress on a socket in a shared @@ -1528,9 +1531,10 @@ The host side only changes run requests into `wslc run`, and reports the exit co login directories (`~/.claude`, `~/.codex`, `~/.gemini`) go into the egress container read-only. The egress container copies the one file that the harness needs into the private home. Keys and `GH_TOKEN` go in as environment. The session VM cannot reach -LiteLLM on the loopback of this machine. Thus `--auth gateway` runs a gateway of its own in -the VM, as [The gateway inside the VM](#the-gateway-inside-the-vm) says. That gateway -replaces `LLMAO_URL` and `LLMAO_KEY`, also when they name a remote gateway. +LiteLLM on the loopback of this machine. Thus `--auth gateway` and `--server-auth gateway` +run a gateway of their own in the VM, as [The gateway inside the +VM](#the-gateway-inside-the-vm) says. That gateway replaces `LLMAO_URL` and `LLMAO_KEY`, +also when they name a remote gateway. `--unconfined` runs the same configuration without namespaces, with the egress on a TCP port. It is for platforms that have no sandbox backend. macOS gets Seatbelt later. diff --git a/scripts/seed-corpus.py b/scripts/seed-corpus.py index 31a225a6..89bf92c2 100755 --- a/scripts/seed-corpus.py +++ b/scripts/seed-corpus.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 """Seed a memory record from Markdown, one entry per section, for a realistic demo corpus. -Usage: seed-corpus.py --root ROOT [--kind KIND] [--min-words N] PATH... +Usage: seed-corpus.py --root ROOT [--kind KIND] [--min-words N] [--memory AMEM] PATH... Each .md file under PATH is split on second-level headings; each section becomes an entry titled ":
". Files under a directory named decisions become decisions; everything else the kind given (default fact). Sections shorter than --min-words -are folded into the previous one. Writes go through the memory CLI, so they land at raw -trust under the owner, like any other write. +are folded into the previous one. Writes go through the amem CLI (--memory, `amem` on PATH +by default), so they land at raw trust under the owner, like any other write. """ import argparse, os, re, subprocess, sys @@ -34,7 +34,7 @@ def sections(text): def main(): ap = argparse.ArgumentParser() ap.add_argument("--root", required=True); ap.add_argument("--kind", default="fact") - ap.add_argument("--min-words", type=int, default=40); ap.add_argument("--memory", default="memory") + ap.add_argument("--min-words", type=int, default=40); ap.add_argument("--memory", default="amem") ap.add_argument("paths", nargs="+") a = ap.parse_args() files = [] From ed672a888bef87da13bf137f4465e6735b2161e1 Mon Sep 17 00:00:00 2001 From: Sander Striker Date: Thu, 29 Oct 2026 22:58:28 +0100 Subject: [PATCH 04/14] fix: answer the third adversarial pass on the corrected facts The third pass found these errors, and this commit fixes them: - The act 6 and act 11 commands in AGENTS.md failed without HARNESS. They now give it, and say that act 6 needs the gateway key. - The section on the gateway in the VM, the error message and the doctor text named only --auth gateway, and local/.env as the only source of the key. amem reads the key from the environment or credentials.env. - Four more doc comments named a "memory" command. - docs/terms.md says to write "outbox note" for what a hook leaves. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bhj4Ck4y5iwZUsjpgQx8A2 --- AGENTS.md | 6 ++++-- crates/memory/src/credentials.rs | 2 +- crates/memory/src/taps.rs | 2 +- crates/memory/src/wslc.rs | 2 +- crates/memory/tests/launch.rs | 4 ++-- crates/web/tests/reader_browser.rs | 2 +- local/README.md | 25 +++++++++++++------------ 7 files changed, 23 insertions(+), 20 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0b63d218..01314999 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,8 +48,10 @@ before you override the instruction. The demo seeds its record only once. So, after you change a file in `docs/` or `examples/decisions/`, seed a new record with `make demo-ready RESET=1`. Then run - `make golden` and act 6 (`make demo-all ACTS=6`). After you change those two lists, run - act 11. + `make golden` and act 6 (`make demo-all HARNESS=codex ACTS=6`). Act 6 needs the + gateway key in `local/.virtual-key`. Without it, the act says "skipped" and checks + nothing. After you change those two lists, run act 11 + (`make demo-all HARNESS=codex ACTS=11`). - **Show that a new assertion fails before you trust it.** Run it against the code as it was before your change. Put what it reported in the pull request. If a test only ever passed, nobody has shown that it tests anything. Assert the rule, not a bound on the diff --git a/crates/memory/src/credentials.rs b/crates/memory/src/credentials.rs index 0d232cc7..7eac8932 100644 --- a/crates/memory/src/credentials.rs +++ b/crates/memory/src/credentials.rs @@ -38,7 +38,7 @@ pub const KNOWN: &[(&str, &str)] = &[ ), ( "LITELLM_MASTER_KEY", - "--auth gateway on the wslc backend, which runs its own gateway", + "--auth or --server-auth gateway on the wslc backend, which runs its own gateway", ), ("POSTGRES_PASSWORD", "that gateway's database"), ]; diff --git a/crates/memory/src/taps.rs b/crates/memory/src/taps.rs index 2c51d209..deb59b35 100644 --- a/crates/memory/src/taps.rs +++ b/crates/memory/src/taps.rs @@ -1,4 +1,4 @@ -//! `memory tap`: a connector installed under the root and run under its grant. The +//! `amem tap`: a connector installed under the root and run under its grant. The //! component lives at `/taps/.wasm`, the grant in the grants file, and the //! credential in the launcher's environment under the variable the grant names; the //! tap gets the host's outbound hook and nothing else. diff --git a/crates/memory/src/wslc.rs b/crates/memory/src/wslc.rs index 36a57f44..9a0787ae 100644 --- a/crates/memory/src/wslc.rs +++ b/crates/memory/src/wslc.rs @@ -565,7 +565,7 @@ fn gateway_up(root: &Path) -> Fallible { let master = std::env::var("LITELLM_MASTER_KEY") .ok() .filter(|k| !k.is_empty()) - .ok_or("the gateway inside the VM (--auth or --server-auth gateway) needs LITELLM_MASTER_KEY (from local/.env)")?; + .ok_or("the gateway inside the VM (--auth or --server-auth gateway) needs LITELLM_MASTER_KEY, in the environment or credentials.env (make reads local/.env)")?; let pg_password = std::env::var("POSTGRES_PASSWORD").unwrap_or_else(|_| "litellm".into()); if run_ok(&s(&["network", "inspect", GATEWAY_NETWORK])).is_err() { run_ok(&s(&["network", "create", GATEWAY_NETWORK]))?; diff --git a/crates/memory/tests/launch.rs b/crates/memory/tests/launch.rs index 66262b6b..4e2f0e88 100644 --- a/crates/memory/tests/launch.rs +++ b/crates/memory/tests/launch.rs @@ -1945,7 +1945,7 @@ fn a_confined_run_under_umask_000_runs() { // ---- the wslc backend's inside half, driven by a fake host ---- -/// `memory inside` writes run requests for the host and waits for exit codes. This fake +/// `amem inside` writes run requests for the host and waits for exit codes. This fake /// host answers them from the test, which proves the protocol and the plan the inside /// builds for the harness image without a container runtime. /// The host's side of the protocol: wait for the request file. @@ -2025,7 +2025,7 @@ fn inside_writes_image_requests_and_reports_done() { // ---- hooks: a handoff the harness did not write ---- /// A fake harness that behaves like Claude Code's hooks: before "compaction" and at -/// session end it runs `memory hook claude` with the event on standard input and a +/// session end it runs `amem hook claude` with the event on standard input and a /// transcript on disk, and writes no handoff of its own. const FIRES_HOOKS: &str = r#" t=./transcript.jsonl diff --git a/crates/web/tests/reader_browser.rs b/crates/web/tests/reader_browser.rs index 6b5aebdd..f124585a 100644 --- a/crates/web/tests/reader_browser.rs +++ b/crates/web/tests/reader_browser.rs @@ -1,4 +1,4 @@ -//! The browser reader, driven by a real browser: `memory web` serves the page, the +//! The browser reader, driven by a real browser: `amem web` serves the page, the //! page exports a domain into a bundle, verifies every entry's digest against the //! manifest, indexes the entries in SQLite with FTS5 compiled to WebAssembly, and ranks //! a query the way the server does. Headless Chrome or Chromium loads the page with a diff --git a/local/README.md b/local/README.md index b2325303..95cd0197 100644 --- a/local/README.md +++ b/local/README.md @@ -72,10 +72,10 @@ permission prompts off. Nobody is there to answer them, and a task needs edits a commands. The sandbox is the boundary, and the working tree is what the harness can change. The harness prints the answer and exits with its exit code. A launch that leaves no handoff exits with code 4, unless the harness itself failed: then the launch exits with -the code of the harness. A note that a hook leaves, before compaction or at session end, -counts as a handoff when the launcher files it. The reason: a non-interactive launch whose -outcome is only in its transcript leaves nothing for the next session. If you intend this, -use `--no-handoff`. +the code of the harness. An outbox note that a hook leaves, before compaction or at +session end, counts as a handoff when the launcher files it. The reason: a non-interactive +launch whose outcome is only in its transcript leaves nothing for the next session. If you +intend this, use `--no-handoff`. `make work` claims the next admitted task that the session may take. It makes the prompt from: @@ -1390,7 +1390,7 @@ hook left it, not the agent, and it points to the full transcript. The launcher always files a note from before compaction, because its content is no longer in the context of the harness. It files a note from session end only when the session left no handoff of its own. This note is the handoff of last resort. Thus a prompted launch -where the agent wrote no handoff does not exit 4 when the launcher filed a note. +where the agent wrote no handoff does not exit 4 when the launcher filed an outbox note. Transcripts stay where the harness keeps them, under the private home in the state of the record. Thus nothing is lost, also when the launcher files no note. The hook reads all @@ -1427,13 +1427,14 @@ and filed two facts. ## The gateway inside the VM -`--backend wslc --auth gateway` runs LiteLLM over Postgres as two containers, on their own -network in the session VM. It does this because the session VM cannot reach the gateway on -the loopback of the host. The egress container joins that network. The two containers stay -running between launches, as the host stack does. -`wslc remove -f memory-litellm memory-postgres` removes them. The egress uses the key -`LITELLM_MASTER_KEY` from `local/.env`. This is correct for a laptop. It is not correct -for the Foundation, where the front desk mints a virtual key for each principal. +`--backend wslc` with `--auth gateway` or `--server-auth gateway` runs LiteLLM over +Postgres as two containers, on their own network in the session VM. It does this because +the session VM cannot reach the gateway on the loopback of the host. The egress container +joins that network. The two containers stay running between launches, as the host stack +does. `wslc remove -f memory-litellm memory-postgres` removes them. The egress uses the +key `LITELLM_MASTER_KEY` from the environment or `credentials.env` (`make` reads +`local/.env`). This is correct for a laptop. It is not correct for the Foundation, where +the front desk mints a virtual key for each principal. ## The WSL containers C API From cc34848e40ee53f0a40e68547cd71a9cb2508a7b Mon Sep 17 00:00:00 2001 From: Sander Striker Date: Thu, 29 Oct 2026 23:13:55 +0100 Subject: [PATCH 05/14] fix: answer the fourth adversarial pass on the corrected facts The fourth pass found these errors, and this commit fixes them: - The error for a tap with no component said to run a "memory" command. The command is amem. - Three more places in local/README.md said "note" for an outbox note. - Act 6 needs a key for llm.apache.org, not for any gateway. - The text and the error now say that the key file is the root's credentials.env. - Comments in the Seatbelt backend, its profile text and the otel crate's description called the binary "memory". Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bhj4Ck4y5iwZUsjpgQx8A2 --- AGENTS.md | 4 ++-- README.md | 4 ++-- crates/memory/src/taps.rs | 2 +- crates/memory/src/wslc.rs | 2 +- crates/otel/Cargo.toml | 2 +- crates/sandbox/src/seatbelt.rs | 8 ++++---- local/README.md | 19 ++++++++++--------- 7 files changed, 21 insertions(+), 20 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 01314999..429866d4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,8 +48,8 @@ before you override the instruction. The demo seeds its record only once. So, after you change a file in `docs/` or `examples/decisions/`, seed a new record with `make demo-ready RESET=1`. Then run - `make golden` and act 6 (`make demo-all HARNESS=codex ACTS=6`). Act 6 needs the - gateway key in `local/.virtual-key`. Without it, the act says "skipped" and checks + `make golden` and act 6 (`make demo-all HARNESS=codex ACTS=6`). Act 6 needs a key + for llm.apache.org in `local/.virtual-key`. Without it, the act says "skipped" and checks nothing. After you change those two lists, run act 11 (`make demo-all HARNESS=codex ACTS=11`). - **Show that a new assertion fails before you trust it.** Run it against the code as it diff --git a/README.md b/README.md index ef3da21e..e626d41a 100644 --- a/README.md +++ b/README.md @@ -283,8 +283,8 @@ port 4000. For the Foundation's gateway, put the PAT in `LLMAO_KEY`, and set `--auth gateway` (`AUTH=gateway` with `make`). The default is `--auth subscription`. With `--server-auth gateway` (`SERVER_AUTH=gateway`), only the memory server's own model calls use the gateway. On the `wslc` backend, the gateway is a LiteLLM in the session VM, -with `LITELLM_MASTER_KEY` from the environment or `credentials.env` (`make` reads -`local/.env`). That backend does not use `LLMAO_URL` or `LLMAO_KEY`. +with `LITELLM_MASTER_KEY` from the environment or the root's `credentials.env` (`make` +reads `local/.env`). That backend does not use `LLMAO_URL` or `LLMAO_KEY`. The page has a companion. The companion is a browser extension in `crates/web/companion`. It fetches for a session with the browser's own logins. It is for Chrome. Load it unpacked diff --git a/crates/memory/src/taps.rs b/crates/memory/src/taps.rs index deb59b35..7e50a4e7 100644 --- a/crates/memory/src/taps.rs +++ b/crates/memory/src/taps.rs @@ -64,7 +64,7 @@ pub async fn run( })?; if !path.is_file() { return Err(format!( - "no component installed as {name}; run: memory tap install {name} " + "no component installed as {name}; run: amem tap install {name} " ) .into()); } diff --git a/crates/memory/src/wslc.rs b/crates/memory/src/wslc.rs index 9a0787ae..b2207d33 100644 --- a/crates/memory/src/wslc.rs +++ b/crates/memory/src/wslc.rs @@ -565,7 +565,7 @@ fn gateway_up(root: &Path) -> Fallible { let master = std::env::var("LITELLM_MASTER_KEY") .ok() .filter(|k| !k.is_empty()) - .ok_or("the gateway inside the VM (--auth or --server-auth gateway) needs LITELLM_MASTER_KEY, in the environment or credentials.env (make reads local/.env)")?; + .ok_or("the gateway inside the VM (--auth or --server-auth gateway) needs LITELLM_MASTER_KEY, in the environment or the root's credentials.env (make reads local/.env)")?; let pg_password = std::env::var("POSTGRES_PASSWORD").unwrap_or_else(|_| "litellm".into()); if run_ok(&s(&["network", "inspect", GATEWAY_NETWORK])).is_err() { run_ok(&s(&["network", "create", GATEWAY_NETWORK]))?; diff --git a/crates/otel/Cargo.toml b/crates/otel/Cargo.toml index 120e26ab..85ca5884 100644 --- a/crates/otel/Cargo.toml +++ b/crates/otel/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true rust-version.workspace = true license.workspace = true repository.workspace = true -description = "OpenTelemetry traces from the memory binary: a facade over the OTLP exporter" +description = "OpenTelemetry traces from the amem binary: a facade over the OTLP exporter" [dependencies] opentelemetry = { version = "0.33", default-features = false, features = ["trace"] } diff --git a/crates/sandbox/src/seatbelt.rs b/crates/sandbox/src/seatbelt.rs index edc4965e..cf05be4f 100644 --- a/crates/sandbox/src/seatbelt.rs +++ b/crates/sandbox/src/seatbelt.rs @@ -15,7 +15,7 @@ //! - a handful of devices, and the system read-only, [`SYSTEM`]; //! - the metadata, and only that, of each directory above a grant, so a path can be //! looked up through them; nothing in them can be listed or read; -//! - the harness's install, the memory binary and the projection, read-only; +//! - the harness's install, the amem binary and the projection, read-only; //! - the private home and the working tree, read and write; //! - the network: the egress socket, and the relay's port on loopback, and nothing else. //! @@ -98,7 +98,7 @@ const DEVICES: &str = r#" /// How a grant may be used. #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum Access { - /// Read: the harness's install, the memory binary, the projection. + /// Read: the harness's install, the amem binary, the projection. Read, /// Read and write: the private home and the working tree. ReadWrite, @@ -137,7 +137,7 @@ pub fn profile(plan: &Plan) -> Result { ) } -/// The plan's mounts as grants: the egress socket to connect to, the memory binary a +/// The plan's mounts as grants: the egress socket to connect to, the amem binary a /// file to read, and each other mount a directory, read-only or read-write as mounted. fn grants(plan: &Plan) -> Result, Error> { if plan.layout.backend != Backend::Seatbelt { @@ -212,7 +212,7 @@ fn render(grants: &[Grant], relay: &str, host_home: Option<&Path>) -> Result Date: Thu, 29 Oct 2026 23:20:12 +0100 Subject: [PATCH 06/14] fix: answer the fifth adversarial pass on the corrected facts The fifth pass found these, and this commit fixes them: - Two more places in local/README.md said "note" for an outbox note. - Comments in the sandbox crate called the binary "memory". - Act 6 says "skipped" only when there is no key. A key for another gateway makes it fail instead. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bhj4Ck4y5iwZUsjpgQx8A2 --- AGENTS.md | 4 ++-- crates/sandbox/src/lib.rs | 16 ++++++++-------- crates/sandbox/src/seatbelt.rs | 2 +- local/README.md | 10 +++++----- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 429866d4..9becf94a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,8 +49,8 @@ before you override the instruction. The demo seeds its record only once. So, after you change a file in `docs/` or `examples/decisions/`, seed a new record with `make demo-ready RESET=1`. Then run `make golden` and act 6 (`make demo-all HARNESS=codex ACTS=6`). Act 6 needs a key - for llm.apache.org in `local/.virtual-key`. Without it, the act says "skipped" and checks - nothing. After you change those two lists, run act 11 + for llm.apache.org in `local/.virtual-key`. Without a key there, the act says + "skipped" and checks nothing. After you change those two lists, run act 11 (`make demo-all HARNESS=codex ACTS=11`). - **Show that a new assertion fails before you trust it.** Run it against the code as it was before your change. Put what it reported in the pull request. If a test only ever diff --git a/crates/sandbox/src/lib.rs b/crates/sandbox/src/lib.rs index 425da01e..91378b5d 100644 --- a/crates/sandbox/src/lib.rs +++ b/crates/sandbox/src/lib.rs @@ -1,7 +1,7 @@ //! Confinement for a harness: what it may see, and the one way out. //! //! A [`Plan`] says what goes into the sandbox: a root with the system's binaries read-only, -//! a private home, the working tree, the memory binary, the egress socket, and nothing +//! a private home, the working tree, the amem binary, the egress socket, and nothing //! else. The network namespace is empty. The harness reaches the egress through a relay //! on loopback that forwards to the socket, and the egress is the only process with the //! network. @@ -376,7 +376,7 @@ pub struct Layout { pub home: PathBuf, /// The egress socket. pub socket: PathBuf, - /// The memory binary, which runs the relay and the hooks. + /// The amem binary, which runs the relay and the hooks. pub memory: PathBuf, /// The loopback address the relay listens on, as `host:port`. pub relay: String, @@ -433,7 +433,7 @@ impl Layout { format!("{} hook {harness}", shell_word(&self.memory)) } - /// The memory binary as a JSON, and so JavaScript, string literal. + /// The amem binary as a JSON, and so JavaScript, string literal. fn memory_literal(&self) -> String { serde_json::to_string(&self.memory.display().to_string()).unwrap_or_default() } @@ -490,7 +490,7 @@ pub struct Request<'a> { /// OpenCode lists a custom provider's models, and cannot ask models.dev here. pub model: Option<&'a str>, /// The harness runs in the harness image rather than from this host's binaries: the - /// harness is at `/usr/local/bin/`, the memory binary is already inside, and + /// harness is at `/usr/local/bin/`, the amem binary is already inside, and /// nothing of the host's system is mounted. Set by the `wslc` backend. pub image: bool, /// Where the working tree appears inside, when not at its host path. The `wslc` @@ -518,7 +518,7 @@ impl Request<'_> { pub const INSIDE_HOME: &str = "/home/user"; /// The egress socket inside. pub const INSIDE_SOCKET: &str = "/run/memory/egress.sock"; -/// The memory binary inside. Outside `/usr`, which is mounted read-only before this. +/// The amem binary inside. Outside `/usr`, which is mounted read-only before this. pub const INSIDE_MEMORY: &str = "/opt/memory/memory"; /// The loopback address the relay listens on inside. pub const INSIDE_RELAY: &str = "127.0.0.1:4100"; @@ -840,7 +840,7 @@ fn provider_of(req: &Request<'_>) -> Result { /// /// Where the layout is the host's own, as Seatbelt's is, nothing is mounted and nothing /// hidden, but the tree is granted read-write: it may not sit in the read-only -/// projection, nor hold the private home, the projection, the memory binary that runs +/// projection, nor hold the private home, the projection, the amem binary that runs /// the hooks, or the egress socket. fn refuse_a_hidden_workdir(req: &Request<'_>, at: &Layout) -> Result<(), Error> { let inside = req.workdir_inside(); @@ -2830,14 +2830,14 @@ mod tests { ] { assert!(plugin.contains(needle), "{needle} in\n{plugin}"); } - // driving it takes a fake memory binary, which is a shell script + // driving it takes a fake amem binary, which is a shell script #[cfg(unix)] if let Ok(node) = find_on_path("node") { drive_opencode_plugin(&node, &plugin); } } - /// The plugin under a node, against a fake memory binary that records what it is + /// The plugin under a node, against a fake amem binary that records what it is /// sent and refuses a tool call: a denial throws its reason; the turns arrive as /// events and the idle event writes the transcript and leaves the session-end note /// without awaiting anything, since OpenCode's run exits on idle; a session id is a diff --git a/crates/sandbox/src/seatbelt.rs b/crates/sandbox/src/seatbelt.rs index cf05be4f..7fa21aae 100644 --- a/crates/sandbox/src/seatbelt.rs +++ b/crates/sandbox/src/seatbelt.rs @@ -685,7 +685,7 @@ mod tests { let mut m = mount(path, false); let mut bad = plan_with(Vec::new()); if file { - // the home as the memory binary's literal + // the home as the amem binary's literal bad.layout.memory = PathBuf::from(path); m.read_only = true; } diff --git a/local/README.md b/local/README.md index 7bbabdd3..01d4941a 100644 --- a/local/README.md +++ b/local/README.md @@ -1384,14 +1384,14 @@ the model would use a shell instead, which the hook does not see. Each hook runs `amem` in the sandbox: at `/opt/memory/memory` under namespaces and in the harness image, and at its host path under Seatbelt. The binary reads the event and the tail of the transcript, and leaves a note in an outbox in the private home. When the -session ends, the launcher files the notes as handoffs. Each has a mark that says that the -hook left it, not the agent, and it points to the full transcript. +session ends, the launcher files the outbox notes as handoffs. Each has a mark that says +that the hook left it, not the agent, and it points to the full transcript. The launcher always files an outbox note from before compaction, because its content is no longer in the context of the harness. It files an outbox note from session end only when -the session left no handoff of its own. This note is the handoff of last resort. Thus a -prompted launch where the agent wrote no handoff does not exit 4 when the launcher filed -an outbox note. +the session left no handoff of its own. This outbox note is the handoff of last resort. +Thus a prompted launch where the agent wrote no handoff does not exit 4 when the launcher +filed an outbox note. Transcripts stay where the harness keeps them, under the private home in the state of the record. Thus nothing is lost, also when the launcher files no outbox note. The hook reads From f08c293fc2c01ee3d7b7309e0241b33566933ed2 Mon Sep 17 00:00:00 2001 From: Sander Striker Date: Thu, 29 Oct 2026 23:21:23 +0100 Subject: [PATCH 07/14] docs: say that act 6 also takes the key from LLMAO_KEY The sixth adversarial pass found that act 6 runs with a key in LLMAO_KEY too, not only with a key in local/.virtual-key. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bhj4Ck4y5iwZUsjpgQx8A2 --- AGENTS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9becf94a..71e3b1d5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,8 +49,8 @@ before you override the instruction. The demo seeds its record only once. So, after you change a file in `docs/` or `examples/decisions/`, seed a new record with `make demo-ready RESET=1`. Then run `make golden` and act 6 (`make demo-all HARNESS=codex ACTS=6`). Act 6 needs a key - for llm.apache.org in `local/.virtual-key`. Without a key there, the act says - "skipped" and checks nothing. After you change those two lists, run act 11 + for llm.apache.org, in `local/.virtual-key` or `LLMAO_KEY`. Without a key, the act + says "skipped" and checks nothing. After you change those two lists, run act 11 (`make demo-all HARNESS=codex ACTS=11`). - **Show that a new assertion fails before you trust it.** Run it against the code as it was before your change. Put what it reported in the pull request. If a test only ever From cffa9cab8d19328b58f28986c1f06c56e295a41d Mon Sep 17 00:00:00 2001 From: Sander Striker Date: Fri, 30 Oct 2026 09:20:37 +0100 Subject: [PATCH 08/14] fix: answer the review and correct two open facts The Dockerfile comment names the amem binary and its path in the image. The image workflow says why it stages the binary as `memory`. The demo narrative says that act 10 writes a new public note. The count of 309 is 304 live entries, the new note, and four superseded entries. The eval task asf-pressures names a part that exists, and expects six. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bhj4Ck4y5iwZUsjpgQx8A2 --- .github/workflows/image.yml | 1 + docs/demo-narrative.md | 7 ++++--- evals/demo.toml | 4 ++-- images/harness/Dockerfile | 8 ++++---- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/image.yml b/.github/workflows/image.yml index 0e5bb051..fb747743 100644 --- a/.github/workflows/image.yml +++ b/.github/workflows/image.yml @@ -84,6 +84,7 @@ jobs: toolchain: stable - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - name: Build the amem binary + # staged as `memory`: the Dockerfile copies that file to /opt/memory/memory run: cargo build --release --locked -p amem && cp target/release/amem images/harness/memory - name: Build the image run: docker build --pull -t harness:${{ github.sha }} images/harness diff --git a/docs/demo-narrative.md b/docs/demo-narrative.md index 3c859f57..002d91af 100644 --- a/docs/demo-narrative.md +++ b/docs/demo-narrative.md @@ -398,9 +398,10 @@ nothing already in the log. record. 304 entries are live: all except the four superseded entries and the one erased entry. It replays 172 ledger events. Nothing is behind the log. -**10.3 Exported at a label.** You mark one note public. A public export carries that one -entry. An internal export carries all 309 entries of `dom-demo`, with the superseded -ones, but not the platform's domain. The entry erased in 1.4 is in neither export. +**10.3 Exported at a label.** You write one new note, marked public. A public export +carries that one entry. An internal export carries all 309 entries of `dom-demo`: the 304 +live, the new note, and the four superseded, but not the platform's domain. The entry +erased in 1.4 is in neither export. **10.4 Imported elsewhere.** A second record, under another owner and domain, imports the internal bundle: 309 entries. A search of it for DR-19 finds 10 hits. diff --git a/evals/demo.toml b/evals/demo.toml index ec51dccd..ca09e0a0 100644 --- a/evals/demo.toml +++ b/evals/demo.toml @@ -27,8 +27,8 @@ expect = "S2" [[task]] id = "asf-pressures" -prompt = "In the 'Seeding a Foundation' notes, how many pressures does the part 'What ASF stresses that the paper had to answer' list? Give the number." -expect = "\\b(five|5)\\b" +prompt = "In the 'Seeding a Foundation' notes, how many pressures does the part 'What the Foundation taught the design' list? Give the number." +expect = "\\b(six|6)\\b" [[task]] id = "committers-gate" diff --git a/images/harness/Dockerfile b/images/harness/Dockerfile index d4bfd0c2..23c22c41 100644 --- a/images/harness/Dockerfile +++ b/images/harness/Dockerfile @@ -1,8 +1,8 @@ # The harness image: every harness `amem launch` knows but Antigravity, at pinned versions -# (Muse Code by its release channel), on one base, with the memory binary where the sandbox -# expects it, and Python for workloads that are programs rather than harnesses, such as a -# Gofannon agent. The same image on every machine is the point; nothing here depends on what -# a developer's WSL or laptop contains. +# (Muse Code by its release channel), on one base, with the amem binary at +# /opt/memory/memory, where the sandbox expects it, and Python for workloads that are +# programs rather than harnesses, such as a Gofannon agent. The same image on every machine +# is the point; nothing here depends on what a developer's WSL or laptop contains. # # Build with `make image`, which copies the release binary in beside this file. FROM ubuntu:24.04 From f25435aa80164e1a7a2e91a471858c7b80ff6254 Mon Sep 17 00:00:00 2001 From: Sander Striker Date: Fri, 30 Oct 2026 09:52:26 +0100 Subject: [PATCH 09/14] fix: point the golden questions and the evals at the current text The adversarial pass found that the paper and the case study had changed under the golden suite and the evals. - golden/docs.toml: 6 of 14 questions named a heading that does not exist, and 2 matched the wrong document. All 14 now name the current heading. - evals/demo.toml: committers-gate expected "committer", but the case study now names other people as the gate. asf-pressures accepted a "6" from a section reference. The seed instruction now uses make demo-ready. - Act 10 says on screen that it writes a new note. - docs/reference-stack.md no longer says that the launcher runs two harnesses. - local/README.md says that its eval results are for the old tasks. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bhj4Ck4y5iwZUsjpgQx8A2 --- docs/reference-stack.md | 2 +- evals/demo.toml | 10 +++++++--- golden/docs.toml | 40 ++++++++++++++++++++-------------------- local/README.md | 2 ++ scripts/demo/10-moves.sh | 2 +- 5 files changed, 31 insertions(+), 25 deletions(-) diff --git a/docs/reference-stack.md b/docs/reference-stack.md index d2087b8e..dc1ee518 100644 --- a/docs/reference-stack.md +++ b/docs/reference-stack.md @@ -28,7 +28,7 @@ This rule also gives two results with no added work. First, the paper requires o | Ledger and lease cache | PostgreSQL, one schema for each domain | A claim is one statement with row locking and `SKIP LOCKED`. It is atomic and takes a few ms. Domains are independent, so partitions and replicas scale by domain. A rebuild from the log is possible | One container | | Policy engine | OPA as a sidecar, registry and grants as data | Partial evaluation compiles the session filter one time. Each later check is local. The filter goes into the Solr query, so Solr never scores a denied entry | One sidecar for each memory-server replica | | Memory server | A stateless service. It has the MCP and HTTP surface, the compiled filter and T0 in a cache for each session, and the read and write checks. In a launch, it limits reads to the ceiling of the model | Replicas behind a load balancer. T0 is a prefix that the harness caches. Thus orientation needs zero tool calls | One container. Scale by count | -| Launcher and sandbox | Ours. Linux namespaces, WSL containers, or Seatbelt on macOS (not yet run on a Mac) confine each harness. The harness has no network other than the egress. `--unconfined` removes the sandbox and keeps the egress, for a platform without a sandbox backend. So far, it runs Claude Code and Codex | It runs where the harness runs, one for each session. Thus it scales with the people and agents that work, not with the stack | The `amem` binary | +| Launcher and sandbox | Ours. Linux namespaces, WSL containers, or Seatbelt on macOS (not yet run on a Mac) confine each harness. The harness has no network other than the egress. `--unconfined` removes the sandbox and keeps the egress, for a platform without a sandbox backend | It runs where the harness runs, one for each session. Thus it scales with the people and agents that work, not with the stack | The `amem` binary | | Egress | Ours. It is the only network of the confined harness. It serves the MCP of the memory server and forwards model calls. It checks the ceiling for each call. It also injects credentials, applies spend limits, meters, and records decision contexts. It checks a tunnelled call (Copilot, and some subscription logins) when the tunnel opens, and does not meter it. It also carries the calls of the launcher to hosted tool servers | One egress beside each confined session. It forwards to LiteLLM for models, and to tool servers for hosted taps. A loopback relay and a unix socket connect it to the harness | Part of `amem`. Under WSL, it has its own container, which also runs the memory server | | Recall deputy, librarians, auditors | Agents on Gofannon, with the cheap tier through llmao | The platform already runs this way. Librarians are a consumer group on the log, as any other consumer group is | The existing Gofannon and LiteLLM containers | | Taps | MCP servers, one for each tracker or list, each one with a ceiling at registration; as WASI components that the memory server hosts (section 4) | Contributed taps run in a sandbox, with exactly the network and filesystem that the grant allows | Containers, or components that the memory server loads | diff --git a/evals/demo.toml b/evals/demo.toml index ca09e0a0..269fdb10 100644 --- a/evals/demo.toml +++ b/evals/demo.toml @@ -6,7 +6,7 @@ # scripts/eval.py evals/demo.toml --root ~/memory-demo2 --harness claude --auth subscription # scripts/eval.py evals/demo.toml --root ~/memory-demo2 --harness codex --auth gateway --model qwen3.8-27b # -# Seed the record first: scripts/demo.sh ~/memory-demo2 ... +# Seed the record first: make demo-ready DEMO_ROOT=~/memory-demo2 # Put in front of every prompt, under every condition alike. A harness given a memory # server does not always think to ask it; a task run says so in its prompt, and so does @@ -28,9 +28,13 @@ expect = "S2" [[task]] id = "asf-pressures" prompt = "In the 'Seeding a Foundation' notes, how many pressures does the part 'What the Foundation taught the design' list? Give the number." -expect = "\\b(six|6)\\b" +# the part cites "paper §6" and another part is "6. Trust and review", so a bare "6" is +# not enough: the number leads the answer, or it counts the pressures +expect = "^\\W*(six|6)\\b|\\b(six|6) (pressures|rows)\\b" [[task]] id = "committers-gate" prompt = "In the 'Seeding a Foundation' notes on trust and review, who is the promotion gate? One sentence." -expect = "committer" +# the gate is named people that domain policy authorises, under PMC oversight; the case +# study gives committers sampled review, so "committers" is no longer the answer +expect = "(named people|authori[sz]ed by (the )?domain policy|PMC)" diff --git a/golden/docs.toml b/golden/docs.toml index d5c42cb5..67346cf6 100644 --- a/golden/docs.toml +++ b/golden/docs.toml @@ -8,14 +8,14 @@ domain = "dom-demo" [[question]] -ask = "How does recall decide between pushing sections and letting the caller pull, and what does the paper say about caller-executed recall?" -answers = ["Serving bindings"] -keywords = "recall caller-executed sampling deputy" +ask = "What does the recall deputy do, and why can a low-privilege caller not launder reads through it?" +answers = ["The recall deputy"] +keywords = "recall deputy brief caller filter" [[question]] ask = "Who pays for recall at a foundation, and which principals get it server-side?" -answers = ["Serving, identity, and who pays"] -keywords = "recall committer public tier caller-executed" +answers = ["Serving and who pays"] +keywords = "recall committer server-side budget" [[question]] ask = "Why is the domain the partition key everywhere?" @@ -24,8 +24,8 @@ keywords = "domain partition key" [[question]] ask = "What label does an entry get when a session writes after reading restricted material?" -answers = ["Information flow"] -keywords = "write inherit label classification" +answers = ["Labels and flow"] +keywords = "write label rises restricted classification" [[question]] ask = "What happens when an entry must be erased, and what remains in its place?" @@ -33,9 +33,9 @@ answers = ["Erasure"] keywords = "erasure stub receipt" [[question]] -ask = "How is a rogue or defective librarian contained?" -answers = ["Containing the librarian"] -keywords = "librarian contain audit tombstone" +ask = "How is a wrong or compromised curator contained?" +answers = ["Containing the Curator"] +keywords = "curator contain auditors reversible diff" [[question]] ask = "Which three uses does WebAssembly have in the stack?" @@ -54,23 +54,23 @@ keywords = "policy stack federation" [[question]] ask = "What do trust tiers mean, and how does an entry move between them?" -answers = ["Tiers"] -keywords = "trust tier raw curated promotion" +answers = ["Promotion"] +keywords = "raw curated promotion tier" [[question]] -ask = "What is progressive disclosure in retrieval?" -answers = ["Progressive disclosure"] -keywords = "progressive disclosure summaries" +ask = "How does the read path start small, with summaries before bodies?" +answers = ["Disposable Agents: Search"] +keywords = "search read path summaries get" [[question]] -ask = "How does the system detect a vendor changing a model without announcing it?" -answers = ["Detecting unadvertised vendor change"] -keywords = "vendor change pin drift" +ask = "How does the system detect a vendor changing a model behind a stable name?" +answers = ["Vendor changes nobody announced"] +keywords = "vendor model probe detector drift" [[question]] ask = "What can a task's requirements field be abused for, and what guards it?" -answers = ["The task fabric"] -keywords = "task requirements inflation admission" +answers = ["A task's life"] +keywords = "task requirements guard admission" [[question]] ask = "Which sources become entries at the foundation, and how do they get there?" diff --git a/local/README.md b/local/README.md index 01d4941a..a9af0b41 100644 --- a/local/README.md +++ b/local/README.md @@ -1086,6 +1086,8 @@ it left. Thus the script measures all conditions in the same way. The results go condition, the input, cached and output tokens, and the seconds. We measured on 9 October over the demo record, with four questions for each condition. +Since then, the case study changed, and two tasks changed with it: `asf-pressures` and +`committers-gate`. The results below are for the tasks as they were on 9 October. Claude Code on a Max login: diff --git a/scripts/demo/10-moves.sh b/scripts/demo/10-moves.sh index 18a45c0f..9b00ea2e 100644 --- a/scripts/demo/10-moves.sh +++ b/scripts/demo/10-moves.sh @@ -13,7 +13,7 @@ a log show --domain dom-demo | j "f\" {len(d)} events: \" + ', '.join(f'{n} {k} say "The index, rebuilt" "The materializer reads the log and the record and builds the index from them, and replays the ledger's own events; it says what the record holds at another version than the log names." a log materialize --domain dom-demo | j "f\" {d['events']} events read, {d['live']} entries live in the index, {d['ledger_events']} ledger events replayed, {len(d['behind'])} behind the log\"" -say "Exported at a label" "A bundle is the entries at or below a label, Markdown and a manifest. You mark one note public; a public export carries that note alone, an internal one the whole domain." +say "Exported at a label" "A bundle is the entries at or below a label, Markdown and a manifest. You write one new note, marked public; a public export carries that note alone, an internal one the whole domain." a write --domain dom-demo --kind fact --title "The demo record is open to read" --classification public --body "This note is public: it may leave in a bundle at the public label." | j "f\" wrote {d['id']}, {d['classification']}\"" a export "$work/public" --domain dom-demo --max-label public 2>&1 | sed 's/ to .*, highest/, highest/; s/^/ /' a export "$work/internal" --domain dom-demo --max-label internal 2>&1 | sed 's/ to .*, highest/, highest/; s/^/ /' From 9ea23d292a42cbbff037b9ae66feeed7556ddfa7 Mon Sep 17 00:00:00 2001 From: Sander Striker Date: Fri, 30 Oct 2026 10:08:21 +0100 Subject: [PATCH 10/14] fix: make the two changed eval tasks reject a guess The second adversarial pass found that committers-gate accepted "the PMC", which a model guesses with no record. It also found that asf-pressures accepted a section number and refused "**six** pressures". The patterns now reject the guesses and accept the answers that the record gives. - The seed instruction says RESET=1, because an existing record keeps its old text. - The trust-tiers golden question also names the part that defines the tiers. - The demo narrative and PLAN.md say that their counts are for the old tasks and the old suite. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bhj4Ck4y5iwZUsjpgQx8A2 --- PLAN.md | 2 +- docs/demo-narrative.md | 2 ++ evals/demo.toml | 12 +++++++----- golden/docs.toml | 4 ++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/PLAN.md b/PLAN.md index fd57c983..a6363b6f 100644 --- a/PLAN.md +++ b/PLAN.md @@ -74,7 +74,7 @@ that run turns up. | 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 --reason `, 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//.erasures/.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`), fourteen 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. 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 | +| 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 --reason `, 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//.erasures/.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`), fourteen 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 moved to the paper's current headings. 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.` 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.]`: 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:`: 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:#` 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 ` copies a component under the root, a name checked before it becomes a path; `amem tap run --config [--every ]` 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 | diff --git a/docs/demo-narrative.md b/docs/demo-narrative.md index 002d91af..c4e6713e 100644 --- a/docs/demo-narrative.md +++ b/docs/demo-narrative.md @@ -281,6 +281,8 @@ model. **6.1 With and without.** Both runs use the same four questions, the same harness, the same model and the same sandbox. With this memory: 4 of 4. With an empty memory: 0 of 4. +These counts are for the questions as they were on 9 October; two of them changed with +the case study since. Codex searches, finds nothing, and says so or gives a wrong answer. With memory, it finished in 71 seconds against 126. It wrote less than half as much: about 1,600 tokens against 3,600. It read more fresh input, about 19,000 tokens against 11,000. It read diff --git a/evals/demo.toml b/evals/demo.toml index 269fdb10..182a077f 100644 --- a/evals/demo.toml +++ b/evals/demo.toml @@ -6,7 +6,8 @@ # scripts/eval.py evals/demo.toml --root ~/memory-demo2 --harness claude --auth subscription # scripts/eval.py evals/demo.toml --root ~/memory-demo2 --harness codex --auth gateway --model qwen3.8-27b # -# Seed the record first: make demo-ready DEMO_ROOT=~/memory-demo2 +# Seed a fresh record first: make demo-ready RESET=1 DEMO_ROOT=$HOME/memory-demo2. An +# existing record keeps the text it was seeded with, and RESET=1 removes the whole root. # Put in front of every prompt, under every condition alike. A harness given a memory # server does not always think to ask it; a task run says so in its prompt, and so does @@ -29,12 +30,13 @@ expect = "S2" id = "asf-pressures" prompt = "In the 'Seeding a Foundation' notes, how many pressures does the part 'What the Foundation taught the design' list? Give the number." # the part cites "paper §6" and another part is "6. Trust and review", so a bare "6" is -# not enough: the number leads the answer, or it counts the pressures -expect = "^\\W*(six|6)\\b|\\b(six|6) (pressures|rows)\\b" +# not enough: the number stands on a line of its own, or it counts the pressures +expect = "(?m)^\\W*(six|6)\\W*$|\\b(six|6)\\b\\W*(\\w+\\W+)?pressures\\b" [[task]] id = "committers-gate" prompt = "In the 'Seeding a Foundation' notes on trust and review, who is the promotion gate? One sentence." # the gate is named people that domain policy authorises, under PMC oversight; the case -# study gives committers sampled review, so "committers" is no longer the answer -expect = "(named people|authori[sz]ed by (the )?domain policy|PMC)" +# study gives committers sampled review, so "committers" is no longer the answer, and +# "the PMC" is what a model guesses about Apache with no record at all +expect = "named (people|individuals|persons)|(people|persons|individuals|reviewers)\\b[^.]{0,40}\\b(domain('s)? )?policy[^.]{0,15}authori[sz]|(people|persons|individuals|reviewers)\\b[^.]{0,20}authori[sz]ed by (the )?domain('s)? policy|policy-authori[sz]ed (people|individuals|persons|reviewers)" diff --git a/golden/docs.toml b/golden/docs.toml index 67346cf6..2596d850 100644 --- a/golden/docs.toml +++ b/golden/docs.toml @@ -54,13 +54,13 @@ keywords = "policy stack federation" [[question]] ask = "What do trust tiers mean, and how does an entry move between them?" -answers = ["Promotion"] +answers = ["Promotion", "Four questions, four labels"] keywords = "raw curated promotion tier" [[question]] ask = "How does the read path start small, with summaries before bodies?" answers = ["Disposable Agents: Search"] -keywords = "search read path summaries get" +keywords = "search read path summary get" [[question]] ask = "How does the system detect a vendor changing a model behind a stable name?" From 77da9f6d3322a4c260258b0f63170ed8c180663a Mon Sep 17 00:00:00 2001 From: Sander Striker Date: Fri, 30 Oct 2026 10:20:31 +0100 Subject: [PATCH 11/14] fix: accept the plain right answers in the two eval tasks The third adversarial pass found that the patterns refused right answers, such as "It lists six." and "A named person authorised by domain policy". - asf-pressures accepts six or 6, except as the two section numbers that the record cites. - committers-gate accepts the singular and Markdown emphasis. It requires "domain policy", so a guess about another policy does not pass. - The trust-tiers golden question is now two questions, one for each answer. - The seed command is on a line of its own. - The demo narrative names the date of its run, 12 October. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bhj4Ck4y5iwZUsjpgQx8A2 --- PLAN.md | 2 +- docs/demo-narrative.md | 2 +- evals/demo.toml | 15 +++++++++------ golden/docs.toml | 9 +++++++-- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/PLAN.md b/PLAN.md index a6363b6f..3613637a 100644 --- a/PLAN.md +++ b/PLAN.md @@ -74,7 +74,7 @@ that run turns up. | 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 --reason `, 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//.erasures/.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`), fourteen 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 moved to the paper's current headings. 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 | +| 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 --reason `, 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//.erasures/.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`), fourteen 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. 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.` 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.]`: 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:`: 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:#` 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 ` copies a component under the root, a name checked before it becomes a path; `amem tap run --config [--every ]` 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 | diff --git a/docs/demo-narrative.md b/docs/demo-narrative.md index c4e6713e..b853867b 100644 --- a/docs/demo-narrative.md +++ b/docs/demo-narrative.md @@ -281,7 +281,7 @@ model. **6.1 With and without.** Both runs use the same four questions, the same harness, the same model and the same sandbox. With this memory: 4 of 4. With an empty memory: 0 of 4. -These counts are for the questions as they were on 9 October; two of them changed with +These counts are for the questions as they were on 12 October; two of them changed with the case study since. Codex searches, finds nothing, and says so or gives a wrong answer. With memory, it finished in 71 seconds against 126. It wrote less than half as much: about 1,600 tokens diff --git a/evals/demo.toml b/evals/demo.toml index 182a077f..189d109c 100644 --- a/evals/demo.toml +++ b/evals/demo.toml @@ -6,8 +6,10 @@ # scripts/eval.py evals/demo.toml --root ~/memory-demo2 --harness claude --auth subscription # scripts/eval.py evals/demo.toml --root ~/memory-demo2 --harness codex --auth gateway --model qwen3.8-27b # -# Seed a fresh record first: make demo-ready RESET=1 DEMO_ROOT=$HOME/memory-demo2. An -# existing record keeps the text it was seeded with, and RESET=1 removes the whole root. +# Seed a fresh record first. An existing record keeps the text it was seeded with, and +# RESET=1 removes the whole root: +# +# make demo-ready RESET=1 DEMO_ROOT=$HOME/memory-demo2 # Put in front of every prompt, under every condition alike. A harness given a memory # server does not always think to ask it; a task run says so in its prompt, and so does @@ -29,9 +31,10 @@ expect = "S2" [[task]] id = "asf-pressures" prompt = "In the 'Seeding a Foundation' notes, how many pressures does the part 'What the Foundation taught the design' list? Give the number." -# the part cites "paper §6" and another part is "6. Trust and review", so a bare "6" is -# not enough: the number stands on a line of its own, or it counts the pressures -expect = "(?m)^\\W*(six|6)\\W*$|\\b(six|6)\\b\\W*(\\w+\\W+)?pressures\\b" +# the part cites "paper §6" and another part is "6. Trust and review", so a six or 6 +# counts unless it is one of those two section numbers. The table in part 6 also has six +# rows, so a model that counts the wrong table passes too; no pattern can tell +expect = "(? Date: Sat, 31 Oct 2026 01:32:39 +0100 Subject: [PATCH 12/14] fix: answer the fourth review pass on the open facts The 6.1 caveat moves to the end of its paragraph. The committers-gate pattern accepts "policy" without "domain" and a curly apostrophe. The PLAN caveat says that the corpus is now nine files. The gateway diagnostic in wslc.rs names --auth gateway and --server-auth gateway. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bhj4Ck4y5iwZUsjpgQx8A2 --- PLAN.md | 2 +- crates/memory/src/wslc.rs | 2 +- docs/demo-narrative.md | 5 ++--- evals/demo.toml | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/PLAN.md b/PLAN.md index 3613637a..735c1184 100644 --- a/PLAN.md +++ b/PLAN.md @@ -74,7 +74,7 @@ that run turns up. | 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 --reason `, 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//.erasures/.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`), fourteen 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. 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 | +| 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 --reason `, 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//.erasures/.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`), fourteen 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.` 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.]`: 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:`: 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:#` 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 ` copies a component under the root, a name checked before it becomes a path; `amem tap run --config [--every ]` 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 | diff --git a/crates/memory/src/wslc.rs b/crates/memory/src/wslc.rs index b2207d33..e2b5df0e 100644 --- a/crates/memory/src/wslc.rs +++ b/crates/memory/src/wslc.rs @@ -118,7 +118,7 @@ pub(crate) fn via_for(l: &Launch, windows: bool) -> Result { } else if l.interactive { Some("the C API gives a process no terminal, so an interactive launch goes through the command line") } else if wants_gateway(l) { - Some("the gateway inside the VM needs a network of its own, which the C API cannot make, so a gateway route goes through the command line") + Some("the gateway inside the VM needs a network of its own, which the C API cannot make, so --auth gateway or --server-auth gateway goes through the command line") } else { None }; diff --git a/docs/demo-narrative.md b/docs/demo-narrative.md index b853867b..0c6cba83 100644 --- a/docs/demo-narrative.md +++ b/docs/demo-narrative.md @@ -281,8 +281,6 @@ model. **6.1 With and without.** Both runs use the same four questions, the same harness, the same model and the same sandbox. With this memory: 4 of 4. With an empty memory: 0 of 4. -These counts are for the questions as they were on 12 October; two of them changed with -the case study since. Codex searches, finds nothing, and says so or gives a wrong answer. With memory, it finished in 71 seconds against 126. It wrote less than half as much: about 1,600 tokens against 3,600. It read more fresh input, about 19,000 tokens against 11,000. It read @@ -290,7 +288,8 @@ less cached input, the `c` figures: about 147,000 against 256,000. Cached tokens input that the provider has seen before in the session, and the provider charges less for them. These are amounts, not cost. The provider's prices decide whether the memory run was cheaper. The demo also measures the host's side: 7.4 seconds of CPU against -10.3. In both runs, the same harness had a peak of about 166 MiB. +10.3. In both runs, the same harness had a peak of about 166 MiB. These counts are for the +questions as they were on 12 October; two of them changed with the case study since. ## Act 7 · The platform diff --git a/evals/demo.toml b/evals/demo.toml index 189d109c..21b8e7ae 100644 --- a/evals/demo.toml +++ b/evals/demo.toml @@ -42,4 +42,4 @@ prompt = "In the 'Seeding a Foundation' notes on trust and review, who is the pr # the gate is named people that domain policy authorises, under PMC oversight; the case # study gives committers sampled review, so "committers" is no longer the answer, and # "the PMC" is what a model guesses about Apache with no record at all -expect = "named[*_ ]+(people|persons?|individuals?|reviewers?|maintainers|humans)[^.]{0,40}\\bdomain('s)?[*_]* policy|(people|persons?|individuals?|reviewers?|maintainers|humans)\\b[^.]{0,40}\\bdomain('s)?[*_]* policy[^.]{0,15}authori[sz]|(people|persons?|individuals?|reviewers?|maintainers|humans)\\b[^.]{0,30}authori[sz]ed by [*_]*(the )?domain('s)?[*_]* policy|policy-authori[sz]ed (people|persons?|individuals?|reviewers?|maintainers|humans)" +expect = "named[*_ ]+(people|persons?|individuals?|reviewers?|maintainers|humans)[^.]{0,40}\\b(domain(['’]s)?[*_]* )?policy|(people|persons?|individuals?|reviewers?|maintainers|humans)\\b[^.]{0,40}\\bdomain(['’]s)?[*_]* policy[^.]{0,15}authori[sz]|(people|persons?|individuals?|reviewers?|maintainers|humans)\\b[^.]{0,30}authori[sz]ed by [*_]*(the )?domain(['’]s)?[*_]* policy|policy-authori[sz]ed (people|persons?|individuals?|reviewers?|maintainers|humans)" From 195412388973053805801d99245d9ce3fdc32da8 Mon Sep 17 00:00:00 2001 From: Sander Striker Date: Sat, 31 Oct 2026 01:59:37 +0100 Subject: [PATCH 13/14] fix: answer the fifth review pass on the open facts The committers-gate pattern requires the domain's policy, or a policy that authorises the people. It accepts "human", "humans", "approver" and "approvers". The asf-pressures pattern rejects a 6 in a link anchor, at the end of a range, or in a list of parts. The trust-tiers question accepts the two entries that define the tiers. The init example in CONTRIBUTING.md uses --root. AGENTS.md says what to read in the output of golden and act 6, because neither fails on a miss. Act 8.1 and the M11 row in PLAN.md say that their questions changed since they ran. The golden row in PLAN.md says fifteen questions. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bhj4Ck4y5iwZUsjpgQx8A2 --- AGENTS.md | 4 +++- CONTRIBUTING.md | 2 +- PLAN.md | 4 ++-- docs/demo-narrative.md | 3 ++- evals/demo.toml | 13 ++++++++----- golden/docs.toml | 2 +- 6 files changed, 17 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 71e3b1d5..2402bed2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,7 +50,9 @@ before you override the instruction. `examples/decisions/`, seed a new record with `make demo-ready RESET=1`. Then run `make golden` and act 6 (`make demo-all HARNESS=codex ACTS=6`). Act 6 needs a key for llm.apache.org, in `local/.virtual-key` or `LLMAO_KEY`. Without a key, the act - says "skipped" and checks nothing. After you change those two lists, run act 11 + says "skipped" and checks nothing. Neither command fails on a miss. Read what they + print: golden's "Answers naming no entry in this record" and "Missed" lists, and the + `passed` column of act 6's `memory` row. After you change those two lists, run act 11 (`make demo-all HARNESS=codex ACTS=11`). - **Show that a new assertion fails before you trust it.** Run it against the code as it was before your change. Put what it reported in the pull request. If a test only ever diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index db013f23..89ce8ed6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,7 +35,7 @@ make check # format, clippy, tests, guard, docs-only and conformance tests | `make conformance` | `scripts/conformance.py`: the seven M0 steps over MCP, against a new record that this build serves. `URL=` points it at a different server. `OPERATOR=` gives it the export, import and reindex of that server, for the last two steps | | `make test` | only `cargo test` | | `make fmt` | `cargo fmt`, which changes the files in place | -| `make run ARGS='...'` | the `amem` binary, for example `ARGS='init /tmp/m --domain dom-me'` | +| `make run ARGS='...'` | the `amem` binary, for example `ARGS='init --root /tmp/m --domain dom-me'` | | `make verify` | `check`, and also `cargo deny`, `cargo machete`, coverage against `COVERAGE_MIN`, and the browser builds of `dm-core`, `dm-policy` and `dm-reader`. CI runs this target | | `make e2e` | the browser tests in `e2e/`. Chromium in the Playwright image drives the pages. The tests put screenshots in `e2e/screenshots/`. It needs Podman or Docker, not Node. It is not part of `check` or `verify`. Run it for a change to the pages. See [e2e/README.md](e2e/README.md) | | `make reader` | builds again the glue and the wasm of the browser reader into `crates/web/static/reader/`. It needs `wasm-bindgen-cli` 0.2.129 (`cargo install wasm-bindgen-cli --version 0.2.129`). Commit the result | diff --git a/PLAN.md b/PLAN.md index 735c1184..c5a54678 100644 --- a/PLAN.md +++ b/PLAN.md @@ -74,7 +74,7 @@ that run turns up. | 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 --reason `, 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//.erasures/.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`), fourteen 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 | +| 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 --reason `, 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//.erasures/.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.` 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.]`: 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:`: 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:#` 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 ` copies a component under the root, a name checked before it becomes a path; `amem tap run --config [--every ]` 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 | @@ -93,7 +93,7 @@ that run turns up. | 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.] hosted = ""` 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 = ""` (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:` 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:` 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: 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 | +| 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; `make reader` builds it into `crates/web/static/reader/`, which is committed so the web crate embeds it like the vendored libraries. 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//