Commit Graph

485 Commits

Author SHA1 Message Date
yinhewang ccf440902a fix(slots): wire pinned slot injection into mem::context (#288)
* fix(slots): wire pinned slot injection into mem::context

`renderPinnedContext` and `listPinnedSlots` were introduced in #182 and tested
in `test/slots.test.ts`, but no code path actually invokes them. `mem::context`
(which `/agentmemory/session/start` calls, and which the `session-start.mjs`
hook reads back to populate Claude Code's SessionStart context) reads profile
/ sessions / observations but never reads slots — so pinned content stays in
KV and never reaches the conversation.

#182's description hedged with "pinned slots **can be** injected into
SessionStart context" and described pinned slots as "**candidates for**
SessionStart injection". The helpers and tests for them landed; the wiring
did not.

Reflection has the same shape of problem: `mem::slot-reflect` writes to
`pending_items` / `session_patterns` / `project_context` on the Stop hook,
but the next session never reads those slots, so the reflect → next-session
loop is open.

This adds the missing call at the top of `mem::context`, gated by
`isSlotsEnabled()` so behaviour for `AGENTMEMORY_SLOTS=false` users is
unchanged. Slot content is pushed as a `ContextBlock` with
`recency = Date.now()` so pinned content ranks first in the budget-bounded
selection — "always current" semantics. The existing `INJECT_CONTEXT` gate
in `session-start.mjs` still decides whether the result actually reaches
Claude Code stdout, so default-off users are unaffected on both flags.

`test/context-slots.test.ts` covers:

- pinned global slot lands in returned context
- multiple pinned slots render in label-sorted order
- unpinned slots are excluded even when they have content
- empty pinned slots (the seeded defaults) are excluded
- project-scoped slot shadows global slot with the same label
- AGENTMEMORY_SLOTS=off path emits nothing

Signed-off-by: wyh0626 <44987669+wyh0626@users.noreply.github.com>

* fix(slots): parallelize slot and profile reads in mem::context

Per CodeRabbit feedback on #288: `listPinnedSlots` and the profile fetch read
independent KV scopes, so the project's `src/functions/**` guideline asks for
`Promise.all`. When `AGENTMEMORY_SLOTS=false` the slot side short-circuits to
`Promise.resolve([])`, so the existing flag gate is preserved and no extra
KV traffic is introduced for opt-out users.

Signed-off-by: wyh0626 <44987669+wyh0626@users.noreply.github.com>

---------

Signed-off-by: wyh0626 <44987669+wyh0626@users.noreply.github.com>
2026-05-11 19:23:53 +01:00
Rohit Ghumare 27c38b74e6 fix(providers/minimax): use shared env loader + bump default base URL (#285) (#289)
@rager306 reported v0.9.8 MiniMax compression failing with
"MiniMax API error 401: invalid api key" against a known-good key.
Direct curl to https://api.minimax.io/anthropic/v1/messages with the
same key succeeded; control curl to https://api.minimaxi.com/anthropic
reproduced the 401. The 401 came from the stale fallback host, not
the key.

Root cause: split-brain env source. src/config.ts (provider
detection, API key load) reads ~/.agentmemory/.env via the merged
getMergedEnv() loader. src/providers/minimax.ts read MINIMAX_BASE_URL
directly off process.env. Users who put MINIMAX_BASE_URL only in
~/.agentmemory/.env (e.g. systemd / launchd / .env-only deployments)
got their key loaded fine but the base URL fell through to the stale
api.minimaxi.com default.

MiniMax's current Anthropic-compatible endpoint per their docs is
api.minimax.io/anthropic/v1/messages
(https://platform.minimax.io/docs/api-reference/text-anthropic-api).

Fix:
- Switch MinimaxProvider.baseUrl resolution from
  process.env['MINIMAX_BASE_URL'] to getEnvVar('MINIMAX_BASE_URL'),
  matching how every other config knob in agentmemory is loaded.
- Bump the default from https://api.minimaxi.com/anthropic to
  https://api.minimax.io/anthropic.
- Update the inline JSDoc to mention ~/.agentmemory/.env as a source
  alongside process.env, so the env contract is discoverable from
  the file.
- Add test/minimax-provider.test.ts asserting the default is the
  current host and the env override is honored.

Verified locally: 862/862 tests pass on the branch. @rager306 has
already verified end-to-end that a local patch with the same change
fixed his /agentmemory/observe + compression flow.
2026-05-11 19:17:04 +01:00
Rohit Ghumare ec84ff8fd3 docs: merge guidance + per-host shapes for MCP install snippets (#282)
Real-world failure mode: users paste the README's universal mcpServers
JSON into an existing config file (e.g. ~/.cursor/mcp.json with
firecrawl/linear/drawio already wired) and end up with two top-level
objects, breaking the host's MCP loader with "Expected double-quoted
property name in JSON at position 386". Plus the README doesn't tell
them where each host actually reads its config from, doesn't include
the AGENTMEMORY_URL env block that the website snippet ships, and
quotes a stale "43 memory tools" count in the OpenClaw and Hermes
paste-prompts (the server has been at 51 since the v0.7 essential-set
expansion).

This commit fixes the install copy on README and website together so
the two don't drift again.

README:
  - Replace per-host table with a single canonical agentmemory block
    that includes the AGENTMEMORY_URL env (matches the website's
    UNIVERSAL_JSON), plus an explicit "merge into existing mcpServers,
    do not replace the file" line.
  - Per-host table now lists exact config file path per host (e.g.
    ~/.cursor/mcp.json, claude_desktop_config.json,
    ~/.codeium/windsurf/mcp_config.json, ~/.gemini/settings.json)
    and flags the non-mcpServers shapes (OpenCode uses mcp + array
    command; Codex uses TOML).
  - Adds Roo Code and Windsurf (previously absent), keeps Cline /
    Goose / Kilo Code / pi / Hermes / OpenClaw / Aider.
  - Adds sandboxed-client paragraph pointing AGENTMEMORY_FORCE_PROXY=1
    + AGENTMEMORY_URL=<LAN-IP> at the v0.9.7 escape hatch (closes the
    Flatpak / Snap repro from #234).
  - Stale "43 memory tools" -> "51 memory tools" in the OpenClaw and
    Hermes paste-prompt blocks.
  - OpenClaw paste-prompt JSON now ships the env block too.
  - The "Standalone MCP" section's universal JSON ships AGENTMEMORY_URL
    and lists Roo Code + Windsurf + Gemini CLI alongside Cursor /
    Claude Desktop / Cline.

Website (AgentInstall.tsx):
  - "WORKS FOR CLAUDE DESKTOP · CURSOR · CLINE · WINDSURF · GEMINI CLI
    · OPENCODE" -> "CLAUDE DESKTOP · CURSOR · CLINE · ROO CODE ·
    WINDSURF · GEMINI CLI — MERGE INTO EXISTING mcpServers". OpenCode
    does NOT use mcpServers (top-level `mcp` key, command-as-array,
    `environment` rather than `env`), so removing it from the
    universal label.
  - Adds an OPENCODE snippet with the correct `mcp` shape and
    `environment.AGENTMEMORY_URL`.
  - Adds a VS CODE (mcp.json) snippet — VS Code's .vscode/mcp.json
    uses `servers` (not `mcpServers`), `type: "stdio"`, and the
    standard `env`. The vscode:mcp/install deeplink already worked
    via its own flat config shape; this snippet covers the copy-JSON
    path for VS Code users editing mcp.json by hand.
  - "+ HERMES · OPENCLAW · MORE" toggle relabeled to "+ OPENCODE ·
    VS CODE · CODEX · HERMES · OPENCLAW" so the catalog is visible
    before opening it.

No new dependencies. Website typechecks and builds clean (Next 16
App Router, static export). Repo tests 859/859.
2026-05-11 16:18:41 +01:00
Rohit Ghumare 0f0bb5091d chore(release): v0.9.8 — local fallback tools/list returns 7 not 4 (#284)
Single-issue release: PR #283 fixed the local-mode tools/list path
in @agentmemory/mcp to expose all 7 IMPLEMENTED_TOOLS instead of the
4-tool intersection ESSENTIAL_TOOLS ∩ IMPLEMENTED_TOOLS that bug
manifested as in MCP clients (Cursor / Roo Code / others) when no
agentmemory server was reachable on localhost:3111.

Bumping 0.9.7 -> 0.9.8 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).
v0.9.8
2026-05-11 14:56:06 +01:00
Rohit Ghumare 0a6c399e23 fix(mcp): local fallback exposes all 7 IMPLEMENTED_TOOLS, not 4 (#234) (#283)
Reported live in Cursor: agentmemory MCP server shows exactly four
tools — memory_recall, memory_save, memory_sessions,
memory_smart_search — when no agentmemory server is running on
localhost:3111. Should be seven: those four plus memory_export,
memory_audit, memory_governance_delete (the full InMemoryKV-backed
local capability set the shim ships).

Root cause: the local-fallback branch of tools/list in
src/mcp/standalone.ts intersected the shim's IMPLEMENTED_TOOLS set
(7) with getVisibleTools(), which honors the shim's own
AGENTMEMORY_TOOLS env. Default env is unset, so getVisibleTools()
returns the 8 ESSENTIAL_TOOLS (the server's default "core" tier).
The 4 visible tools are exactly that intersection:

    ESSENTIAL_TOOLS  ∩  IMPLEMENTED_TOOLS  =
    {memory_save, memory_recall, memory_smart_search, memory_sessions}

The intersection was meaningless: the shim doesn't dispatch by
ESSENTIAL_TOOLS, it dispatches by IMPLEMENTED_TOOLS. Whatever
AGENTMEMORY_TOOLS the SERVER honored shouldn't shape the SHIM's
local-fallback list — the shim's local capabilities are fixed by
its in-memory KV, not by an env knob.

Fix: switch the filter source from getVisibleTools() to
getAllTools(). The filter through IMPLEMENTED_TOOLS still picks the
right 7 names, but now from the unfiltered universe of tool
definitions instead of the env-filtered subset.

Refactored the tools/list handler out of the inline createStdioTransport
callback into an exported handleToolsList() so it's directly testable.
Added a regression test that asserts the local-fallback path returns
exactly the 7 IMPLEMENTED_TOOLS regardless of AGENTMEMORY_TOOLS=core
(default) or unset.

Verified live via stdio against a downed server: shim now returns
all 7 names. Verified the previous bug repro (4 names) is gone.

860 / 860 tests pass on the branch (859 baseline + 1 regression test).
2026-05-11 14:46:11 +01:00
Rohit Ghumare 1813d08cbf docs(changelog): scrub v0.9.7 entry of client/sandbox names (#281)
v0.9.7 fixes are documented in client-agnostic terms now. The shim's
probe-failure + escape-hatch fix lands the same diagnostics for every
MCP integration; we don't need to enumerate which clients hit the
sandboxed-loopback case to make the change useful. Verified-live note
on stdio end-to-end stays so readers know the 51-tool path is proven.

The four fixes in the section are unchanged. The contributor thanks
to @jcalfee and @satabd are unchanged.
v0.9.7
2026-05-11 14:07:13 +01:00
Rohit Ghumare 82d1b06df5 fix(mcp): probe diagnostics + sandbox escape hatch (#234, #278) (#280)
* fix(mcp): probe diagnostics + sandbox escape hatch (#234, #278)

Three small follow-ups to v0.9.6 reported live on #234 and #278:

1. @agentmemory/mcp shim silently degraded to 7-tool local fallback for
   sandboxed MCP clients (Flatpak VS Codeium / Roo Code) because the
   500ms livez probe failed inside the sandbox network namespace and the
   catch swallowed the error. Probe now logs the URL, status, and reason
   to stderr; default timeout raised to 2000ms; AGENTMEMORY_PROBE_TIMEOUT_MS
   overrides the timeout; AGENTMEMORY_FORCE_PROXY=1 skips the probe and
   trusts AGENTMEMORY_URL outright for sandboxed clients that can reach
   the server through a known route but not the host loopback. Closes
   the #234 follow-up reported by @jcalfee.

2. Docker compose stack persisted state to an ephemeral container path.
   iii-config.docker.yaml used file_path: ./data/state_store.db, which
   the engine resolved against its container WORKDIR=/home/nonroot --
   not the /data mount where iii-data is bound. State and stream stores
   were silently written to the container layer and lost on every
   docker compose down. Paths are now absolute (/data/state_store.db
   and /data/stream_store). Existing users need a one-time docker
   compose down -v before upgrade.

3. CLI banner leaked "which: no iii in ..." when iii wasn't on PATH.
   execFileSync default stdio inherits stderr; GNU which writes the
   miss line to stderr (exit 1). Switched to explicit
   stdio: ["ignore", "pipe", "pipe"] in whichBinary().

4. docker-compose.yml now caps engine container log size at 30MB total
   (json-file driver, max-size=10m, max-file=3) so the iiidev/iii
   crash/restart spam reported by @satabd in #278 can no longer fill
   the host disk. The upstream engine spam itself needs filing against
   iii-hq/iii -- this is the compose-side guardrail.

Version 0.9.6 -> 0.9.7 across 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,
and the export round-trip test expectation.

* fix(mcp): AGENTMEMORY_DEBUG flag + loud unexpected-shape warning

@jcalfee on #234 confirmed the v0.9.6 fix (and PR #280's escape hatch)
still surfaces 7 tools in Roo Code even though:
  - Flatpak share=network is on (probe succeeds via curl in-sandbox)
  - server's /agentmemory/mcp/tools returns 51 tools via curl
  - AGENTMEMORY_FORCE_PROXY=1 is set

Roo Code shows 7 tools even when docker is down -- strong signal of
client-side caching at the Roo Code layer, not a shim regression.
But the shim has zero visibility into what it actually returns to
the MCP client right now: success path is silent, only failure paths
log.

Add AGENTMEMORY_DEBUG=1 (or =true) so the shim writes to stderr:
  - which mode handle.mode resolved to (proxy vs local) and baseUrl
  - shape of the /agentmemory/mcp/tools response (keys + tools type)
  - count of tools returned to the MCP client
  - tool names in the local-fallback list if we hit it

Also: previously, when the server returned a JSON shape that wasn't
{tools: Array}, the shim silently fell back to the 7-tool local list
with no log line. Now that path warns to stderr unconditionally,
pointing at AGENTMEMORY_DEBUG=1 for inspection.

This is diagnosis enablement -- not a fix for whatever is making
Roo Code show 7 tools. Once @jcalfee can run the shim with
AGENTMEMORY_DEBUG=1 and share the stderr output, we'll know whether
the shim returns 51 (Roo Code bug) or 7 (deeper shim bug).
2026-05-11 13:59:11 +01:00
Rohit Ghumare 13924d2f1b chore(release): v0.9.6 — search recall + MCP shim + hook latency (#272)
Three reliability fixes from #269/#270/#271:

- search/recall surfaces saved memories (closes #265)
- MCP shim proxies full server tool set (closes #234)
- session/subagent hooks no longer block startup (closes #221)

Also fixes packages/mcp version drift — was stuck at 0.9.4 through v0.9.5,
now lockstepped with main.
v0.9.6
2026-05-10 19:25:41 +01:00
Rohit Ghumare 5fc73f2eb2 fix: surface saved memories in smart_search/recall (#265) (#269)
* fix(memory): fall back to KV.memories on search enrich miss (#265)

mem::remember persists memories to KV.memories under a synthetic
sessionId ("memory" or memory.sessionIds[0]). The BM25 index sees that
synthetic sessionId, but KV.observations under it never has an entry —
so both enrichment paths silently dropped every memory hit:

- HybridSearch.enrichResults (powers /smart-search, memory_smart_search)
- mem::search obsResults map (powers /search, memory_recall)

Result: memory_save succeeds and the memory shows up in the viewer and
GET /memories, but every search/recall returns []. The save→search flow
was effectively write-only.

Both sites now try KV.observations first, then fall back to KV.memories
and coerce the Memory record into a CompressedObservation. Verified live
end-to-end against an iii-engine 0.11.2 stack: prior to the fix
/smart-search and /search both returned [] for a saved memory; after the
fix the memory surfaces with score > 0 in both responses.

Regression tests added to test/hybrid-search.test.ts and test/search.test.ts.

* refactor(memory): extract memoryToObservation helper

Three copies of the Memory→CompressedObservation conversion existed:
hybrid-search.ts (added in #265 fix), search.ts, remember.ts. Consolidate
into src/state/memory-utils.ts and import from all three callsites.

Behavior unchanged. Full suite: 854 passing.
2026-05-10 19:14:44 +01:00
Rohit Ghumare 6e011e56b6 fix: MCP standalone shim proxies full server tool set (#234) (#270)
`agentmemory mcp` capped non-Claude clients (OpenCode, Cursor, Gemini
CLI, Cline, …) at the local 7-tool `IMPLEMENTED_TOOLS` set even when an
agentmemory server was reachable. With `AGENTMEMORY_TOOLS=all` on the
server the user expected 51 tools — the shim filtered the response down
to whatever subset overlapped its hardcoded set (4 by default, 7 with
the env var on).

When a server is reachable, the shim now:

- Delegates `tools/list` to GET `/agentmemory/mcp/tools` and returns
  the server's full advertised set verbatim.
- Forwards any tool not in the local `IMPLEMENTED_TOOLS` set to
  POST `/agentmemory/mcp/call` (the server validates per tool).

The local `InMemoryKV` fallback path is unchanged — it still implements
only the 7 essential tools, and unknown tools throw a clear error
pointing to `AGENTMEMORY_URL` when no server is reachable.

Verified live end-to-end:

- Pre-fix: shim → tools/list returns 4 tools (default) / 7 (TOOLS=all);
  `memory_lesson_save` → "Unknown tool".
- Post-fix: shim → tools/list returns 51 tools when the server runs
  with TOOLS=all; `memory_lesson_save` → lesson created end-to-end via
  the shim's stdio JSON-RPC interface.

Two regression tests added covering the proxy-forward path and the
no-server failure mode.

Closes #234.
2026-05-10 19:08:50 +01:00
Rohit Ghumare 1ff5849d9c fix: cap session-start/subagent-start hook latency (#221) (#271)
Two hook scripts blocked Claude Code's startup waiting on REST responses
they didn't actually need:

- `session-start` awaited a 5000ms POST and discarded the response when
  `AGENTMEMORY_INJECT_CONTEXT=false` (the default). Pure latency.
- `subagent-start` had a `// fire and forget` comment but the code
  awaited a 2000ms POST. Pure latency.

Under fan-out (Slack-bot orchestrators, multi-agent harnesses, fanned
`claude -p` jobs) the awaited timeouts stack and feed back into the
engine; the reporter hit a positive feedback loop that OOM-killed
iii-engine.

Fix:

- `session-start` — fire-and-forget when `INJECT_CONTEXT=false`. Cap the
  inject path at 1500ms (down from 5000ms) so a slow server can't block
  the agent indefinitely when stdout is actually consumed.
- `subagent-start` — actually fire-and-forget, matching the existing
  comment. Cap at 800ms.

Verified live against a black-hole TCP listener (accepts, never replies):
- session-start (no inject): 5.05s → 0.85s
- session-start (inject):    5.05s → 1.55s
- subagent-start:            2.05s → 0.87s

Built artifacts in `plugin/scripts/` regenerated via `npx tsdown`.

Closes #221.
2026-05-10 19:08:34 +01:00
Rohit Ghumare 0322da8004 docs(changelog): reframe v0.9.5 intro — sandbox-model shift, not regression
Missed reframe: the intro paragraph for the 0.9.5 entry still called
v0.11.6 a "regression". v0.11.6 is actually a new sandbox-everything-
via-`iii worker add` architecture that agentmemory hasn't been
refactored for yet. Pin lifts once the refactor lands, not on
upstream "fix".

Brings the intro paragraph in line with the body of the same entry
and with the cli.ts / docker-compose.yml / README install section
comments that already use the correct framing.
v0.9.5
2026-05-09 17:45:05 +01:00
Rohit Ghumare 0c73d868be chore(release): v0.9.5 — search recall + plugin compatibility (#261)
Bug-fix patch focused on search recall correctness and plugin
compatibility. Pins iii-engine to v0.11.2 because v0.11.6 introduces
a new sandbox-everything-via-`iii worker add` model that agentmemory
hasn't been refactored for yet — pin lifts once that refactor lands.
Adds a hard guard against silent vector-index corruption, fixes BM25
indexing for memories saved via memory_save, and lands four Hermes
plugin fixes.

Per AGENTS.md release checklist:
- package.json version 0.9.4 -> 0.9.5
- src/version.ts VERSION constant
- src/types.ts ExportData version union
- src/functions/export-import.ts supportedVersions Set
- test/export-import.test.ts assertion
- plugin/.claude-plugin/plugin.json version
- CHANGELOG.md detailed entries with contributor shoutouts

Headlines (full detail in CHANGELOG):

Fixed:
- BM25 search now indexes memories saved via memory_save (#258, #257)
  Thanks @Nizar-BenHamida for the precise repro.
- Embedding providers no longer silently corrupt the vector index when
  an API returns wrong-dimension vectors (#248, #247, #256)
  Thanks @AmmarSaleh50 for issue + fix + tests.
- Hermes handle_tool_call returns JSON strings, not raw dicts (#255, #254)
  Thanks @KyoMio for the Anthropic-protocol repro.
- Hermes status reflects real service state on systemd installs (#253, #250)
  Thanks @OptionalCoin for tracing it to env-source divergence.
- Hermes hooks accept passthrough kwargs (#252, #249)
  Thanks @OptionalCoin again for the log analysis.
- agentmemory demo now seeds observations correctly (#251, #229)
  Thanks @seishonagon for root-cause analysis.
- LLM compression / summarization timeouts increased (#213)
  Thanks @xuli500177.
- Pi / OpenClaw / Hermes integration plugin fixes (#230)
  Thanks @deepmroot.

Changed:
- iii-engine pinned to v0.11.2 across every install path (#260).
  v0.11.6 introduces a new `iii worker add` sandbox model that
  agentmemory still pre-dates; pin lifts when we refactor agentmemory
  to register as a sandboxed worker. Override with
  AGENTMEMORY_III_VERSION=<version> for users who've migrated manually.
- README documents iii worker add extension surface (#242).
- README iii Console install/launch commands corrected (#243).

Validated: 852/852 tests pass, npm run build clean.
2026-05-09 17:39:20 +01:00
Rohit Ghumare 3b10330c27 fix(install): pin iii-engine to v0.11.2 across all install paths (#260)
* fix(install): pin iii-engine to v0.11.2 across all install paths

iii-engine v0.11.6 ships a regression where engine-internal cron/http
trigger registrations fail validation at boot, causing the agentmemory
worker to drop into an EPIPE reconnect loop and BM25 search to return
empty after every save. Symptom: `npx @agentmemory/agentmemory demo`
reports "worker not reachable" and memory_smart_search returns 0
results regardless of memory_save success.

Until the engine fix lands upstream, every install path now resolves
to v0.11.2:

- src/cli.ts auto-installer downloads
  github.com/iii-hq/iii/releases/download/iii/v0.11.2/iii-<arch>.tar.gz
  directly instead of piping `install.iii.dev/iii/main/install.sh`,
  which tracks `latest` and pulls the broken 0.11.6.
- src/cli.ts Docker fallback pulls `iiidev/iii:0.11.2`, not `:latest`.
- src/cli.ts install instructions and Windows guidance updated to
  point at the v0.11.2 release page.
- docker-compose.yml `image: iiidev/iii:0.11.2` (was `:latest`).
- README.md install section documents the pin and lists per-arch
  curl commands for the pinned tarball.

Escape hatch: AGENTMEMORY_III_VERSION=<version> overrides the pin for
users who've verified compat manually. Bumping the default requires
end-to-end testing with a fresh agentmemory install + the engine
fix shipped.

Verified the pinned URL resolves: `iii-aarch64-apple-darwin.tar.gz`
extracts an `iii` binary that reports `--version` 0.11.2.

Per-arch assets covered by `iiiReleaseAsset()`: darwin arm64 / x64,
linux x64 / arm64 / armv7, win32 x64 / arm64.

* fix(install): address review feedback on pin patch

Three issues from review of f04f53f:

1. docker-compose.yml hardcoded `iiidev/iii:0.11.2`, ignoring the
   AGENTMEMORY_III_VERSION escape hatch. Switch to
   `${AGENTMEMORY_III_VERSION:-0.11.2}` so `AGENTMEMORY_III_VERSION=0.11.7
   docker compose up` actually moves to 0.11.7. Default unchanged.
   Verified with `docker compose config`: env-set produces 0.11.7,
   unset produces 0.11.2.

2. Displayed Linux install one-liner in installInstructions() (and the
   matching README per-arch examples) called `tar -xz -C ~/.local/bin`
   without ensuring the directory existed. Fresh macOS users without a
   prior `~/.local/bin` would hit `tar: ~/.local/bin: Cannot open: No
   such file or directory`. Prepend `mkdir -p ~/.local/bin && ` to the
   shown command and to the README example. The programmatic install
   path in runUpgrade was already correct (line 1055 mkdir step), so
   only display strings change.

3. runUpgrade's auto-installer branch assumed every release asset was a
   tarball — would silently pipe a Windows .zip through `tar -xz` and
   fail. Detect Windows or `.zip`-suffix asset, skip the auto-installer
   on that path, and surface the manual download + Docker fallback
   instructions instead. Matches what installInstructions() already
   shows for Windows.

Skipped finding (none in this round — all three were valid). No new
findings surfaced during fix.

Validated:
- `npx tsc --noEmit` — no new errors from changed files.
- `npm run build` — clean.
- `docker compose -f docker-compose.yml config` — interpolation
  resolves correctly with and without AGENTMEMORY_III_VERSION set.
2026-05-09 16:23:47 +01:00
Rohit Ghumare 1a035383de fix(memory): index memories into BM25 + backfill on startup (closes #257) (#258)
memory_save returned 200 with a valid memory ID, but every subsequent
memory_smart_search and memory_recall returned empty results. Direct
REST /agentmemory/smart-search showed the same — confirming the bug was
not in the MCP layer.

Root cause: mem::remember in src/functions/remember.ts wrote the new
Memory to KV.memories but never called getSearchIndex().add(). The same
pattern that #228 fixed for vectors-on-observe was missing here for
BM25-on-remember. Compounded by rebuildIndex() walking only sessions/
observations and skipping KV.memories, so even a restart-rebuild
couldn't recover indexed memories.

Three changes:

1. src/functions/remember.ts — synthesize a CompressedObservation-shaped
   record from the saved Memory (title + content + concepts + files) and
   add it to BM25 right after kv.set(). Wrapped in try/catch so an
   indexing hiccup never blocks the durable save.

2. src/functions/search.ts — rebuildIndex() now walks KV.memories before
   sessions/observations, so a fresh rebuild covers the full corpus.
   Skips memory.isLatest === false so superseded entries don't pollute
   results.

3. src/index.ts — startup backfill for users upgrading from <0.9.5: when
   the persisted BM25 is non-empty (no rebuild triggered) but legacy
   memories were never indexed, walk KV.memories and add anything
   missing. SearchIndex.has(id) is the new idempotency gate.
   indexPersistence.scheduleSave() persists the augmented index.

Tests in test/remember-bm25-index.test.ts cover:
- SearchIndex.has() before/after add
- A saved memory is findable by keyword (the case the bug broke)
- The exact repro from the issue (mem_moy3u6ua_..., query "BM25 test")
- Concept-only matches (no title/content overlap)

Out of scope (file as follow-up):
- Removing superseded memories from BM25 when mem::cascade-update fires.
  Currently relies on isLatest filter at result time; not perfect but
  doesn't regress recall.
- Memory restore via IndexPersistence (covered transparently by the
  startup backfill, but a dedicated round-trip test would be tighter).

Reported by @Nizar-BenHamida with full repro + log capture.
2026-05-09 14:23:45 +01:00
Ammar Saleh ab4a16664e fix(embedding): guard provider responses against dimension mismatches (#248)
* fix(embedding): guard provider responses against dimension mismatches

Closes #247.

Embedding providers in src/providers/embedding/ trust that the API
returns vectors matching their declared dimensions. None of them check
result.length === this.dimensions. When that breaks, the failure is
silent: src/state/vector-index.ts:10 returns 0 from cosineSimilarity on
length mismatch, so a wrong-size vector gets stored, never matches
anything, and the affected memory becomes invisible without a single
error surfacing.

Add a single dimension-check wrapper at the EmbeddingProvider boundary
in createEmbeddingProvider() / createImageEmbeddingProvider(). Every
provider inherits the guard for free; new providers added later are
covered automatically. Throws a descriptive error naming the provider,
the call site (embed / embedBatch[i] / embedImage), expected vs got
dimensions.

Tests cover the happy path and each method's mismatch path.

Signed-off-by: ammarsaleh50 <ammar.alammar23@gmail.com>

* fix(embedding): preserve provider prototype in dimension-guard wrapper

Address CodeRabbit review on #248: the wrapper built a plain object
instead of preserving the provider's prototype chain, which broke the
existing toBeInstanceOf(GeminiEmbeddingProvider) /
toBeInstanceOf(OpenAIEmbeddingProvider) checks against
createEmbeddingProvider() in test/embedding-provider.test.ts.

Switch the wrapper to Object.create(provider) so the prototype chain is
preserved. `name` and `dimensions` fall through to the underlying
provider; only `embed` / `embedBatch` / `embedImage` are overridden
to insert the dimension check.

Add a regression test asserting that `withDimensionGuard` preserves
`instanceof`.

Signed-off-by: ammarsaleh50 <ammar.alammar23@gmail.com>

* fix(persistence): refuse to load vector index when persisted dim mismatches active provider

The factory-boundary dimension guard in this PR catches wrong-dim
vectors on the live-API write path. The persistence restore path is the
symmetric on-ramp:

  IndexPersistence.load() at src/state/index-persistence.ts:62-66
  deserializes vectors directly from KV with no dimension check.

If a user persists an index built against an N-dim provider and then
swaps embedding configuration (EMBEDDING_PROVIDER, OPENAI_EMBEDDING_MODEL,
local model upgrade, etc.), the restore brings old-dim vectors back
into a freshly-instantiated VectorIndex while live observations write
new-dim vectors alongside. cosineSimilarity returns 0 on every cross-
dim pair — same silent recall degradation #247 documents, just on a
different on-ramp.

This commit adds:

1. VectorIndex.firstDimensions() — exposes the dimension of any stored
   vector (or 0 if empty). All vectors in a single index are expected
   to share a dimension; the first entry is representative because the
   live-write path is now gated by the guard added earlier in this PR.

2. A startup check in src/index.ts after IndexPersistence.load(). When
   the persisted index has a different dimension than the active
   provider, the default behavior is to refuse to start with a clear
   error message:

     [agentmemory] Refusing to start: persisted vector index has
     dimension 384, but the active provider (openai) declares 1536.
     Loading would silently corrupt search (cross-dimension cosine
     returns 0). Choose one:
       - Re-embed the existing index against the new provider, then start.
       - Set AGENTMEMORY_DROP_STALE_INDEX=true to discard the persisted
         vectors and rebuild from live observations.
       - Switch the embedding provider back to the one that wrote the index.

3. Opt-in escape hatch: AGENTMEMORY_DROP_STALE_INDEX=true logs a warning,
   discards the persisted vectors, and lets the live path rebuild over
   time. Friendlier for users who deliberately swap providers.

Test: VectorIndex.firstDimensions() returns 0 for empty and the correct
size for populated indexes (small + 1536-dim).

Closes #256.

* fix(persistence): walk every persisted vector when validating dimensions

Review feedback on 57311c6: firstDimensions() only sampled the first
stored vector. A legacy on-disk index that mixes dimensions inside one
file (possible when a previous run swapped embedding model mid-session
before the live-API guard in this PR existed) would slip past — the
first vector might match while later ones don't, and the silent
cross-dim cosine corruption returns through the back door.

- Replace VectorIndex.firstDimensions() with validateDimensions(expected)
  that walks every entry, returns the obsIds whose dim doesn't match,
  and reports the set of distinct dims actually seen on disk.
- src/index.ts now refuses to load whenever any single vector mismatches
  (not just the bulk dim). Error message reports how many of the total
  are bad, the distinct dims seen on disk, and the first 5 mismatched
  obsIds so the user can spot-check.
- Test rewrite covers: empty index, all-match, partial mismatch (the
  case firstDimensions() missed), and entire-index mismatch.

The reviewer also flagged "the other similar check around the second
occurrence" — verified there is no second occurrence in the codebase.
The only restoreFrom site is bm25Index.restoreFrom at src/index.ts:333
which restores a BM25 index that doesn't carry dimensions. Skipping.

---------

Signed-off-by: ammarsaleh50 <ammar.alammar23@gmail.com>
Co-authored-by: Rohit Ghumare <ghumare64@gmail.com>
2026-05-09 13:19:21 +01:00
Rohit Ghumare eaecc658d3 fix(hermes): serialize handle_tool_call returns to JSON string (closes #254) (#255)
Hermes stores handle_tool_call's return value as the tool result
`content` field in the session history. Anthropic-protocol LLM providers
reject non-string content on the next request with:

  Failed to deserialize the JSON body into the target type:
  messages[N]: content should be a string or a list at line 1 column XXXXX

Once triggered, every subsequent request in that session 400s until the
session JSON is manually cleaned.

The plugin's handle_tool_call returned raw Python dicts in all 4 paths
(memory_recall, memory_save, memory_search, unknown-tool fallback). Wrap
each return in json.dumps so the result is always a string.

This matches the contract that agentmemory's own MCP server already
honors in src/mcp/standalone.ts:

  { type: "text", text: JSON.stringify(payload, null, ...) }

Tightens the return-type annotation on both the abstract base and the
concrete class from Any -> str so mypy / type checkers catch any future
regression.

Reported by @KyoMio with full Anthropic-protocol repro.
2026-05-09 11:50:05 +01:00
Rohit Ghumare c2f2618c74 fix(hermes): preload ~/.agentmemory/.env so status reflects service env (closes #250) (#253)
When agentmemory runs as a systemd user service (or any process manager
that loads ~/.agentmemory/.env directly), the values never reach the
shell that runs `hermes memory status`. Status then reads os.environ in
the Hermes CLI process, finds AGENTMEMORY_URL / AGENTMEMORY_SECRET
unset, and reports the plugin as "Missing" even though the service is
healthy and live sessions can use it.

Preload the documented config file at plugin-import time using
os.environ.setdefault so anything explicitly set in the shell still
wins. Best-effort: silently skip when the file is absent, unreadable,
or malformed — the plugin falls back to its existing defaults.

Both ~/.agentmemory/.env (primary, per agentmemory's own README) and
$XDG_CONFIG_HOME/agentmemory/.env (fallback) are checked.

Reported by @OptionalCoin.

Tested:
  $ printf 'AGENTMEMORY_URL=http://example\nAGENTMEMORY_SECRET=s\n' \
      > ~/.agentmemory/.env
  $ unset AGENTMEMORY_URL AGENTMEMORY_SECRET
  $ python -c "import sys; sys.path.insert(0, 'integrations'); \
                import hermes; \
                import os; \
                print(os.environ['AGENTMEMORY_URL'])"
  http://example
  $ AGENTMEMORY_URL=http://shell-wins python -c "..."
  http://shell-wins         # explicit shell value still wins
2026-05-09 11:41:25 +01:00
Rohit Ghumare 2c24c9077e fix(hermes): accept passthrough kwargs in memory provider hooks (closes #249) (#252)
Hermes calls memory provider hooks with extra context kwargs (e.g.
session_id) that the strict signatures rejected with:

  Memory provider 'agentmemory' sync_turn failed: AgentMemoryProvider.
  sync_turn() got an unexpected keyword argument 'session_id'

Add **kwargs: Any to every hook so extra context fields are tolerated:
sync_turn, on_session_end, on_pre_compress, on_memory_write, prefetch,
queue_prefetch, shutdown. Mirror the same change on the abstract
fallback MemoryProvider used when `agent.memory_provider` is unavailable
at import time.

Where Hermes passes session_id, prefer it over the cached
self._session_id so per-turn observations land on the correct session
when Hermes routes a hook through a different conversation than the one
the provider was initialized with.

Reported and locally verified by @OptionalCoin.
2026-05-09 11:18:49 +01:00
Rohit Ghumare 3f5bb87da1 fix(cli): include project + cwd in demo seeder observe payload (closes #229) (#251)
`npx @agentmemory/agentmemory demo` was seeding 0 observations because
the per-observation POST to /agentmemory/observe was missing the
`project` and `cwd` fields, which `api::observe` validates as required
non-empty strings.

session/start and session/end already received project/cwd correctly;
only the observe call in `seedDemoSession` was inconsistent. project is
already a function parameter so no plumbing was needed.

Repro before fix:

  $ npx @agentmemory/agentmemory demo
  ▲  observe failed for Write: 400 Bad Request — {"error":"hookType,
     sessionId, project, cwd, and timestamp are required strings"}
  ◇  Seeded 0 observations across 3 sessions
2026-05-09 11:18:21 +01:00
Rohit Ghumare 989e9c022e docs(readme): replace console screenshot with Workers page
Swap the iii console screenshot from the generic dashboard view to the
Workers page, which actually shows agentmemory instances connected to
the engine with live function counts and runtime metadata. More
illustrative of what users see when they open the console alongside a
running agentmemory.
2026-05-08 17:56:31 +01:00
Rohit Ghumare 4ef188eba4 docs(readme): correct iii console install + launch commands (#243)
The console ships with `iii` and is launched as a subcommand, not via a
separate installer or `iii-console` binary. Update the README to use the
real surface from iii.dev/docs/console:

- Drop the bogus `install.iii.dev/console` curl line — there is no
  separate installer.
- Replace `iii-console ...` with `iii console --port 3114`. Port flag
  is enough on a default agentmemory setup; engine-port, ws-port, and
  bridge-port are documented as overrides.
- Mention `--enable-flow` for the experimental architecture-graph page.
- Add the missing console pages to the capability table: Workers,
  Queues, Config, Flow.
2026-05-08 17:52:26 +01:00
Rohit Ghumare 69472ee093 Update README.md 2026-05-08 17:45:26 +01:00
Rohit Ghumare 6e2add106c Update README.md 2026-05-08 17:43:54 +01:00
Rohit Ghumare b91a9a762e docs(readme): show how to extend agentmemory with iii workers (#242)
* docs(readme): show how to extend agentmemory with iii workers

Promote the iii console to its own section and add an "Extend with one
command" section that maps each `iii worker add <name>` to a concrete
agentmemory capability — multi-instance memory, scheduled consolidation,
durable retries on embeddings, sandboxed code execution, SQL state
adapter, and an extra MCP host.

Lists only workers published on workers.iii.dev with direct links to
each registry page so users can install them and see the new functions
appear in the viewer + console immediately.

* Update README.md
2026-05-08 17:43:19 +01:00
xuli500177 1effa2c27e fix: increase systemic timeouts for summarization and consolidation (#213)
* fix: increase summarize timeout in stop hook

* fix: increase fetch timeouts in session-end hook

* fix: increase default sdk invocation timeout to 180s
2026-05-07 15:43:52 +01:00
Mandeep Singh 2daeb5afda Add tested Pi/OpenClaw/Hermes integration fixes (#230) 2026-05-07 15:41:52 +01:00
Rohit Ghumare 94fc1191ef chore: bump version to 0.9.4 + add CHANGELOG (#216) v0.9.4 2026-04-29 17:26:04 +01:00
Rohit Ghumare 3a8ba85696 Merge pull request #215 from rohitg00/fix/210-auto-fire-graph-extract
fix(graph): auto-fire mem::graph-extract at session end (closes #210)
2026-04-29 15:56:31 +01:00
Rohit Ghumare 91702963be fix(graph): auto-fire mem::graph-extract at session end (closes #210)
mem::graph-extract was registered and the REST endpoint at
POST /agentmemory/graph/extract was live, but no internal caller invoked
the function. Setting GRAPH_EXTRACTION_ENABLED=true populated nothing —
the graph KV stayed empty unless users manually POSTed observations to
the extract endpoint.

Wire the function into event::session::stopped (alongside the existing
mem::slot-reflect glue). Fire-and-forget via triggerVoid; the existing
node-merge (name+type) and edge-merge (source|target|type) keys make
re-runs idempotent.

README pipeline diagram updated: graph extraction shown at the
Stop/SessionEnd phase rather than implying it runs per PostToolUse.
The previous wording was misleading — the BM25 + vector indices ARE
written per-PostToolUse, but the graph never was.

Note: mem::temporal-graph-extract has the same orphan status and is
NOT addressed here. Tracking separately.
2026-04-29 13:10:47 +01:00
Rohit Ghumare 2dfe12833e Merge pull request #214 from rohitg00/fix/212-doctor-plugin-hooks
doctor: detect Claude Code plugin-hooks load state (refs #212)
2026-04-29 12:07:00 +01:00
Rohit Ghumare eb353d270c feat(doctor): detect whether Claude Code loaded the plugin hooks
Adds a check to `agentmemory doctor` that scans the latest Claude Code
debug log (~/.claude/debug/latest) for the
`Loaded hooks from standard location for plugin agentmemory` line.

When the plugin manifest is enabled but the hooks never auto-register,
users see no error -- the plugin silently does nothing. Doctor now
surfaces this with a remediation hint (reinstall + restart, require
CC >= 2.1.x).

States:
- loaded:        ✓  shows manifest path resolved by CC
- not-loaded:    ✗  hints reinstall + restart
- no-debug-log:  ✗  hints to run claude --debug once
- no-cc-dir:     skipped (user may not use Claude Code)

Refs #212.
2026-04-29 11:38:16 +01:00
Rohit Ghumare 1a8c0f325c Update README.md 2026-04-28 10:46:58 +01:00
Rohit Ghumare bd4b74e067 fix: prevent process crash on iii-engine state::set timeout (#204)
Under sustained write load (e.g. Claude Code plugin hooks across
multiple agents), `IndexPersistence`'s 5s debounce flush could fire
into an iii-engine queue that hadn't drained, hitting the SDK's 30s
default timeout on `state::set`. The rejection escaped the
`setTimeout(() => this.save(), DEBOUNCE_MS)` callback in
`src/state/index-persistence.ts` because the returned promise was
discarded — landing as an unhandledRejection that terminated the
long-lived memory service. Reproducible at ~5–15 crashes/hour on
~1.7K observations/day workloads.

Two layers of defense:

1. **`IndexPersistence` swallows kv.set rejections internally.** The
   scheduled-save callback now `.catch()`s the returned promise and
   `save()` wraps the kv.set calls in try/catch. Failures are logged
   via `logger.warn` with throttling (once per minute) so a queue-
   pressure burst doesn't spam the log. Recent index updates stay
   in memory and retry on the next debounce flush.

2. **Top-level `unhandledRejection` handler in `src/index.ts`.**
   Logs and continues for any other path we missed — defense in
   depth so a single SDK timeout can't take down the memory mesh.
   Also throttled.

Adds two regression tests in `test/index-persistence.test.ts`:
- scheduled save under a kv.set that rejects must not raise
  unhandledRejection
- direct `save()` must resolve (not throw) when kv.set rejects

Thanks @bunke for the painstaking writeup — the journalctl trace
pointing at iii-sdk/dist/index.mjs:405 made the root cause obvious.

Closes #204
2026-04-27 11:08:11 +01:00
Rohit Ghumare aef1dbbfe8 fix(replay,cli): clamp maxFiles to upper bound + cap traversal by entries visited
Five follow-ups from CodeRabbit and qodo on the import-jsonl patch:

1. **Out-of-range maxFiles silently degraded to default** — caller
   passing maxFiles > 1000 got a 202 with maxFiles: 200 in the
   response, no error. The HTTP layer in src/triggers/api.ts only
   validated `Number.isInteger && >= 1` (no ceiling), so the contract
   drifted between transport and handler. Now:
   - HTTP returns 400 with the bound in the error message
   - The SDK-callable handler clamps valid integers to MAX_FILES_UPPER_BOUND
     (safety net for non-HTTP callers)
   - Constants exported from src/functions/replay.ts so api.ts and
     downstream callers share one source of truth

2. **Walker traversal bounded by `discovered`** — `discovered` only
   counts .jsonl files. Trees dominated by non-jsonl files
   (node_modules, lockfiles) could still walk past the function
   timeout. Switched to a separate `walked` counter incremented per
   directory entry visited; threshold raised to max(limit*50, 50_000)
   to give legitimate trees room. Returns `traversalCapped: boolean`
   so callers can distinguish "found more than we showed" from
   "stopped walking early for safety".

3. **CLI swallowed flag values from --port and --tools** — the
   import-jsonl arg loop only consumed `--max-files`'s value token.
   `agentmemory --port 3112 import-jsonl` left "3112" as a positional,
   resolving pathArg to a numeric string. Added a VALUE_FLAGS set
   (`--port`, `--tools`) that consumes the next token alongside the
   flag.

4. **Suggested --max-files in warning could exceed 1000** — the old
   message said "Re-run with --max-files=5100" for a 5000-file tree,
   but the server caps at 1000, so the user would see the same
   warning with no progress. Now clamps the suggestion to
   maxFilesUpperBound, and when discovered > upper bound (or
   traversalCapped fired), recommends batching by subdirectory
   instead.

5. **Help text omitted the upper bound** — added the 1000 ceiling
   and the "batch by subdirectory" guidance for trees larger than
   the cap.
2026-04-27 10:50:19 +01:00
Rohit Ghumare a334ea0611 fix(replay,cli): cap walker traversal, validate maxFiles, harden flag parsing
Five follow-ups from CodeRabbit on the import-jsonl patch:

1. **Walker no longer short-circuits** — after dropping the in-loop
   limit return, \`findJsonlFiles\` walked the entire tree even when
   the user only wanted 200 files, risking 30s timeouts on million-file
   trees. Added a \`traversalCap = max(limit * 10, 10_000)\` so
   \`discovered\`/\`truncated\` stay accurate but the walk stops well
   short of pathological depths.

2. **\`maxFiles\` accepted floats and unbounded values** — guard the
   handler with \`Number.isInteger\` + upper bound (1000) so SDK callers
   can't pass 1.5 or 1_000_000. Out-of-range falls back to default 200.

3. **CLI flag value contaminated \`pathArg\`** — the old
   \`filter(a => !a.startsWith('-'))\` left the numeric value of
   \`--max-files 500\` as a positional, so \`agentmemory import-jsonl
   --max-files 500\` resolved \`pathArg\` to \`"500"\`. Replaced the
   filter with a single pass that consumes the flag's value and skips
   both space-form and equals-form.

4. **CLI swallowed invalid \`--max-files\` values silently** — emit a
   \`p.log.warn\` with the offending token when parsing fails so the
   user sees why their flag was ignored.

5. **Help text only documented the space form** — added the equals
   form (\`--max-files=<N>\`) so both invocations are visible in
   \`--help\`.
2026-04-27 10:50:19 +01:00
Rohit Ghumare 6768b1055b fix(replay): preserve real sessionId + surface 200-file scan cap
Two bugs in the JSONL import path that combined to make
\`agentmemory import-jsonl\` against a real \`~/.claude/projects\` tree
quietly mis-import: ~10% of files scanned, each landing under a fresh
random session id even when the file's own header named one.

### #202 — \`parseJsonlText\` ignores the file's sessionId

The parser pre-populated \`sessionId\` from \`fallbackSessionId\` and then
guarded \`if (entry.sessionId && !sessionId)\`, so the file's real id
was never adopted. Each call returned a freshly generated session,
breaking merge-on-reimport (Session.observationCount accumulation,
endedAt extension, jsonl-import tag set) and inflating the session
count ~4× on real trees.

Fix: don't pre-populate sessionId; fall back to \`fallbackSessionId\`
and then \`generateId('sess')\` only after the loop has had a chance
to find the file's id.

### #203 — \`import-jsonl\` CLI silently caps at 200 files

\`findJsonlFiles\` had a hard 200-file safety cap (sensible given the
30s function timeout) but the CLI exposed no flag to override it and
the post-import summary never warned that files were skipped. On a
2167-file tree users got a silent ~10% import.

Fix:
- \`findJsonlFiles\` now returns \`{files, truncated, discovered}\` so
  the handler can keep counting beyond the cap without retaining paths
- Handler returns \`discovered\`, \`truncated\`, \`maxFiles\` in the response
- CLI accepts \`--max-files <N>\` (and \`--max-files=<N>\`)
- CLI warns on truncation: how many were skipped, suggested re-run
  with a larger cap

Adds three regression tests in \`test/replay.test.ts\` covering: file
sessionId beats fallback, two parses of the same file produce the same
id, fallback used only when file has no id.

Thanks @bloodcarter for the precise repros and root-cause pointers.

Closes #202
Closes #203
2026-04-27 10:50:19 +01:00
Rohit Ghumare 47184fdf17 fix(config): handle quoted values with trailing inline comments + reuse provider detection
Three follow-ups from CodeRabbit on the earlier .env-loading patch:

1. **Quoted values followed by inline comments** — \`TOKEN="abc" # note\`
   parsed as the literal string \`"abc"\` because the round-trip checked
   "ends with quote" before stripping the trailing comment. Reworked to
   detect the opening quote, find the matching close quote, and slice
   between them; falls through to the existing \` #\` strip for
   unquoted values. Adds two regression tests covering both single and
   double quotes followed by comments.

2. **Provider detection diverged from \`detectProvider()\`** — the
   \`api::config-flags\` helper missed \`GOOGLE_API_KEY\` (which
   \`detectProvider\` accepts as an alias for \`GEMINI_API_KEY\`) and
   used raw \`getEnvVar()\` instead of the trimmed \`hasRealValue()\`
   semantics, so whitespace-only keys read as set. Extracted a single
   \`detectLlmProviderKind()\` helper in \`src/config.ts\` that mirrors
   \`detectProvider\`'s checks; \`api::config-flags\` now calls it.

3. **Test env restore stringified \`undefined\`** — when \`HOME\` or
   \`USERPROFILE\` was unset on the host, \`process.env[k] = ORIGINAL\`
   coerced to the literal string \`"undefined"\`. Conditional
   restore (delete vs assign) preserves the original missing-state.
2026-04-27 01:05:57 +01:00
Rohit Ghumare 293e008756 fix(config): strip inline # comments and route /config/flags through merged env
Two related env-loading bugs that combined to make valid `~/.agentmemory/.env`
configs silently behave as if half the user's flags were unset:

1. `loadEnvFile()` only stripped full-line `#` comments. Trailing inline
   comments — natural when annotating boolean toggles like
   `AGENTMEMORY_AUTO_COMPRESS=true   # opt in` — survived into the value,
   so the strict `=== "true"` checks in `isAutoCompressEnabled()`,
   `isConsolidationEnabled()`, and `isGraphExtractionEnabled()` always
   returned false. Now strips ` #` outside quotes; preserves `#` inside
   quoted values and as a non-leading character of unquoted values.

2. `api::config-flags` (`GET /agentmemory/config/flags`) read provider
   keys from `process.env` directly instead of `getMergedEnv()`. With the
   key set in `~/.agentmemory/.env` (the README's recommended path) the
   provider was actually working, but the diagnostics endpoint reported
   `provider: "noop"` and the viewer falsely warned "No LLM provider key
   set". Now reads via `getEnvVar()` so both surfaces agree.

Adds `test/env-loader.test.ts` covering inline-comment stripping,
quoted-value preservation, and embedded-hash values.

Thanks @bloodcarter for the precise repros and root-cause pointers.

Closes #200
Closes #201
2026-04-27 01:05:57 +01:00
Rohit Ghumare c4efcd9ff3 feat(api): add GET /agentmemory/memories/:id endpoint
The route was simply not registered. POST /remember writes to
\`KV.memories\` keyed by the generated id; GET /memories lists from
the same scope. There was no by-id retrieval handler at all, so any
\`save then fetch-by-ID\` round-trip (including the MCP tool chain's
memory verification step) returned 404 even though the data was in
storage.

Adds \`api::memory-by-id\` with path-param \`:id\`. Returns 200
\`{ memory }\` on hit, 404 \`{ error }\` on miss, 400 if id is empty.
Auth gate matches the rest of the protected endpoints.

Thanks @Axialon for the precise repro and root-cause guess (which
was correct: it was a missing handler, not a scope mismatch).

Closes #196
2026-04-27 00:54:13 +01:00
Rohit Ghumare 8f44c3ddde docs(hermes): correct plugin install path
Hermes scans `~/.hermes/plugins/<name>/` flat. Memory category
namespace (`memory/`) is reserved for bundled providers and won't
discover user plugins nested under it.

Closes #197
2026-04-27 00:53:05 +01:00
Rohit Ghumare 18e3257992 fix(v0.9.3): address CodeRabbit review findings
- cli: use getBaseUrl()-derived viewer URL instead of hardcoded localhost
  (new getViewerUrl() helper; honors AGENTMEMORY_VIEWER_URL + base hostname)
- cli: null-safe graph count (totalNodes || nodes || nodeCount) in status
  + doctor — matches viewer dashboard fallback
- api: auth config-flags via middleware::api-auth + explicit checkAuth
  (leaked provider + version state previously)
- api: use detectEmbeddingProvider() from config instead of hand-rolled
  key check (was missing GEMINI_API_KEY + OPENROUTER_API_KEY; now matches
  config.ts single-source-of-truth)
- api: rename local 'provider' to 'providerKind' to avoid shadowing
  registerApiTriggers() parameter
- api: extract flagDisabledResponse() factory + graphDisabledResponse()
  + consolidationDisabledResponse() helpers; status_code 404 -> 503
  (disabled-by-config != missing route)
- viewer: add --bg-subtle token to both light + dark themes
  (6 call sites previously fell back to transparent)
- viewer: .flag-summary div -> button with aria-expanded + aria-controls
  for keyboard a11y
- viewer: body flex column + .view flex:1 min-height:0 so banner + footer
  sit outside scroll region without clipping view content
- viewer: dashboard + graph tab prefer totalNodes/totalEdges (matches
  actual mem::graph-stats response; pre-existing fallback bug)

Zero behavior change in happy path. Fixes latent correctness + a11y issues.
v0.9.3
2026-04-24 19:53:07 +01:00
Rohit Ghumare cd7a378170 refactor(v0.9.3): simplify cli helpers + viewer banner
- Extract apiFetch() helper to deduplicate fetch/json/catch pattern
- Drop redundant --port re-parse in getRestPort (env already set by outer block)
- Unify safeDesc/safeCode in viewer banner render (single escHtml)
- Extract formatChecks() + DoctorCheck type in runDoctor
- Remove numbered step comments from runDoctor (flow speaks for itself)

Zero behavior change.
2026-04-24 19:53:07 +01:00
Rohit Ghumare 4bd0ceb72e release: v0.9.3 — DX patch (feature-flag visibility + doctor command)
Every disabled feature flag is now visible everywhere: viewer banner, CLI
status, doctor output, and REST error responses. New `agentmemory doctor`
command runs 10 diagnostic checks in one shot, each failure includes a
concrete fix hint. Viewer gains first-run hero pointing at `demo` command
and a footer with preset issue-report link.

Bumps across 8 files per release checklist:
  package.json, plugin/.claude-plugin/plugin.json, src/version.ts,
  src/types.ts, src/functions/export-import.ts, test/export-import.test.ts,
  packages/mcp/package.json, CHANGELOG.md

See CHANGELOG for full details.
2026-04-24 19:53:07 +01:00
Rohit Ghumare 196cbd6343 chore: bump version to 0.9.2 + add CHANGELOG
Rolls up #186 (OPENAI_BASE_URL / OPENAI_EMBEDDING_MODEL), #187 (Stop-hook
recursion 5-layer defense + NoopProvider + AGENTMEMORY_ALLOW_AGENT_SDK opt-in),
#188 (viewer empty-tabs + import-jsonl synthetic compression + auto-derived
lessons/crystals + richer session detail + audit/replay/frontier shape fixes),
#189 (OPENAI_EMBEDDING_DIMENSIONS + model-dimensions table), and #190
(README/website docs refresh).

Bumps: package.json, plugin/.claude-plugin/plugin.json, src/version.ts,
src/types.ts ExportData.version union, src/functions/export-import.ts
supportedVersions, test/export-import.test.ts assertion, and
packages/mcp/package.json shim (was stuck at 0.9.0).
v0.9.2
2026-04-22 16:38:37 +01:00
Rohit Ghumare baa5fec2ca docs: refresh README + website stats after #186/#187/#188/#189
- README provider table: surface the no-op default and call out the
  opt-in Claude-subscription fallback with AGENTMEMORY_ALLOW_AGENT_SDK
  (from #187) instead of listing 'Claude subscription' as the default.
- README env block: document OPENAI_BASE_URL / OPENAI_EMBEDDING_MODEL
  (#186) and OPENAI_EMBEDDING_DIMENSIONS (#189), plus MINIMAX_API_KEY.
- Hero stat-tests SVG: 654 -> 827 (both dark and light variants) to
  match current suite size after recursion guard + idempotent
  lesson/crystal tests + openai dimension tests landed.
- website/lib/generated-meta.json regenerated by
  website/scripts/gen-meta.mjs (v0.9.1, 51 tools, 12 hooks,
  119 endpoints, 848 tests).
2026-04-22 16:23:42 +01:00
Rohit Ghumare 50723e1b52 fix(embedding): derive OpenAI provider dimensions from model
Follow-up to #186. The PR made model configurable via OPENAI_EMBEDDING_MODEL
but left dimensions hardcoded at 1536. A user pointing at
text-embedding-3-large (3072 dims) would see provider.dimensions
return 1536, which vector-store schemas and hybrid-search weights
rely on for correct sizing.

- Add MODEL_DIMENSIONS table: text-embedding-3-small=1536,
  text-embedding-3-large=3072, text-embedding-ada-002=1536.
- Make dimensions a computed readonly field.
- New OPENAI_EMBEDDING_DIMENSIONS env var for custom / self-hosted
  OpenAI-compatible endpoints not in the table. Positive integers only;
  reject non-numeric / non-positive values with a clear error.
- Unknown model names fall back to 1536 with the explicit override
  available if the server returns a different size.
- Tests cover known models, dimension override, unknown-model fallback,
  and validation of the override env var.
2026-04-22 14:00:46 +01:00
Edison-A-N 343b51c06d feat: support OPENAI_BASE_URL and OPENAI_EMBEDDING_MODEL env vars
Signed-off-by: Edison-A-N <zhangn661@gmail.com>
2026-04-22 12:36:56 +01:00
Rohit Ghumare 769d7c4eac Merge pull request #187 from rohitg00/fix/stop-hook-recursion-loop
fix(hooks): break Stop-hook infinite recursion via agent-sdk fallback
2026-04-22 12:32:27 +01:00
Rohit Ghumare 766aaed214 merge main into fix/stop-hook-recursion-loop
Pulls in #184 runtime fix, #188 viewer pipeline, lockfile gitignore,
CI two-step install, StateScope types, firstPrompt typeof guard,
content-addressed lesson/crystal IDs, image-quota fail-closed, and
onnxruntime optionalDependencies. Resolves workflow conflicts flagged
by CodeRabbit — ci.yml / publish.yml on main are already the correct
two-step install pattern.

# Conflicts:
#	.github/workflows/publish.yml
2026-04-22 12:31:30 +01:00