* chore(release): v0.9.29 with project-scope parity across surfaces Version trio + plugin manifests + supportedVersions + ExportData union bumped to 0.9.29; CHANGELOG entry covering everything since v0.9.28 with upgrade notes for the four visible behavior changes. Fixes the endpoint-count drift on main (130 registered routes vs docs saying 129 after #1132 landed in parallel with #1136). Project-scope parity: OpenCode plugin, Hermes plugin, Pi extension, and JSONL replay now resolve project the same way the hooks do (env override, git toplevel basename, cwd basename) instead of sending raw filesystem paths, closing #903 and #1135 and pre-empting the same bug in pi. The filesystem watcher accepts AGENTMEMORY_PROJECT_NAME with the old AGENTMEMORY_PROJECT kept as a deprecated alias, replay handles Windows-recorded paths, and OpenCode file enrichment matches the agent's lowercase tool names (the capitalized set never matched). Tests: opencode fallback expectations updated to basenames per the canonicalization, git-toplevel resolution covered with a fixture repo, new project-scope-parity suite for replay and fs-watcher. * fix(release): review findings, git-toplevel parity, doc counts - skills generator dedupes routes on method plus path, so the REST reference lists all 130 registered routes instead of hiding the second method on ten dual-method paths (header said 119) - fs-watcher trims AGENTMEMORY_PROJECT_NAME and the deprecated alias, treating whitespace as unset, and derives the git toplevel basename when watching a subdirectory - replay resolves the git toplevel basename when the recorded cwd still exists locally (memoized per cwd), keeping the basename fallback for historical or cross-platform paths; no env override here since a bulk import spans many projects - parity tests for replay git-root resolution, watcher git-root and trim behavior - stat-tests badge updated from 1428+ to 1550+ passing * fix(cli): refuse second-instance boot over a live daemon Closes the class behind issue 1140: agentmemory consolidate (or any unrecognized word) fell through the command table into the full server boot, registering a duplicate worker on the running engine; on iii 0.11.2 the second instance's shutdown tears down the daemon's HTTP trigger routing until a full engine restart. Unknown subcommands now error with the supported list, and main() probes livez on the resolved port and refuses to boot over a live daemon, so multi-instance setups on other ports are unaffected. Verified behaviorally against the built CLI: both paths refuse with exit 1. Also from review: the watcher stamps each event with its own root's project via a per-root map (an explicit config.project still overrides for every root), and replay only accepts a non-empty string cwd from parsed JSONL so malformed entries cannot reach the filesystem probe. * test(watcher): two-repository flush events scope to their own project * chore(release): bump packages/mcp, guard it, refresh CONTRIBUTING packages/mcp was still 0.9.28 after the release bump because nothing guarded it; a consistency test now pins it to package.json. CONTRIBUTING release list corrected to the files a bump actually touches (no tracked lockfile, the two extra plugin manifests, the export test derives from VERSION now), and the subsystems table gains src/cli, integrations/pi, and the generated-manifest note. * fix(export): refuse over-frame export instead of dropping the worker Closes the availability bug in issue 1142: GET /agentmemory/export assembles the full store and returns it through sdk.trigger, so a store whose serialized export passes the engine's 16 MiB WebSocket frame (tungstenite max_frame_size, not raisable under the 0.11.2 pin) dies on the worker->engine hop, drops the worker, and 404s every endpoint for ~1s. The session collections page on maxSessions/offset but ~18 others do not, so a large store hits this at any parameter combination. A shared frame-guard measures the serialized size before returning: mem::export returns a small oversized error instead of the giant object, and api::mesh-export returns 413 (same dead-end as #890). Either way the over-frame payload never crosses the boundary, so the daemon stays up and the failure is one clean request with a hint to narrow the range. Full pagination of the non-session collections is a follow-up. Layer 1 of the fix; verified with a synthetic oversized export returning the error object (tiny) rather than the payload. * ci: collapse to a single npm install to fix Node 24/26 CI The two-step install (npm install --package-lock-only then npm ci) failed only on the Node 24/26 matrix rows: their stricter npm rejects rolldown's optional platform bindings (@rolldown/binding-android-arm64) that a --package-lock-only pass does not fully enumerate. Lockfiles are gitignored, so npm ci re-validation buys no reproducibility here. A single lenient npm install resolves and installs in one pass. * fix(mesh): scope exported memories by project like actions api::mesh-export filtered actions by ?project but returned every project's memories. On a mesh instance federating one project to a peer, the peer pulled other projects' memories (cross-project leak), and those extras could push the payload past the 16 MiB transport frame into a 413 even when the requested project's own slice fit. Memories carry the same optional project field as actions, so filter both before the frame-size guard runs. Adds a regression test asserting a project-scoped export excludes other projects' memories and that an oversized memory in another project no longer 413s the scoped request. * chore(release): credit the Antigravity native hooks adapter in 0.9.29 notes * chore(release): sweep stale 0.9.28 refs for 0.9.29 Deploy Dockerfiles/compose/render pins, AGENTS.md stats header, opencode plugin manifest, website meta snapshot, test-count claims (1,428 -> 1,596) in README/AGENTS/stat SVGs, and the missing 0.9.29 CHANGELOG compare link. * chore(release): sync stat-tests badge to 1596+ and commit bridge exec bit * refactor: trim frame-guard comments and drop issue refs from code
Deploy agentmemory on fly.io
This template runs agentmemory on a single fly.io machine with a 1 GB
persistent volume mounted at /data. The HMAC secret is generated on
first boot and persisted to the volume — you capture it from the deploy
logs exactly once.
What you get
- A public HTTPS endpoint serving the agentmemory REST API on port 3111
- A 1 GB Fly Volume at
/datafor memories, BM25 index, and stream backlog auto_stop_machines = "stop"andmin_machines_running = 0— the machine sleeps when idle, so cost floor approaches $0 for low traffic- HTTP healthcheck at
/agentmemory/livezevery 30 s - The HMAC bearer secret is generated on first boot inside the
container and persisted to
/data/.hmac(chmod 600); the operator copies it from the deploy logs once.
One-time setup
Pick a unique Fly app name first — agentmemory itself is likely taken.
Every command below references $APP, so set it once and the rest of the
flow stays consistent:
# 1. Install flyctl: https://fly.io/docs/flyctl/install/
# 2. Pick your unique app name (and matching volume name):
export APP="agentmemory-$(whoami)" # or any other globally-unique name
export VOLUME="${APP//-/_}_data" # Fly volume names can't contain '-'
# 3. From this directory:
fly launch --copy-config --no-deploy --name "$APP"
# 4. Create the volume in the same region as the app:
fly volumes create "$VOLUME" --region iad --size 1
# 5. Deploy:
fly deploy --app "$APP"
If fly launch reports the name is taken, pick another value for $APP,
re-export, and re-run.
Capture the HMAC secret
Right after the first deploy succeeds:
fly logs --app "$APP" | grep -A1 AGENTMEMORY_SECRET=
You will see exactly one line of the form AGENTMEMORY_SECRET=<64 hex chars>.
Copy it into your client environment (~/.bashrc, Claude Desktop config,
the viewer unlock prompt, etc.). The secret is never printed again on
subsequent boots.
If the first-boot log line is no longer available, read the persisted secret from the mounted volume:
fly ssh console --app "$APP" -C "sh -lc 'cat /data/.hmac'"
Verify the deployment
curl "https://$APP.fly.dev/agentmemory/livez"
# {"status":"ok"}
For an authenticated call, your client must send Authorization: Bearer <secret>.
Viewer access (port 3113 stays internal)
The viewer port is intentionally not exposed publicly. Tunnel to it:
fly proxy 3113:3113 --app "$APP"
# then open http://localhost:3113
fly proxy opens an mTLS WireGuard channel to the machine, so the
viewer's bearer token still has to ride a loopback connection on your
laptop — the v0.9.12 plaintext-bearer guard stays satisfied.
The entrypoint sets AGENTMEMORY_VIEWER_HOST=:: only when it detects
Fly's runtime variables (FLY_APP_NAME / FLY_ALLOC_ID). That makes
the viewer listen on the machine's fly-local-6pn WireGuard interface
as well as loopback so fly proxy can reach it. The same branch
pre-seeds VIEWER_ALLOWED_HOSTS=localhost:3113,127.0.0.1:3113,[::1]:3113,
which are the Host headers fly proxy 3113:3113 actually emits on
your laptop.
When AGENTMEMORY_VIEWER_HOST is non-loopback the viewer enforces two
extra guards: it refuses to start unless VIEWER_ALLOWED_HOSTS is
explicitly set, and every request to /agentmemory/* must present
Authorization: Bearer $AGENTMEMORY_SECRET. Static HTML and the
favicon are still served unauthenticated. If a proxied viewer request
gets a 401, the browser UI prompts for AGENTMEMORY_SECRET and stores
it in session storage so subsequent viewer API calls include the bearer.
Use the value printed in the first-boot logs or read /data/.hmac
inside the machine.
Security warning. Setting
AGENTMEMORY_VIEWER_HOST=0.0.0.0or::turns the viewer into a network-reachable proxy that signs every upstream call withAGENTMEMORY_SECRET. Never enable that outside a network you trust (Fly's WireGuard mesh in this template), and never set it in a plaindocker run -p 3113:3113 …on a shared host — the entrypoint deliberately skips the override when Fly env vars are absent so a plain Docker pull stays loopback-only.
Rotate the HMAC secret
fly ssh console --app "$APP"
rm /data/.hmac
exit
fly machine restart <machine-id>
fly logs --app "$APP" | grep AGENTMEMORY_SECRET=
Update every client with the new secret. Old tokens stop working immediately.
Back up /data
fly ssh console --app "$APP" -C "tar czf - /data" > "$APP-$(date +%Y%m%d).tar.gz"
To restore on a fresh machine:
cat "$APP-YYYYMMDD.tar.gz" | fly ssh console --app "$APP" -C "tar xzf - -C /"
fly machine restart <machine-id>
Cost floor and egress
- Idle (machine stopped): the volume costs ~$0.15/GB/month. A 1 GB volume is roughly $0.15/month.
- Active (machine running on
shared-cpu-1xwith 512 MB): about $1.94/month if it ran 24/7; in practiceauto_stop_machineskeeps that well under $1. - Outbound bandwidth: 100 GB/month free on the Hobby plan, then $0.02/GB in North America / Europe.
See https://fly.io/docs/about/pricing/ for the up-to-date rate card.
Known caveats
- The volume lives in one region. To survive a region outage, create a
second volume in another region and update
primary_regionafter the failover, or take snapshots withfly volumes snapshots create. - The Dockerfile builds in the Fly Builder on every deploy — first build is ~30 seconds; cached layers shrink rebuilds to under 10 seconds. Image is ~114 MB.
- First deploy lands on a shared IPv4 + dedicated IPv6 by default
(free). If you need a dedicated IPv4 for legacy clients without SNI,
run
fly ips allocate-v4 --app "$APP"— costs $2/month. - Cold-start (from machine launch to passing
/agentmemory/livez) is ~9 seconds measured.grace_period = "30s"on the health check gives a 3x safety margin. - Bump
AGENTMEMORY_VERSIONorIII_VERSIONin the Dockerfile to upgrade.fly deploy --build-arg AGENTMEMORY_VERSION=<x>also works for a one-off without editing the file.