Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

## [Unreleased]

## [0.9.7] — 2026-05-11
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add the missing [0.9.7] reference link target.

Line 9 uses reference-link syntax ([0.9.7]), but the corresponding definition is missing in the link table, so this entry won’t resolve to a compare URL.

📎 Proposed doc fix
+[0.9.7]: https://github.com/rohitg00/agentmemory/compare/v0.9.6...v0.9.7
 [0.9.6]: https://github.com/rohitg00/agentmemory/compare/v0.9.5...v0.9.6
 [0.9.5]: https://github.com/rohitg00/agentmemory/compare/v0.9.4...v0.9.5
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` at line 9, The changelog entry "## [0.9.7] — 2026-05-11"
references a missing link target; add a matching reference-link definition for
"[0.9.7]" in the link table at the bottom of CHANGELOG.md using the same format
as the other releases (i.e., a `[0.9.7]: <compare-URL-or-tag>` line that points
to the repo's compare or tag URL for 0.9.7 so the header resolves correctly).


Three small follow-ups to v0.9.6 reported live by [@jcalfee](https://github.com/jcalfee) on [#234](https://github.com/rohitg00/agentmemory/issues/234): the `@agentmemory/mcp` shim silently degraded to a 7-tool local fallback for sandboxed MCP clients (Flatpak VS Codeium / Roo Code) because its 500 ms `livez` probe failed inside the sandbox network namespace and the catch swallowed the error; the Docker compose stack persisted state to an ephemeral container path instead of the named volume; and a `which iii` lookup leaked a "no iii in $PATH" line to the CLI banner.

### Fixed

- **`@agentmemory/mcp` standalone shim now surfaces probe failures and ships an escape hatch for sandboxed clients.** The `livez` probe in `src/mcp/rest-proxy.ts` used a 500 ms timeout and silently swallowed every failure; sandboxed clients (Flatpak VS Codeium spawning the shim from inside its bubblewrap network namespace, Snap-packaged editors, restrictive container runtimes) hit a connection failure on the probe, fell back to the 7-tool `IMPLEMENTED_TOOLS` set, and had no log line explaining why. The probe now writes the URL, HTTP status (or thrown reason), and the active timeout to `stderr` on every failure, the default timeout is raised to 2000 ms, `AGENTMEMORY_PROBE_TIMEOUT_MS` overrides it for slow loopbacks, and `AGENTMEMORY_FORCE_PROXY=1` skips the probe entirely and trusts `AGENTMEMORY_URL` — the right escape hatch when the shim is reachable to the server via a known route but can't see the host's `localhost`. (closes [#234](https://github.com/rohitg00/agentmemory/issues/234) follow-up, thanks [@jcalfee](https://github.com/jcalfee) for the host-vs-Flatpak repro)

- **Docker compose stack no longer loses state on `docker compose down`.** `iii-config.docker.yaml` configured `iii-state` and `iii-stream` with relative `file_path: ./data/...`, which the engine resolves against its container `WORKDIR=/home/nonroot` — not the `/data` mount where the named `iii-data` volume lives. State and stream stores were written to the ephemeral container layer and discarded on every container restart, so memories, BM25 index, and stream backlog vanished. Both paths are now absolute (`/data/state_store.db` and `/data/stream_store`), routing writes through the named volume as the compose file always intended. Existing users need a one-time `docker compose down -v` to clear the old empty volume layout before the upgrade.

- **CLI banner no longer leaks `which: no iii in $PATH` when iii isn't installed.** `whichBinary()` in `src/cli.ts` called `execFileSync("which", ["iii"])` with default stdio, which inherits the child's `stderr` to the parent process — and GNU `which` writes "no iii in (...)" to `stderr` (with exit 1) on miss. The catch swallowed the throw but the stderr line had already drained into the user's terminal between the `agentmemory` banner and the "iii-engine ready" line. `stdio: ["ignore", "pipe", "pipe"]` now captures both streams. Pure cosmetic, no behavior change.

- **Docker compose stack now caps engine container log size at 30 MB total.** [@satabd](https://github.com/satabd) reported the `iiidev/iii:0.11.2` engine container filling a host disk with a 129 GB `<container-id>-json.log` when the engine fell into a crash/restart spam loop ([#278](https://github.com/rohitg00/agentmemory/issues/278)). The compose service now sets `logging.driver: json-file` with `max-size: 10m` and `max-file: 3`, so unbounded engine stdout/stderr can no longer eat the host's disk. The upstream engine spam itself is filed against `iiidev/iii` — this is the compose-side guardrail.

### Changed

- `@agentmemory/mcp` package version bumped from 0.9.6 → 0.9.7 to lockstep with the main package.

## [0.9.6] — 2026-05-10

Three reliability fixes that close field-reported regressions in v0.9.5: search/recall returns saved memories again, the standalone MCP shim no longer caps non-Claude clients at a 7-tool subset, and the Claude Code session/subagent hooks no longer block agent startup for up to five seconds against a slow or unreachable REST server.
Expand Down
5 changes: 5 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ services:
- iii-data:/data
- ./iii-config.docker.yaml:/app/config.yaml:ro
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"

volumes:
iii-data:
4 changes: 2 additions & 2 deletions iii-config.docker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ workers:
name: kv
config:
store_method: file_based
file_path: ./data/state_store.db
file_path: /data/state_store.db
- name: iii-queue
config:
adapter:
Expand All @@ -34,7 +34,7 @@ workers:
name: kv
config:
store_method: file_based
file_path: ./data/stream_store
file_path: /data/stream_store
- name: iii-observability
config:
enabled: true
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agentmemory/agentmemory",
"version": "0.9.6",
"version": "0.9.7",
"description": "Persistent memory for AI coding agents, powered by iii-engine's three primitives",
"type": "module",
"main": "dist/index.mjs",
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agentmemory/mcp",
"version": "0.9.6",
"version": "0.9.7",
"description": "Standalone MCP server for agentmemory — thin shim that re-exposes @agentmemory/agentmemory's MCP entrypoint",
"type": "module",
"bin": {
Expand Down
2 changes: 1 addition & 1 deletion plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agentmemory",
"version": "0.9.6",
"version": "0.9.7",
"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",
Expand Down
5 changes: 4 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,10 @@ function findIiiConfig(): string {
function whichBinary(name: string): string | null {
const cmd = IS_WINDOWS ? "where" : "which";
try {
const out = execFileSync(cmd, [name], { encoding: "utf-8" });
const out = execFileSync(cmd, [name], {
encoding: "utf-8",
stdio: ["ignore", "pipe", "pipe"],
});
const first = out
.split(/\r?\n/)
.map((line) => line.trim())
Expand Down
2 changes: 1 addition & 1 deletion src/functions/export-import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
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"]);
if (!supportedVersions.has(importData.version)) {
return {
success: false,
Expand Down
35 changes: 31 additions & 4 deletions src/mcp/rest-proxy.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
const DEFAULT_URL = "http://localhost:3111";
const HEALTH_PROBE_TIMEOUT_MS = 500;
const DEFAULT_HEALTH_PROBE_TIMEOUT_MS = 2_000;
const CALL_TIMEOUT_MS = 15_000;
const LOCAL_MODE_TTL_MS = 30_000;

function probeTimeoutMs(): number {
const raw = process.env["AGENTMEMORY_PROBE_TIMEOUT_MS"];
if (!raw) return DEFAULT_HEALTH_PROBE_TIMEOUT_MS;
const n = Number(raw);
return Number.isFinite(n) && n > 0 ? Math.floor(n) : DEFAULT_HEALTH_PROBE_TIMEOUT_MS;
}

function forceProxy(): boolean {
const raw = process.env["AGENTMEMORY_FORCE_PROXY"];
return raw === "1" || raw === "true";
}

export interface ProxyHandle {
mode: "proxy";
baseUrl: string;
Expand All @@ -29,14 +41,23 @@ function authHeader(): Record<string, string> {
}

async function probe(url: string): Promise<boolean> {
const timeout = probeTimeoutMs();
try {
const res = await fetch(`${url}/agentmemory/livez`, {
method: "GET",
headers: authHeader(),
signal: AbortSignal.timeout(HEALTH_PROBE_TIMEOUT_MS),
signal: AbortSignal.timeout(timeout),
});
if (!res.ok) {
process.stderr.write(
`[@agentmemory/mcp] livez probe ${url}/agentmemory/livez -> ${res.status} ${res.statusText}; falling back to local InMemoryKV (set AGENTMEMORY_FORCE_PROXY=1 to skip the probe)\n`,
);
}
return res.ok;
} catch {
} catch (err) {
process.stderr.write(
`[@agentmemory/mcp] livez probe ${url}/agentmemory/livez failed in ${timeout}ms: ${err instanceof Error ? err.message : String(err)}; falling back to local InMemoryKV (set AGENTMEMORY_FORCE_PROXY=1 to skip the probe, or raise AGENTMEMORY_PROBE_TIMEOUT_MS)\n`,
);
return false;
}
}
Expand All @@ -58,8 +79,14 @@ export async function resolveHandle(): Promise<Handle> {
}
if (probeInFlight) return probeInFlight;
const url = baseUrl();
const skipProbe = forceProxy();
probeInFlight = (async () => {
const up = await probe(url);
const up = skipProbe ? true : await probe(url);
if (skipProbe) {
process.stderr.write(
`[@agentmemory/mcp] AGENTMEMORY_FORCE_PROXY set; skipping livez probe and trusting ${url}\n`,
);
}
if (up) {
const handle: ProxyHandle = {
mode: "proxy",
Expand Down
38 changes: 31 additions & 7 deletions src/mcp/standalone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,30 +370,54 @@ const transport = createStdioTransport(async (method, params) => {
return {};

case "tools/list": {
// When a server is reachable, expose every tool it advertises (51
// when AGENTMEMORY_TOOLS=all on the server). Without this, the shim
// capped non-Claude clients at the local 7-tool set even with the
// server up (issue #234).
const debug = process.env["AGENTMEMORY_DEBUG"] === "1" || process.env["AGENTMEMORY_DEBUG"] === "true";
const handle = await resolveHandle();
announceMode(handle);
if (debug) {
process.stderr.write(
`[@agentmemory/mcp] tools/list: handle.mode=${handle.mode}${handle.mode === "proxy" ? ` baseUrl=${handle.baseUrl}` : ""}\n`,
);
}
if (handle.mode === "proxy") {
try {
const remote = (await handle.call("/agentmemory/mcp/tools", {
method: "GET",
})) as { tools?: unknown } | null;
if (debug) {
const shape = remote === null
? "null"
: typeof remote !== "object"
? typeof remote
: `keys=${Object.keys(remote as object).join(",")} toolsType=${Array.isArray((remote as { tools?: unknown }).tools) ? `array(len=${((remote as { tools: unknown[] }).tools).length})` : typeof (remote as { tools?: unknown }).tools}`;
process.stderr.write(
`[@agentmemory/mcp] tools/list: remote response shape: ${shape}\n`,
);
}
if (remote && Array.isArray(remote.tools)) {
if (debug) {
process.stderr.write(
`[@agentmemory/mcp] tools/list: returning ${remote.tools.length} tools from server\n`,
);
}
return { tools: remote.tools };
}
process.stderr.write(
`[@agentmemory/mcp] tools/list: server returned unexpected shape (no .tools array); falling back to local IMPLEMENTED_TOOLS list. Set AGENTMEMORY_DEBUG=1 to inspect response.\n`,
);
} catch (err) {
process.stderr.write(
`[@agentmemory/mcp] tools/list proxy failed: ${err instanceof Error ? err.message : String(err)}; falling back to local list\n`,
);
invalidateHandle();
}
}
return {
tools: getVisibleTools().filter((t) => IMPLEMENTED_TOOLS.has(t.name)),
};
const fallback = getVisibleTools().filter((t) => IMPLEMENTED_TOOLS.has(t.name));
if (debug) {
process.stderr.write(
`[@agentmemory/mcp] tools/list: returning ${fallback.length} local fallback tools (${fallback.map((t) => t.name).join(",")})\n`,
);
}
return { tools: fallback };
}

case "tools/call": {
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
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";
exportedAt: string;
sessions: Session[];
observations: Record<string, CompressedObservation[]>;
Expand Down
2 changes: 1 addition & 1 deletion src/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const VERSION = "0.9.6";
export const VERSION = "0.9.7";
2 changes: 1 addition & 1 deletion test/export-import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ describe("Export/Import Functions", () => {
it("export produces valid ExportData structure", async () => {
const result = (await sdk.trigger("mem::export", {})) as ExportData;

expect(result.version).toBe("0.9.6");
expect(result.version).toBe("0.9.7");
expect(result.exportedAt).toBeDefined();
expect(result.sessions.length).toBe(1);
expect(result.sessions[0].id).toBe("ses_1");
Expand Down
68 changes: 68 additions & 0 deletions test/mcp-standalone-proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,72 @@ describe("@agentmemory/mcp standalone — server proxy (issue #159)", () => {
);
expect(remembersCalled).toBe(false);
});

it("AGENTMEMORY_FORCE_PROXY=1 skips livez probe and trusts the server", async () => {
process.env["AGENTMEMORY_FORCE_PROXY"] = "1";
const calls: string[] = [];
installFetch((url, init) => {
calls.push(url);
if (url.endsWith("/agentmemory/livez")) {
throw new Error("probe should be skipped");
}
if (url.endsWith("/agentmemory/remember")) {
return new Response(JSON.stringify({ id: "m-1", action: "created" }), {
status: 200,
headers: { "content-type": "application/json" },
});
}
return new Response("not found", { status: 404 });
});
try {
await handleToolCall("memory_save", { content: "force-proxy" });
expect(calls.some((u) => u.endsWith("/agentmemory/livez"))).toBe(false);
expect(calls.some((u) => u.endsWith("/agentmemory/remember"))).toBe(true);
} finally {
delete process.env["AGENTMEMORY_FORCE_PROXY"];
}
});

it("logs probe failure to stderr so sandboxed clients can diagnose silently dropped tools", async () => {
installFetch((url) => {
if (url.endsWith("/agentmemory/livez")) {
throw new Error("ECONNREFUSED 127.0.0.1:3111");
}
return new Response("not found", { status: 404 });
});
const writes: string[] = [];
const origWrite = process.stderr.write.bind(process.stderr);
process.stderr.write = ((chunk: string | Uint8Array) => {
writes.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
return true;
}) as typeof process.stderr.write;
try {
const localKv = new InMemoryKV(undefined);
await handleToolCall("memory_save", { content: "diag" }, localKv);
} finally {
process.stderr.write = origWrite;
}
const joined = writes.join("");
expect(joined).toMatch(/livez probe .* failed/);
expect(joined).toMatch(/AGENTMEMORY_FORCE_PROXY/);
});

it("AGENTMEMORY_PROBE_TIMEOUT_MS overrides the default probe timeout", async () => {
process.env["AGENTMEMORY_PROBE_TIMEOUT_MS"] = "50";
let probeStarted = 0;
installFetch((url) => {
if (url.endsWith("/agentmemory/livez")) {
probeStarted++;
return new Response("ok", { status: 200 });
}
return new Response("not found", { status: 404 });
});
try {
const localKv = new InMemoryKV(undefined);
await handleToolCall("memory_save", { content: "timeout-knob" }, localKv);
expect(probeStarted).toBe(1);
} finally {
delete process.env["AGENTMEMORY_PROBE_TIMEOUT_MS"];
}
});
});
Loading