Files
Rohit Ghumare 9c82d2aa7d chore(release): v0.9.29 with project-scope parity across capture surfaces (#1141)
* 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
2026-08-09 13:22:25 +01:00
..

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 /data for memories, BM25 index, and stream backlog
  • auto_stop_machines = "stop" and min_machines_running = 0 — the machine sleeps when idle, so cost floor approaches $0 for low traffic
  • HTTP healthcheck at /agentmemory/livez every 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.0 or :: turns the viewer into a network-reachable proxy that signs every upstream call with AGENTMEMORY_SECRET. Never enable that outside a network you trust (Fly's WireGuard mesh in this template), and never set it in a plain docker 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-1x with 512 MB): about $1.94/month if it ran 24/7; in practice auto_stop_machines keeps 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_region after the failover, or take snapshots with fly 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_VERSION or III_VERSION in the Dockerfile to upgrade. fly deploy --build-arg AGENTMEMORY_VERSION=<x> also works for a one-off without editing the file.