From e94d161409be0523e1cec4062eeb9270d7fe7b94 Mon Sep 17 00:00:00 2001 From: Rohit Ghumare Date: Tue, 12 May 2026 16:09:04 +0100 Subject: [PATCH] fix(deploy): distroless volume perms + viewer proxy + budget loop (#299, #301) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @flamerged reported three issues on a real production deployment: 1. (#301) v0.9.7's working-directory fix moved iii-config paths from ./data/... to /data/... so the named volume mount is actually reached. But iiidev/iii is distroless and runs as UID 65532, while `docker volume create` initializes the named volume mountpoint as root:root mode 755. Engine writes fail Permission denied (os error 13), the API silently buffers in RAM, every API call returns success, and state evaporates on every container restart — exactly what 0.9.7 set out to fix. Fix: ship a one-shot iii-init service in docker-compose.yml (busybox:1.36, ~4MB, exits in <100ms) that chowns /data to 65532:65532. iii-engine now has user: "65532:65532" and depends_on.iii-init.condition: service_completed_successfully. Verified live: pre-fix volume stayed 4.0K after API writes; post- fix volume grows to 44K with state_store.db/mem%3A*.bin files written through the named volume. 2. (#299) src/viewer/index.html ports detection hardcoded ':3113' as the fallback when window.location.port is empty (page served on 80/443 behind a reverse proxy). Every browser-side /agentmemory/* fetch went to :3113, which is typically loopback-only on the self-hosted shape — the dashboard rendered cleanly but every panel showed the empty "first run" state. Fix: when neither ?port=N nor window.location.port is set, use window.location.origin as the REST base and window.location.host for the WebSocket URL — same-origin path works for both REST and live updates. Explicit ?port=N / non-default window.location.port paths unchanged. 3. (bundled) mem::context budget loop used `break` on first oversized block. With #288's new pinned-slot injection sorting first via recency: Date.now(), one fat pinned slot could starve every smaller block downstream that would have fit. Switched to `continue` so smaller blocks still slip into remaining budget. Total tokens still bounded by tokenBudget; only composition under contention changes. Bumping 0.9.9 -> 0.9.10 across the 8 standard files (package.json, packages/mcp/package.json, plugin/.claude-plugin/plugin.json, src/version.ts, src/types.ts ExportData literal, src/functions/export-import.ts supportedVersions, the export round-trip test expectation, and CHANGELOG.md). 868 / 868 tests pass. Build clean. Volume + viewer fixes verified end-to-end live. --- CHANGELOG.md | 16 ++++++++++++++++ docker-compose.yml | 18 ++++++++++++++++++ package.json | 2 +- packages/mcp/package.json | 2 +- plugin/.claude-plugin/plugin.json | 2 +- src/functions/context.ts | 2 +- src/functions/export-import.ts | 2 +- src/types.ts | 2 +- src/version.ts | 2 +- src/viewer/index.html | 30 +++++++++++++++++++++++------- test/export-import.test.ts | 2 +- 11 files changed, 65 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 188abb1a..58bb6553 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] +## [0.9.10] — 2026-05-12 + +Three deployment-shape fixes reported live by [@flamerged](https://github.com/flamerged) ([#299](https://github.com/rohitg00/agentmemory/issues/299), [#301](https://github.com/rohitg00/agentmemory/issues/301)): the v0.9.7 docker-compose persistence fix was incomplete because the distroless engine runs as UID 65532 but `docker volume create` initializes the named volume mountpoint as `root:root mode 755`; the viewer's port-detection JS hardcoded `'3113'` so any reverse-proxy fronting on port 80/443 returned an empty dashboard; and the `mem::context` budget loop short-circuited the entire selection on the first oversized block — pinning a large slot could starve all other context blocks even when smaller ones would have fit. + +### Fixed + +- **`docker-compose.yml` now chowns the named volume to UID 65532 before the engine starts.** The `iiidev/iii` image is distroless and runs as UID 65532; docker initializes named volumes as `root:root mode 755`; the engine has no `sh` / `chown` to self-heal at startup, so writes to `/data/state_store.db` and `/data/stream_store` returned `Permission denied (os error 13)`. The engine silently buffered in RAM, the API kept reporting `success: true`, and state evaporated on every container restart — the exact symptom v0.9.7's working-directory fix set out to solve. The compose file now ships an `iii-init` one-shot service (`busybox:1.36`, ~4 MB, exits in <100 ms) that runs `chown -R 65532:65532 /data && chmod 755 /data` once at compose-up, plus a `user: "65532:65532"` directive on the `iii-engine` service and a `depends_on.iii-init.condition: service_completed_successfully` gate so the engine never starts before the volume is owner-correct. Existing deployments that already hit the bug should run `docker compose down && docker compose up -d` after upgrading — the init container will fix the volume in place on the first run. (#301, closes [#301](https://github.com/rohitg00/agentmemory/issues/301) — thanks [@flamerged](https://github.com/flamerged) for the precise UID + mountpoint trace and the chown workaround that confirmed the fix shape) + +- **Viewer dashboard now works behind any reverse proxy on standard ports (80 / 443).** `src/viewer/index.html`'s port-detection JS resolved the REST base URL through `params.get('port') || window.location.port || '3113'` — when the page was served on port 80 or 443, `window.location.port` was the empty string and the fallback hardcoded `':3113'`, so every browser-side `/agentmemory/*` fetch missed the proxied origin and went to `:3113` (typically loopback-only on these deployments, so unreachable from outside). The viewer rendered cleanly but every panel showed the empty "first run" state — even though `curl /agentmemory/sessions` (no explicit port) returned correct data. The fix uses `window.location.origin` as the REST base when neither `?port=` nor `window.location.port` is set, and constructs the WebSocket URL from `window.location.host` (with whatever port the page was served on) so the same-origin path works for both REST and live updates. Behaviour with an explicit `?port=N` or non-default `window.location.port` is unchanged. (#299, closes [#299](https://github.com/rohitg00/agentmemory/issues/299) — thanks [@flamerged](https://github.com/flamerged) for the deployment context + the lines-927–930 trace) + +- **`mem::context` budget loop no longer bails the entire selection on the first oversized block.** The selection loop in `src/functions/context.ts` used `break` when `usedTokens + block.tokens > budget`, so a single oversized block at the top of the sorted list — most commonly a fat pinned slot under #288's new injection path — cut off every smaller block that would have fit. Switched to `continue`, so smaller blocks downstream of an oversized one can still slip into the remaining budget. Net effect: pinned-slot priority semantic is preserved (pinned blocks still sort first via `recency: Date.now()`), but the worst case no longer starves the entire context. Total tokens still bounded by `tokenBudget` (default 2000); only the composition under contention changes. + +### Changed + +- `@agentmemory/mcp` package version bumped from 0.9.9 → 0.9.10 to lockstep with the main package. + ## [0.9.9] — 2026-05-11 Two field-reported regressions closed: pinned memory slots never reached SessionStart context (the `renderPinnedContext` and `listPinnedSlots` helpers shipped in v0.7 had no callers), and the MiniMax compression provider read its base URL straight off `process.env`, missing `~/.agentmemory/.env` values that the rest of agentmemory loads through the shared merged-env path. diff --git a/docker-compose.yml b/docker-compose.yml index bed761ea..6b117652 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,18 @@ services: + # One-shot init container: docker creates named volumes root:root mode + # 755, but the iii-engine image is distroless and runs as UID 65532 + # with no `chown` of its own. Without this, /data is unwritable, the + # engine silently buffers in RAM, and state evaporates on every + # restart — the exact symptom v0.9.7's working-directory fix set out + # to solve. Runs once at compose-up and exits. + iii-init: + image: busybox:1.36 + user: "0:0" + volumes: + - iii-data:/data + entrypoint: ["sh", "-c", "chown -R 65532:65532 /data && chmod 755 /data"] + restart: "no" + iii-engine: # Pinned to v0.11.2 — the last engine that runs agentmemory's current # worker model cleanly. v0.11.6 introduces a new sandbox-everything- @@ -10,6 +24,10 @@ services: # Override per-shell or via .env file: # AGENTMEMORY_III_VERSION=0.11.7 docker compose up image: iiidev/iii:${AGENTMEMORY_III_VERSION:-0.11.2} + user: "65532:65532" + depends_on: + iii-init: + condition: service_completed_successfully ports: - "127.0.0.1:49134:49134" - "127.0.0.1:3111:3111" diff --git a/package.json b/package.json index 3f3a90bf..4c3047d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agentmemory/agentmemory", - "version": "0.9.9", + "version": "0.9.10", "description": "Persistent memory for AI coding agents, powered by iii-engine's three primitives", "type": "module", "main": "dist/index.mjs", diff --git a/packages/mcp/package.json b/packages/mcp/package.json index bde9d083..e7a91f26 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@agentmemory/mcp", - "version": "0.9.9", + "version": "0.9.10", "description": "Standalone MCP server for agentmemory — thin shim that re-exposes @agentmemory/agentmemory's MCP entrypoint", "type": "module", "bin": { diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 45cb2a13..d059828e 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agentmemory", - "version": "0.9.9", + "version": "0.9.10", "description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 12 hooks, 51 MCP tools, 4 skills, real-time viewer.", "author": { "name": "Rohit Ghumare", diff --git a/src/functions/context.ts b/src/functions/context.ts index 8a25f9b8..319de1ee 100644 --- a/src/functions/context.ts +++ b/src/functions/context.ts @@ -168,7 +168,7 @@ export function registerContextFunction( usedTokens += estimateTokens(header) + estimateTokens(footer); for (const block of blocks) { - if (usedTokens + block.tokens > budget) break; + if (usedTokens + block.tokens > budget) continue; selected.push(block.content); usedTokens += block.tokens; if (block.sourceIds && block.sourceIds.length > 0) { diff --git a/src/functions/export-import.ts b/src/functions/export-import.ts index dd2eccad..d2d07331 100644 --- a/src/functions/export-import.ts +++ b/src/functions/export-import.ts @@ -176,7 +176,7 @@ export function registerExportImportFunction(sdk: ISdk, kv: StateKV): void { const strategy = data.strategy || "merge"; const importData = data.exportData; - const supportedVersions = new Set(["0.3.0", "0.4.0", "0.5.0", "0.6.0", "0.6.1", "0.7.0", "0.7.2", "0.7.3", "0.7.4", "0.7.5", "0.7.6", "0.7.7", "0.7.9", "0.8.0", "0.8.1", "0.8.2", "0.8.3", "0.8.4", "0.8.5", "0.8.6", "0.8.7", "0.8.8", "0.8.9", "0.8.10", "0.8.11", "0.8.12", "0.8.13", "0.9.0", "0.9.1", "0.9.2", "0.9.3", "0.9.4", "0.9.5", "0.9.6", "0.9.7", "0.9.8", "0.9.9"]); + const supportedVersions = new Set(["0.3.0", "0.4.0", "0.5.0", "0.6.0", "0.6.1", "0.7.0", "0.7.2", "0.7.3", "0.7.4", "0.7.5", "0.7.6", "0.7.7", "0.7.9", "0.8.0", "0.8.1", "0.8.2", "0.8.3", "0.8.4", "0.8.5", "0.8.6", "0.8.7", "0.8.8", "0.8.9", "0.8.10", "0.8.11", "0.8.12", "0.8.13", "0.9.0", "0.9.1", "0.9.2", "0.9.3", "0.9.4", "0.9.5", "0.9.6", "0.9.7", "0.9.8", "0.9.9", "0.9.10"]); if (!supportedVersions.has(importData.version)) { return { success: false, diff --git a/src/types.ts b/src/types.ts index 5285281b..e3ad63a1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -279,7 +279,7 @@ export interface ExportPagination { } export interface ExportData { - version: "0.3.0" | "0.4.0" | "0.5.0" | "0.6.0" | "0.6.1" | "0.7.0" | "0.7.2" | "0.7.3" | "0.7.4" | "0.7.5" | "0.7.6" | "0.7.7" | "0.7.9" | "0.8.0" | "0.8.1" | "0.8.2" | "0.8.3" | "0.8.4" | "0.8.5" | "0.8.6" | "0.8.7" | "0.8.8" | "0.8.9" | "0.8.10" | "0.8.11" | "0.8.12" | "0.8.13" | "0.9.0" | "0.9.1" | "0.9.2" | "0.9.3" | "0.9.4" | "0.9.5" | "0.9.6" | "0.9.7" | "0.9.8" | "0.9.9"; + version: "0.3.0" | "0.4.0" | "0.5.0" | "0.6.0" | "0.6.1" | "0.7.0" | "0.7.2" | "0.7.3" | "0.7.4" | "0.7.5" | "0.7.6" | "0.7.7" | "0.7.9" | "0.8.0" | "0.8.1" | "0.8.2" | "0.8.3" | "0.8.4" | "0.8.5" | "0.8.6" | "0.8.7" | "0.8.8" | "0.8.9" | "0.8.10" | "0.8.11" | "0.8.12" | "0.8.13" | "0.9.0" | "0.9.1" | "0.9.2" | "0.9.3" | "0.9.4" | "0.9.5" | "0.9.6" | "0.9.7" | "0.9.8" | "0.9.9" | "0.9.10"; exportedAt: string; sessions: Session[]; observations: Record; diff --git a/src/version.ts b/src/version.ts index 14d8a926..d0e5b4af 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = "0.9.9"; +export const VERSION = "0.9.10"; diff --git a/src/viewer/index.html b/src/viewer/index.html index 5de2f87f..17fec4ea 100644 --- a/src/viewer/index.html +++ b/src/viewer/index.html @@ -924,14 +924,30 @@

agentmemory