125 Commits

Author SHA1 Message Date
Rohit Ghumare 9061da56d5 chore(release): v0.9.18 (#459)
Hardening + DX wave. Bundles the four follow-up issues filed during
the v0.9.17 audit (#446 timeout env unify, #447 viewer CSP tighten,
#448 fs-watcher PEM/JWT redaction, #449 mcp-standalone probe DI flake)
plus the lessons-auto-inject gap surfaced in discussion #381 / fixed
in #457.

1007/1007 tests pass.

Files bumped:
- package.json
- packages/mcp/package.json
- plugin/.claude-plugin/plugin.json
- plugin/.codex-plugin/plugin.json
- src/version.ts
- src/types.ts (ExportData.version union)
- src/functions/export-import.ts (supportedVersions Set)
- test/export-import.test.ts (assertion)
- CHANGELOG.md (new section)
2026-05-17 14:05:56 +01:00
Rohit Ghumare a557e34f39 fix(context): auto-inject lessons into mem::context payload (#457) (#458)
Lessons were generated (via mem::lesson-save / mem::reflect) and stored
in the lessons KV scope, but never folded into the auto-inject payload
the session-start hook prepends to the first turn. Agents only saw
lessons if they explicitly called memory_lesson_recall — which they
rarely did unprompted. Half the loop: lessons learned, lessons never
reused.

mem::context now reads lessons KV alongside slots + profile, filters
out tombstoned entries + cross-project leakage, ranks project-scoped
lessons above globals (1.5x boost) then by confidence, caps at 10, and
emits a "## Lessons Learned" block sized like every other context
block so the outer token-budget loop drops it cleanly when tight.

Surfaced in #381 (@rockarxiv asked where lessons get used — answer
was "nowhere, automatically"). Filed as #457, fixed here.

- src/functions/context.ts: parallel-fetch lessons with slots +
  profile, render block after profile and before sessions
- test/context-lessons.test.ts: 8 cases covering inclusion, empty
  state, project ranking, cross-project isolation, deleted-skip, top-N
  cap, confidence rendering, optional context-string append

1007/1007 tests pass.
2026-05-17 13:57:26 +01:00
Rohit Ghumare 49377db0db fix(providers): unify OPENAI_TIMEOUT_MS + AGENTMEMORY_LLM_TIMEOUT_MS (#446) (#453)
* fix(providers): unify OPENAI_TIMEOUT_MS + AGENTMEMORY_LLM_TIMEOUT_MS (#446)

v0.9.17 shipped OPENAI_TIMEOUT_MS scoped to the OpenAI LLM provider
(inline AbortController, default 60s). PR #379 then shipped
AGENTMEMORY_LLM_TIMEOUT_MS in the shared src/providers/_fetch.ts helper
used by every other raw-fetch provider (Gemini, OpenRouter, MiniMax,
OpenAI/Cohere/Voyage/OpenRouter embedding).

Two env vars, same value, different names — ops confusion.

Unify on the global name while keeping back-compat:

  1. OPENAI_TIMEOUT_MS         — OpenAI-scoped alias, takes precedence
  2. AGENTMEMORY_LLM_TIMEOUT_MS — global fall-back across providers
  3. 60 000 ms default

The OpenAI LLM provider now routes through the shared fetchWithTimeout
helper, dropping ~30 lines of duplicate AbortController + clearTimeout
plumbing. Existing users with OPENAI_TIMEOUT_MS set keep the exact
v0.9.17 behaviour; new users setting AGENTMEMORY_LLM_TIMEOUT_MS get
the OpenAI LLM path covered too.

README + .env.example now document AGENTMEMORY_LLM_TIMEOUT_MS as the
canonical name and note OPENAI_TIMEOUT_MS as the OpenAI-scoped alias.

4 new precedence tests in test/fetch-timeout.test.ts cover all four
env-var combinations.

* test(providers): strict parse for OPENAI_TIMEOUT_MS env (CodeRabbit)

CodeRabbit caught parseInt("30ms", 10) silently returning 30 in the
timeout-resolve path. Real users hitting this would think they bound
the call to 30ms when the regex would have rejected it.

parsePositiveInt now rejects anything that isn't pure digits via
/^\d+$/ (after trim). parseInt's lenience on trailing units /
underscores / signs is gone — those fall back to the 60s default
instead of masquerading as an aggressive bound.

New regression test covers "30ms", "1_000", "60s", "30abc", "-30",
"0". Whitespace padding (e.g. " 30 ") is still accepted — that's
normal env-var handling.

992/992 tests pass on the worktree.
2026-05-17 12:41:30 +01:00
Rohit Ghumare 00df540c87 fix(fs-watcher): redact PEM private-key blocks and standalone JWTs in previews (#448) (#450)
PR #332 covered KEY=value and Bearer-prefixed tokens. This extends the
filesystem watcher's preview pipeline to also collapse multi-line and inline
PEM private-key blocks (RSA / DSA / EC / OPENSSH / ENCRYPTED / generic) into
a single REDACTED line bracketed by the BEGIN/END markers, and to redact
standalone JWT-looking strings outside Bearer context. JWT redaction requires
a three-segment dot pattern AND total length >= 100 chars to avoid matching
incidental base64-shaped words.

Closes #448
2026-05-17 12:41:14 +01:00
Rohit Ghumare 92e5c885fc fix(mcp): inject livez probe to kill mcp-standalone test flake (#449) (#451)
The MCP standalone shim's REST proxy probed `:3111/agentmemory/livez` via a
hard-coded `fetch()` with a 2s AbortController timeout. In CI, vitest's mock
setup raced the in-flight probe — tests randomly failed on call counts and
response-shape assertions depending on whether the probe completed before
each test's own deadline. This file has been the source of every
"10-11 pre-existing failures unrelated to this PR" footnote for the last
five releases.

Add a `setLivezProbe(fn)` module-level seam in `src/mcp/rest-proxy.ts`
(option A in the issue). Default stays the real fetch-based probe — wire
behaviour is unchanged in production. `resetHandleForTests()` also resets
the probe so tests can't accidentally leak a stub across files.

In `test/mcp-standalone.test.ts`, install an instant `ok:false` probe in
`beforeEach` so the shim always takes the deterministic InMemoryKV fallback
path. Replace `globalThis.fetch` with a trap that throws if hit — any
regression that bypasses the seam fails loudly instead of timing out
silently and re-introducing the flake.

Validation: 10/10 consecutive runs green (~185ms each, down from 2.2s);
proxy test file unaffected (14/14); full suite 988/988 stable across 3 runs.
2026-05-17 12:41:00 +01:00
Rohit Ghumare 57c785f4db fix(viewer): self-host SVG favicon + restore tight img-src CSP (#447) (#452)
#313 added `data:` to the viewer img-src so an inline-SVG favicon
encoded as `data:image/svg+xml,...` could load. Bare `data:` allows
arbitrary data-URI image types (data:image/png;base64,..., etc),
which the viewer does not need.

Self-host the favicon at `/favicon.svg` and revert the CSP to
`img-src 'self'`. Same artwork (dark rounded tile + green "AM"),
served from a real URL with `Content-Type: image/svg+xml` and
`Cache-Control: public, max-age=3600`.

- new file: src/viewer/favicon.svg (extracted from the URL-decoded
  inline blob)
- src/auth.ts: img-src reverted to 'self'
- src/viewer/index.html: favicon link now href="/favicon.svg"
- src/viewer/server.ts: new GET /favicon.svg route, candidate-path
  loader mirrors loadViewerTemplate() so it resolves both from
  source (vitest) and from dist/ (npm run start)
- package.json: build script copies favicon.svg into dist/viewer/
  alongside index.html
- test/viewer-security.test.ts: CSP assertion now requires bare
  `img-src 'self'` and rejects any `data:` allowance; new e2e test
  hits /favicon.svg and verifies SVG payload + headers
2026-05-17 12:40:33 +01:00
Rohit Ghumare c93c715043 fix(test): stub history + removeAttribute in viewer sandbox (#445)
The viewer-session-id sandbox (added in #366) ran fine until #313
landed hash-routing in `switchTab` — `updateTabRoute` now calls
`history.replaceState`, and the aria-selected toggle calls
`removeAttribute` on the non-active tab buttons. Neither was stubbed
in the vm sandbox, so the combined main produced 2 failures:

  TypeError: b.removeAttribute is not a function
  ReferenceError: history is not defined

Both PRs (#366, #313) passed CI in isolation. The combined failure
only surfaced after merge. Stub both globals.

987/987 tests now pass on main.
2026-05-17 11:28:30 +01:00
Mersad Ajanovic 63a0742447 fix: stabilize viewer navigation and timeline layout (#313)
* fix: stabilize viewer navigation and timeline layout

* fix: address viewer review nits
2026-05-17 11:20:49 +01:00
이민재 e2f2217f59 fix: redact filesystem watcher previews before transport (#332)
* fix: redact filesystem watcher previews

* fix: redact quoted sensitive preview keys

---------

Co-authored-by: 이민재 <19909783+honor2030@users.noreply.github.com>
2026-05-17 11:19:36 +01:00
CauchYoung e9f447c80e fix: recover stale sessions before eviction (#387)
* fix: recover stale sessions before eviction

Constraint: preserve recoverable session insights during stale cleanup
Confidence: high
Scope-risk: narrow
Tested: npm test -- test/evict.test.ts --reporter=dot; npx tsdown; git diff --check
Tested: npm test -- --reporter=dot (fails on existing Windows path-sensitive compress-file and obsidian-export tests)
Signed-off-by: laplace young <yangqk12@whu.edu.cn>

* fix: refine stale session recovery guard

Constraint: preserve raw-only stale sessions until a compression recovery path exists
Confidence: high
Scope-risk: narrow
Tested: npm test -- test/evict.test.ts --reporter=dot; npx tsdown; git diff --check
Signed-off-by: laplace young <yangqk12@whu.edu.cn>

---------

Signed-off-by: laplace young <yangqk12@whu.edu.cn>
Co-authored-by: laplace young <yangqk12@whu.edu.cn>
2026-05-17 11:15:06 +01:00
이민재 0ee3e6510e fix(viewer): handle sessions missing ids (#366)
* fix(viewer): handle sessions missing ids

* fix(viewer): address session id review feedback

---------

Co-authored-by: honor2030 <19909783+honor2030@users.noreply.github.com>
2026-05-17 11:08:19 +01:00
Satyam Pratibhan 97ae69a3e4 fix: add AbortController timeout to all raw-fetch providers (#379)
* fix: add AbortController timeout to all raw-fetch providers (closes #373)

Signed-off-by: Satyam Pratibhan <142714564+SATYAM-PRATIBHAN@users.noreply.github.com.>

* fix: validate timeout ms and compose upstream AbortSignal in fetchWithTimeout

Signed-off-by: Satyam Pratibhan <142714564+SATYAM-PRATIBHAN@users.noreply.github.com.>

---------

Signed-off-by: Satyam Pratibhan <142714564+SATYAM-PRATIBHAN@users.noreply.github.com.>
2026-05-17 10:52:13 +01:00
@aaronjmars 3e7b61efd0 fix(security): reject non-loopback Host headers in viewer server (DNS rebinding) (#294)
The viewer (default :3113) binds to 127.0.0.1 and proxies every request to
the local REST API with the AGENTMEMORY_SECRET attached as Bearer auth.
The Origin allowlist defends against a plain cross-origin attacker page,
but it does not defend against DNS rebinding: a victim visiting
attacker.com whose authoritative DNS short-TTL-rebinds to 127.0.0.1 ends
up issuing same-origin fetches to the viewer (origin and target both
http://attacker.com:3113 from the browser's perspective), no preflight
fires, and the proxy adds the secret server-side. The attacker page then
reads every memory the user has stored.

Fix: derive a Host-header allowlist from VIEWER_ALLOWED_ORIGINS plus the
viewer's actual listen port (resolved post-listen so port=0 unit tests
still work) and reject any other Host with 403 before the route logic
runs. An explicit VIEWER_ALLOWED_HOSTS env var is honoured for reverse-
proxy setups. Tests cover the predicate plus an end-to-end http.request
that confirms an attacker-controlled Host header is rejected before the
HTML / proxy paths.

Detected by Aeon + manual review.
Severity: high
CWE-350 (Reliance on Reverse DNS Resolution for a Security-Critical Action)

Co-authored-by: aeonframework <aeon@aaronjmars.com>
2026-05-17 10:15:09 +01:00
Rohit Ghumare 3a3f866ab5 chore(release): v0.9.17 — OpenAI provider + telemetry id + Compare polish (#432)
Patch bump per the established rule: additive surface only. OpenAI
provider is a new optional surface that activates only when
OPENAI_API_KEY is set, gated by OPENAI_API_KEY_FOR_LLM. Telemetry
project_name pin is pure observability metadata. Compare polish is
docs/website only.

PRs included since v0.9.16:
  #307 — OpenAI-compatible LLM provider (universal adapter for
         OpenAI, Azure OpenAI auto-detected by hostname, DeepSeek,
         SiliconFlow, vLLM, LM Studio, Ollama). Plus the maintainer-
         pushed Azure detection + fetch timeout + README scope hint
         follow-ups. Closes #185, #232, #312, supersedes #240.
  #426 — pin worker telemetry project_name
  #427 — Compare section polish (title + native plugins cell + grid)

Files bumped (9):
  package.json, packages/mcp/package.json, plugin/.claude-plugin/plugin.json,
  plugin/.codex-plugin/plugin.json, src/version.ts, src/types.ts,
  src/functions/export-import.ts, test/export-import.test.ts,
  CHANGELOG.md
2026-05-16 20:36:24 +01:00
이민재 705321be34 docs: refresh current project stats (#330)
* docs: refresh current project stats

* docs: update REST endpoint badge count

* docs: refresh stats for v0.9.16

---------

Co-authored-by: 이민재 <19909783+honor2030@users.noreply.github.com>
2026-05-16 10:00:00 +01:00
이민재 b48176cacd fix(mcp): use DELETE for governance memory deletion proxy (#423)
Co-authored-by: 이민재 <19909783+honor2030@users.noreply.github.com>
2026-05-16 09:59:08 +01:00
Rohit Ghumare b7e1240d06 chore(release): v0.9.16 — DevEx polish + website refresh (#417)
Patch bump per the established rule: additive surface only, no breaks
to MemoryProvider trait, exported types, or default behaviour. New
top-level subcommands (`--reset` already shipped 0.9.15, no new
commands here) are opt-in.

PRs included since v0.9.15:
  #408 — onboarding wires selected agents inline + memory-share callout
  #409 — clarify MCP is opt-in (REST primary)
  #410 — 5-port ready panel, iii console install, global-install prompt
  #411 — splash banner rerender + README install hoist + npx caveat
  #415 — agent-memory.dev refresh (FeaturedIn bar + Agents/Compare/
         CommandCenter/Hero updates)

Files bumped (9):
  package.json, packages/mcp/package.json, plugin/.claude-plugin/plugin.json,
  plugin/.codex-plugin/plugin.json, src/version.ts, src/types.ts,
  src/functions/export-import.ts, test/export-import.test.ts,
  CHANGELOG.md
2026-05-15 19:58:24 +01:00
Rohit Ghumare c3a613aed8 chore(release): v0.9.15 — DevEx overhaul (splash, connect, doctor v2, remove, silent killers) (#407)
Patch bump per the established rule: all changes additive, no breaks
to MemoryProvider trait or exported types or default behaviour. New
top-level subcommands (connect, remove, --reset, --force) are opt-in.

PRs included since v0.9.14:
  #405 — silent killers: viewer port auto-bump, engine version-match
         warning, stop --force, adopt-on-attach, npx PATH hint
  #402 — agentmemory connect — automate native-plugin install for 8
         agents (claude-code, codex, cursor, gemini-cli, openclaw
         end-to-end; hermes/pi/openhuman stubbed)
  #406 — interactive doctor v2 + agentmemory remove (destruction plan
         + two-confirmation flow)
  #403 — splash banner + agent grid + provider picker + smart-defaults
         preferences + bootLog shim (30+ lines of log spam → 10)

Files bumped (9):
  package.json, packages/mcp/package.json, plugin/.claude-plugin/plugin.json,
  plugin/.codex-plugin/plugin.json, src/version.ts, src/types.ts,
  src/functions/export-import.ts, test/export-import.test.ts,
  CHANGELOG.md
2026-05-15 16:43:06 +01:00
Rohit Ghumare 659d0c3e1a feat(cli): splash banner, agent grid, provider picker, smart defaults (#403)
Replaces the 25-line `[agentmemory] X` log spam on first invocation
with a SkillKit-grade onboarding flow:

* `src/cli/splash.ts` — terminal-width-aware ASCII banner with three
  tiers (>=120 / 80-119 / <80 cols), brand-accent orange via ANSI
  256-colour, NO_COLOR-aware, no new deps.
* `src/cli/preferences.ts` — JSON-backed prefs at
  `~/.agentmemory/preferences.json`, atomic write via tmp+fsync+rename,
  graceful defaults on corrupt/missing file, `isFirstRun()` /
  `resetPrefs()` helpers. 7 unit tests added.
* `src/cli/onboarding.ts` — first-run multi-select for agents (native
  plugin + MCP-server rows from the README), single-select for
  provider (anthropic / openai / gemini / openrouter / minimax / skip
  -> BM25-only), seeds `~/.agentmemory/.env`, prints the next-step
  hint.
* `src/logger.ts` — adds `bootLog` / `setBootVerbose`. The 18 startup
  `console.log` calls in `src/index.ts` route through this shim;
  default is muted, `--verbose` (or AGENTMEMORY_VERBOSE=1) restores
  the old chatty output. Warnings, errors, and shutdown logs stay
  loud.
* `src/cli.ts` — splash before clack intro, run onboarding when
  `isFirstRun()` or `--reset`, propagate verbosity to the worker
  logger, wait for `/agentmemory/livez` after `import("./index.js")`
  and print a single-line ready hint. Adds `--reset` flag.

First-run output drops from 30+ lines to ~10. Subsequent runs are a
single ready line. `--verbose` restores the old verbose surface for
debugging. Build passes; the 10 pre-existing failures in
`test/mcp-standalone.test.ts` are unchanged (903 -> 910, all 7 new
passes are `test/preferences.test.ts`).
2026-05-15 16:38:09 +01:00
Rohit Ghumare 29e8865286 feat(cli): interactive doctor v2 + agentmemory remove (#406)
* feat(cli): interactive doctor v2 with inline fixes

Replace passive doctor with an interactive [F]ix/[S]kip/[?]more/[Q]uit
prompt per diagnostic. Each check now ships a one-line problem, cause,
fix-preview, and inline apply() — and we re-check only the affected
diagnostic after a fix, not the whole suite.

New flags:
  doctor --all        apply every fix without prompting (CI)
  doctor --dry-run    show what each fix would do, run nothing

Catalog (src/cli/doctor-diagnostics.ts):
- env-missing                     → runInit()
- no-llm-provider-key             → open .env in $EDITOR
- engine-version-mismatch         → runIiiInstaller + restart
- viewer-unreachable              → stop + restart engine
- stale-pidfile                   → clear pid + state, restart
- env-placeholder-keys            → open .env in $EDITOR
- iii-on-path-not-local-bin       → manual (suggest installer)

The catalog is exported as a pure data structure so tests can assert
each entry has check/fix/message without booting clack.

Passive server checks (graph populated, Claude Code hooks, flags) still
run first — they need a live engine and have no one-shot inline fix.

* feat(cli): add agentmemory remove for clean uninstall

New command tears down everything agentmemory installs:
- ~/.agentmemory/iii.pid + engine-state.json (after stopping engine)
- ~/.agentmemory/.env (asks separately — holds API keys)
- ~/.agentmemory/preferences.json
- ~/.agentmemory/backups/ (connect-PR manifest + backups)
- ~/.local/bin/iii (only when iii --version matches our pin)
- Any agent symlinks from the connect-manifest
- ~/.agentmemory/data/ (asks separately, default keep)

Surface:
  agentmemory remove              # interactive, double-confirms
  agentmemory remove --force      # skip generic confirms
  agentmemory remove --keep-data  # remove binaries + symlinks only

Plan building lives in src/cli/remove-plan.ts as a pure fs-inspecting
function so tests sandbox a fake $HOME under tmpdir() and assert on
plan shape without touching the real home directory.

Always-ask items (.env, memory data, mismatched-version local-bin iii)
still get per-item confirmation even with --force.
2026-05-15 16:20:24 +01:00
Rohit Ghumare b36a6d7c73 feat(cli): add agentmemory connect to automate native-plugin install for 8 agents (#402)
agentmemory connect <agent> wires agentmemory into each supported coding agent: detect install, back up existing config to ~/.agentmemory/backups/, merge agentmemory MCP entry, verify, idempotent on re-run.

Working: claude-code, codex, cursor, gemini-cli, openclaw. Stubbed with manual-install hints: hermes (YAML), pi (TS extension copy), openhuman (no integrations folder). Windows prints manual-install message and exits.

Argv: agentmemory connect [agent] [--all] [--dry-run] [--force]
2026-05-15 16:15:49 +01:00
Rohit Ghumare 372c6a6bbb chore(release): v0.9.14 — CLI installer first + agentmemory stop + Docker-aware teardown (#401)
Patch bump. No breaking changes to the public API or the existing CLI
subcommands. CLI bootstrap flow changes are additive; existing Docker
users keep working via the new AGENTMEMORY_USE_DOCKER=1 opt-in.

Files bumped (9):
  - package.json
  - packages/mcp/package.json
  - plugin/.claude-plugin/plugin.json
  - plugin/.codex-plugin/plugin.json
  - src/version.ts
  - src/types.ts (ExportData.version union — also adds 0.9.13
    which was missed in the previous release)
  - src/functions/export-import.ts (supportedVersions Set)
  - test/export-import.test.ts
  - CHANGELOG.md

PRs included since v0.9.13:
  #396 — feat(cli): install iii-engine binary first, Docker opt-in;
         agentmemory stop with Docker-aware teardown; LISTEN-only
         lsof filter so the CLI no longer signals its own parent.
  #397 — docs(readme): move OpenHuman after pi in agents grid.
2026-05-15 15:11:47 +01:00
Rohit Ghumare 8b98432853 chore(release): v0.9.13 — env-example + init + CJK + load bench + deploy templates + Gemini GA bumps (#383)
* chore(release): v0.9.13 — env-example discovery + CJK tokenizer + load
harness + deploy templates + Gemini GA bumps + 14 advisories closed

Six PRs landed since v0.9.12:

- #372 .env.example discovery (this commit) — repo-root template + `init`
  CLI command + CI sync-checker
- #362 CJK BM25 tokenizer (`@node-rs/jieba` + tiny-segmenter + Hangul)
- #363 `benchmark/load-100k.ts` harness with p50/p90/p99 + per-release
  results dir
- #361 one-click deploy templates for fly.io / Railway / Render / Coolify
  (multi-stage Dockerfile, `iiidev/iii` base, `gosu` privilege drop,
  first-boot HMAC, verified end-to-end on fly.io)
- #364 Python ecosystem via `iii-sdk` example (replaces closed PR #360)
- #370 Gemini GA bumps (LLM default → gemini-2.5-flash, embedding →
  gemini-embedding-001 + L2-norm + 768 dims)

Plus 14 open Dependabot advisories closed in PR #348 via Next.js → 16.2.6
and PostCSS → 8.5.10 overrides.

Bumped:

- src/version.ts: VERSION 0.9.12 → 0.9.13
- package.json: 0.9.12 → 0.9.13, files += ".env.example", build script
  copies .env.example into dist/
- packages/mcp/package.json: 0.9.12 → 0.9.13 (lockstep with main)
- plugin/.claude-plugin/plugin.json, plugin/.codex-plugin/plugin.json:
  0.9.12 → 0.9.13
- src/types.ts: ExportData.version union extended with "0.9.13"
- src/functions/export-import.ts: supportedVersions Set extended
- test/export-import.test.ts: expected version updated

New surface:

- .env.example at repo root — every env var read by src/ documented in
  one place, grouped by surface (LLM, embedding, auth, search tuning,
  behaviour flags, CLI runtime, ports, iii engine pin, Claude Code
  bridge, Obsidian export). Every line commented out by default so the
  file is a template.
- agentmemory init — copies bundled .env.example to ~/.agentmemory/.env
  if absent, refuses to overwrite, prints a diff command. Wired into
  CLI dispatch + help block.
- scripts/check-env-example.mjs — walks src/ for env-read patterns,
  fails CI on drift in either direction. Plugged into ci.yml after
  npm test. Initial bootstrap: 60 keys in sync.

Verified: npm test 903/903, npm run build clean, init smoke pass
(creates ~/.agentmemory/.env on first run, refuses overwrite on second).

* fix(init): atomic copy via COPYFILE_EXCL; address CodeRabbit review

Two valid findings from the CodeRabbit pass on PR #383.

1. `runInit` race between existsSync(target) + copyFile(template, target).
   A parallel `agentmemory init` (or any other process touching
   ~/.agentmemory/.env between the two calls) would silently overwrite
   the config the operator just wrote. Switch to a single atomic
   `copyFile(template, target, fsConstants.COPYFILE_EXCL)` and treat
   the EEXIST error as the "already configured" signal — same warning
   + diff hint as before, but the check + copy now happen in one
   syscall so they cannot race. Other failure paths still surface as
   process exit 1.

2. Comment on `scripts/check-env-example.mjs::walk` claimed it matched
   ".ts / .mts / .mjs" but the regex also matched ".js". Rewrote the
   comment to match the regex (".ts / .mts / .mjs / .js"). Same comment
   pass: noted that test/ never enters because the walk is rooted at
   src/, not because of an explicit skip.

Skipped findings:

- WHAT-style comment on `findEnvExample` — kept a one-liner explaining
  the package-vs-source priority since both paths are real; reduced
  the block from 4 lines to 2 instead of removing it entirely.
- "Add trailing newline to .env.example" — file already ends with `\n`
  (verified `tail -c 5` shows `tion\n`).

Verified locally:
- `npm run build` clean.
- `npm test` 903 / 903 pass.
- First `agentmemory init` against a clean HOME creates the file.
- Second init against the same HOME hits EEXIST and prints the
  "leaving it untouched" warning + diff hint without overwriting.
- `node scripts/check-env-example.mjs` — in sync (60 keys).
2026-05-15 10:11:37 +01:00
Rohit Ghumare 8c3418cc61 feat(search): CJK tokenizer for BM25 (#362)
* feat(search): CJK tokenizer for BM25 (closes #344)

BM25 previously tokenized Chinese, Japanese, and Korean memories as a
single sentence-long token because CJK scripts don't put spaces between
words, leaving the keyword leg of hybrid search as dead weight for CJK
users. Greek, Cyrillic, accented Latin already work since v0.9.12 (#327)
because they split cleanly on whitespace.

Detect CJK by Unicode block, then route per-run:
- Han: @node-rs/jieba (native, no model download), Jieba.cut(text, hmm)
- Kana (with mixed kanji): tiny-segmenter (pure JS, ~25KB)
- Hangul: rule-based, each [가-힯]+ run is one token

Both segmenters declared in optionalDependencies so the bundle stays
small for users who don't need them. If the dep isn't installed the
segmenter soft-falls to whole-run tokenization (the pre-fix behavior)
and prints a one-time stderr hint per language. Non-CJK input goes
through the existing tokenize path unchanged — zero behavior change for
the Latin/Greek/Cyrillic/Hebrew/Arabic paths.

New fixtures in test/search-index.test.ts cover Chinese (项目记忆存储),
Japanese (プロジェクト記憶), and Korean (프로젝트 메모리 저장소). Existing
Greek + ASCII tests stay green. Suite grew 899 -> 902 tests.

* fix(cjk): preserve source order across mixed CJK + non-CJK runs

The previous segmentCjk walked the input three times — once for Han/kana
runs, once for Hangul runs, once for non-CJK pieces — and concatenated
the results. For a mixed string like "abc 메모리 def 项目 ghi" that
emitted ["메모리","项目","abc","def","ghi"], dropping the original
positional information BM25 phrase queries and any downstream consumer
keyed on token order rely on.

Replace with a single left-to-right pass over CJK_RUN_RE: emit any
intervening non-CJK substring (trimmed), then route the matched CJK run
to segmentHangul / segmentKana / segmentHan based on which script it
contains, and finally emit any trailing non-CJK piece.

Added a regression test asserting source order across four mixed-script
strings. Drops the now-unused HAN_KANA_RUN_RE / HANGUL_RUN_RE regex
constants.
2026-05-13 22:20:24 +01:00
Rohit Ghumare 87fae50b79 chore(release): v0.9.12 — BM25 unicode + vector live-write + viewer hardening (#337)
Three landed PRs since v0.9.11:
  - #327 (#295) — BM25 tokenizer now accepts non-ASCII (Greek,
    accented Latin, Hebrew, Arabic, Cyrillic), VectorIndex.add now
    actually called at runtime via vectorIndexAddGuarded helper with
    dim guard + input clip, migrateVectorIndex for dim migrations.
  - #326 (#277) — RetentionScore type no longer declares source
    twice; JSDoc back-compat note no longer shadowed.
  - #335 (#323) — viewer drops Google Fonts <link> (CSP-blocked),
    loadDashboard now surfaces load errors inline instead of
    sticking on "Loading dashboard…".

Bumping 0.9.11 -> 0.9.12 across the 9 standard files:
- package.json
- packages/mcp/package.json
- plugin/.claude-plugin/plugin.json
- plugin/.codex-plugin/plugin.json
- src/version.ts
- src/types.ts (ExportData.version literal)
- src/functions/export-import.ts (supportedVersions)
- test/export-import.test.ts (round-trip expectation)
- CHANGELOG.md (new 0.9.12 entry)

886 / 886 tests pass. Build clean.
2026-05-13 16:17:01 +01:00
Matt Van Horn 1a706ca2d6 fix(integrations): warn when bearer auth crosses plaintext HTTP to non-loopback (#315)
* fix(integrations): warn when bearer auth crosses plaintext HTTP to non-loopback (#275)

Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>

* test(integrations/315): edge cases for IPv6 loopback + LAN IPs + lookalike hostnames + docs

@mvanhorn's PR #315 lands the right plaintext-bearer-auth warn-once
pattern across three runtimes (hermes/python, openclaw/mjs,
pi/typescript). Reviewed cleanly. Adding test cases + docs before
merge so the contract is locked:

Test additions (test/integration-plaintext-http.test.ts pi suite,
+4 cases, 13 total in suite):

- IPv6 loopback (http://[::1]:3111) — URL parser strips brackets,
  hostname comes back as `::1`, the LOOPBACK_HOSTS set already covers
  this. Test pins the parsing semantic so a future refactor can't
  silently break it.

- Private LAN IPs (RFC1918 ranges 192.168.x.y, 10.x.x.x) — NOT
  loopback, guard MUST warn. RFC1918 was a deliberate design choice
  (loopback set stays minimal: localhost / 127.0.0.1 / ::1). LAN
  deployment behind a tunnel still leaks the bearer over the wire.

- No-secret short-circuit (guard with secret="" or undefined) — guard
  never fires when a bearer wouldn't actually be sent. Mirrors the
  usesPlaintextBearerAuth() early-return.

- Lookalike hostname (http://localhost.evil.com:3111) — `parsed.hostname`
  returns the full `localhost.evil.com` string, which is NOT in
  LOOPBACK_HOSTS. Guard warns. This pins the exact-match (vs prefix)
  loopback semantic.

Docs additions:

- integrations/pi/README.md and integrations/hermes/README.md now
  document AGENTMEMORY_REQUIRE_HTTPS in the env-vars table — same row
  shape as AGENTMEMORY_SECRET, names the env value (=1), describes the
  warn-vs-throw semantic, and lists the loopback hostnames the guard
  recognises.

No code changes to the guard itself — it's already correct. Pre-merge
hardening only.

899 / 899 tests pass on rebased branch. Build clean.

---------

Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Rohit Ghumare <ghumare64@gmail.com>
2026-05-13 15:59:50 +01:00
Rohit Ghumare 24f6c5fb24 fix(search): BM25 unicode + vector index live-write — improved (closes #295, supersedes #296) (#327)
* fix #2: BM25 tokenizer now preserves Cyrillic/Unicode via \p{L}\p{N} regex

Signed-off-by: Nikita Nosov <20nik.nosov21@gmail.com>

* fix #1: add getVectorIndex/setVectorIndex + vector embedding in rebuildIndex

Signed-off-by: Nikita Nosov <20nik.nosov21@gmail.com>

* fix #1+#3: vectorIndex.add in remember/observe/compress + setVectorIndex in index.ts

Signed-off-by: Nikita Nosov <20nik.nosov21@gmail.com>

* fix: add migration script, Cyrillic BM25 tests, vector-populate tests, rebuildIndex vector test

Adds migrateVectorIndex utility for re-embedding when embedding provider dimensions change. Adds comprehensive test coverage for Cyrillic tokenization, vector index population on remember/rebuildIndex, and migration edge cases.

Signed-off-by: Nikita Nosov <20nik.nosov21@gmail.com>

* chore: remove internal research spec from PR branch

Signed-off-by: Nikita Nosov <20nik.nosov21@gmail.com>

* refactor(search): centralize vector-index writes via vectorIndexAddGuarded

Pre-merge hardening on top of @nik1t7n's PR #296.

Original PR had vectorIndex.add() inlined at four sites (remember,
observe, compress, rebuildIndex) with the same try/catch shape repeated
each time. That open-codes three sharp edges per site:

1. Missing dimension guard at the write boundary. Per #248, persistence
   load already refuses to start when persisted vectors mismatch the
   active provider's dimensions — but the live-write path had no
   symmetric check. A mis-configured embedding provider returning a
   different-length Float32Array would silently corrupt the in-memory
   index until next restart cleared it.

2. No input clipping. Memory.content can be arbitrarily large (the
   MemorySlot path even allows 20KB content per slot). Most embedding
   providers cap input around 8k tokens and 400 on overflow. A single
   oversize memory could throw at the provider and lose the vector
   entry; worse, on local-Ollama-style providers it can cost real
   inference time.

3. Inconsistent error logging (rebuildIndex had `catch {}` swallowing
   the error entirely, the other three sites logged via logger.warn
   with slightly different shape).

Consolidates into a single exported `vectorIndexAddGuarded()` in
src/functions/search.ts:

  - Reads vectorIndex + currentEmbeddingProvider from module-level
    state (same singletons the PR added).
  - Clips embed input to 16k chars (EMBED_MAX_CHARS) — well under
    every provider's documented limit, leaves headroom for
    chars-per-token variance across languages.
  - Validates embedding.length === provider.dimensions before
    calling vi.add(); logs structured warning and skips on mismatch.
  - Single logger.warn on embed failure with provider name + error
    message + indexed-item kind ("memory" | "observation" |
    "synthetic") + id.
  - Returns boolean (true on success) so callers can branch if they
    want, but every existing call site fire-and-forgets — the contract
    is "soft-fail: index entry skipped, upstream save still succeeds".

Rewrites the four call sites (remember.ts, observe.ts synthetic
branch, compress.ts post-LLM, search.ts rebuildIndex memories + obs
loops) to call vectorIndexAddGuarded() instead of the inline try/catch.

Side effect: cleans up the lingering `catch {}` in rebuildIndex that
was swallowing rebuild-time embed failures silently — every failure
now surfaces in logs at a consistent shape.

Tests:
- test/multimodal.test.ts mocked `../src/functions/search.js` and
  exposed only getSearchIndex; needed vectorIndexAddGuarded added to
  the mock surface or test fails with "No vectorIndexAddGuarded export
  is defined on the search.js mock". Added the export to the mock.
- All other tests (search-index Cyrillic, search rebuildIndex vector,
  vector-index-populate, migrate-vector-index) green on the helper
  refactor — no behavioural change beyond the three guards above.

886 / 886 tests pass. Build clean.

Out-of-scope (deferred to follow-ups):
- Blocking await on every save. The embed call is on the request hot
  path. For local Ollama / on-device providers this is fine; for
  network providers it adds 200ms-2s latency per remember/observe.
  Should be async / fire-and-forget with a write-behind queue —
  separate perf PR after this lands.
- Periodic persistence flush of the vector index. Today the index is
  serialized at process exit (per IndexPersistence). Runtime adds
  between flushes survive in memory but die on crash. Acceptable for
  this PR; future PR can add a debounced flush.
- Batching in rebuildIndex. Today the rebuild path embeds one doc at
  a time. embedBatch() exists on every provider and would be ~5-10x
  faster for cold restarts on large corpora. Defer.

* fix(search): tighter migrate guard + rebuild vector clear + test hygiene

Five reviewer findings applied. Five skipped with reason:

APPLIED:

1. migrate-vector-index.ts filter now requires m.content (and rejects
   empty/whitespace). Without this, undefined m.content would concat
   to "title undefined" and poison the embed input. Memory.content is
   declared string but #277-era data shapes had cases where it could
   be missing — defensive filter.

2. migrate-vector-index.ts success expression now derives from the
   failed counter (success: failed === 0). Previously hardcoded true
   so callers couldn't rely on the boolean alone.

3. search.ts rebuildIndex now calls vectorIndex?.clear() before the
   repopulation loops. Symmetric to the BM25 idx.clear() that was
   already there. Without this, memories deleted between runs leave
   orphan embeddings in the vector store after every rebuild.

4. test/vector-index-dimensions.test.ts assertion tightened from
   toBeGreaterThan(0) to toBe(1) for both totalProcessed and
   vectorSize. The seeded fixture has exactly one observation; loose
   assertion would hide regressions where the migration produced
   extra/duplicate vectors.

5. test/vector-index-populate.test.ts switched to beforeEach/afterEach
   for the setVectorIndex / setEmbeddingProvider lifecycle. Manual
   cleanup in each test body was easy to drift on additions.

PLUS:

6. compress.ts BM25 add now wrapped in try/catch matching the
   remember.ts pattern for parity. Helps cross-file symmetry even
   though SearchIndex.add doesn't throw on valid input.

SKIPPED (with reason):

- Wrapping vectorIndexAddGuarded calls in additional try/catch at
  remember.ts / observe.ts / compress.ts: the helper has internal
  try/catch (src/functions/search.ts:67-89). It NEVER throws. Adding
  outer try/catch is dead code. The reviewer missed the helper's
  contract.
- vector-index-dimensions.test.ts mockKV.delete -> real deletion: the
  tests in that file don't exercise deletion semantics, and
  migrateVectorIndex itself never calls kv.delete. Touching it would
  add noise without effect on any assertion.
- recordAudit + Promise.allSettled in remember.ts: out of scope.
  recordAudit is already called earlier in the function. Parallelizing
  cascade-update with vector-add changes failure semantics (cascade-
  update is currently fire-and-forget) — separate perf PR if wanted.
- validateDimensions(N) assertion in vector-index-dimensions.test.ts:
  vectorSize is the COUNT of stored vectors, not their dimensions.
  Adding a validateDimensions check on the result would require
  exposing the new index. Out of scope for assertion tightening.

886 / 886 tests pass. Build clean.

* fix(migrate): per-session isolation + dim guard + drop unused oldIndex param

Four reviewer findings applied. One skipped with reason:

APPLIED:

1. Per-session isolation in migrateVectorIndex observations phase. The
   previous shape wrapped the entire sessions for-loop in one try/catch
   — a single bad session (kv.list throws, embedBatch rejects, etc.)
   aborted every subsequent session and silently truncated the
   migration. The loop now has per-session try/catch; failures
   increment `failed`, append the session id to a new `failedSessions`
   array on the result, and the loop continues. The kv.list<sessions>
   call itself is now guarded separately — if THAT fails the whole
   migration aborts (no sessions to iterate), but with `failed`
   recorded and `failedSessions` empty so the caller can distinguish.

2. Drop `oldIndex` parameter from migrateVectorIndex signature. It was
   dead — no read path inside the function. Three test call sites
   updated to match the new signature
   migrateVectorIndex(kv, newProvider). Confused the API contract; if
   we ever DO need a previous-index argument it can come back with a
   real use case.

3. Dim guard on every embedding result inside migrateVectorIndex, in
   both the memories phase and the per-session observations phase.
   Extracted into a local isValidEmbedding() helper that mirrors the
   shape of search.ts::vectorIndexAddGuarded's guard: log structured
   warn + skip on mismatch. Without this, a misconfigured provider
   returning the wrong-length Float32Array would corrupt the rebuilt
   index, defeating the #248 persistence-load guard.

4. Rename `vi` -> `vectorIndex` in test/vector-index-populate.test.ts
   so the let binding doesn't shadow the vitest `vi` import. Practical
   impact: future tests in that file can use `vi.fn()` / `vi.spyOn()`
   without renaming this binding first. Cosmetic but cheap.

SKIPPED (with reason):

- Use embedBatch in rebuildIndex (search.ts): perf optimization, not
  correctness. Already called out as deferred in the previous review's
  commit message. Cold-restart speed matters but the fix here is
  bigger refactor (collect-into-batches with metadata, validate each,
  add only successful) and worth its own PR. Will pick up when we
  also do the write-behind queue for live-write batching.

Result envelope:
  Added `failedSessions: string[]` to MigrateVectorIndexResult so the
  caller can decide whether to retry per-session or just abort.
  Existing fields (success, totalProcessed, failed, vectorSize)
  unchanged in shape.

886 / 886 tests pass. Build clean.

* fix(migrate): attribute batch-level failures to correct count + distinguish list-fail

Two new reviewer findings on migrate-vector-index. Six earlier findings
re-flagged in the same review block had already been addressed in prior
commits on this branch (verified inline):

  - compress.ts BM25 try/catch (commit d2098a5)
  - migrate filter requires m.content (commit d2098a5)
  - migrate success: failed === 0 (commit d2098a5)
  - rebuildIndex vector clear (commit d2098a5)
  - observe.ts / remember.ts outer try/catch — SKIPPED with reason
    (vectorIndexAddGuarded has internal try/catch, NEVER throws)
  - mockKV.delete no-op — SKIPPED with reason (no test exercises it)

NEW FINDINGS APPLIED:

1. Catch block in memories phase counted +1 for the entire batch
   regardless of batch size. If kv.list returned 250 memories and
   embedBatch threw, `failed` reflected 1 missed embedding when
   reality is 250.

   textMems is now declared outside the try so the catch can read its
   length. If kv.list itself threw before textMems was populated
   (length 0), we fall back to +1 (something failed but the batch
   size is unknown). If embedBatch threw, `failed += textMems.length`
   so the counter is honest.

2. kv.list<sessions> failure path returned `failed: 1,
   failedSessions: []`. Indistinguishable from "0 sessions, all OK"
   from the caller's perspective — they can't tell whether the
   enumeration itself blew up or simply found no sessions.

   The path now pushes a marker string "<sessions-list-failed>" into
   failedSessions before returning. Marker keeps the existing schema
   (failedSessions: string[]) — no boolean flag added — and gives
   callers a sentinel they can grep for. Per-session entries continue
   to use real session ids, so the marker is unambiguous.

886 / 886 tests pass. Build clean.

* test: swap Cyrillic non-ASCII tokenizer fixtures for Greek

The two regression tests added in this branch for the BM25 non-ASCII
tokenizer fix used Russian-language fixtures
("Проверка памяти", "Тестируем поиск по кириллице", etc). Switching
to Greek-language equivalents ("Προβολή μνήμης",
"Δοκιμάζουμε αναζήτηση σε ελληνικά", etc).

Reasoning: the test fixtures need to exercise the same code path —
non-ASCII Unicode letters that the old ASCII-only \w regex stripped.
Greek code points (U+0370..U+03FF) hit \p{L} identically to Cyrillic
(U+0400..U+04FF), so the regression coverage is preserved. Greek is
also less likely to be misread as politically loaded content in a
public repo.

Test ids renamed from obs_cyrillic to obs_greek; test names from
"indexes and finds Cyrillic text" to "indexes and finds non-ASCII
(Greek) text" (and the mixed-script variant likewise). Branch name
stays as-is to keep PR continuity but the shipped test fixtures are
language-neutral going forward.

11/11 search-index tests pass with the swap. Full suite 886/886.

---------

Signed-off-by: Nikita Nosov <20nik.nosov21@gmail.com>
Co-authored-by: Nikita Nosov <20nik.nosov21@gmail.com>
2026-05-13 15:27:43 +01:00
Rohit Ghumare 25dddc4379 chore(release): v0.9.11 — Codex plugin platform + OpenClaw slot fix + website star button (#317)
Three landed PRs since v0.9.10:
- #311 — Codex plugin support (manifest + marketplace + Codex-shaped
  hooks subset, sharing the same scripts as the Claude Code plugin
  via CLAUDE_PLUGIN_ROOT OOTB-compat injection on Codex's hook
  engine)
- #310 — OpenClaw plugins.slots.memory now actually claimed via
  api.registerMemoryCapability({ promptBuilder }); older builds
  without the capability API still load via the existing hook-only
  path
- #316 — Star on GitHub button in the website hero CTA row, live
  stargazer count via api.github.com, 30-minute localStorage cache,
  graceful degrade on API failure

Bumping 0.9.10 -> 0.9.11 across the 8 standard files:
- package.json
- packages/mcp/package.json
- plugin/.claude-plugin/plugin.json
- plugin/.codex-plugin/plugin.json
- src/version.ts
- src/types.ts (ExportData.version literal)
- src/functions/export-import.ts (supportedVersions)
- test/export-import.test.ts (round-trip expectation)
- CHANGELOG.md (new 0.9.11 entry)

877 / 877 tests pass. Build clean.
2026-05-13 00:21:27 +01:00
Rohit Ghumare 48b11e8351 fix(openclaw): claim plugins.slots.memory via registerMemoryCapability (#310)
* fix(openclaw): claim plugins.slots.memory via registerMemoryCapability

The OpenClaw integration declared kind: "memory" in its manifest and
wired before_agent_start / agent_end hooks, but never called
api.registerMemoryCapability(...). On OpenClaw 2026.4.9+ that
left plugins.slots.memory = "agentmemory" reporting unavailable
even though the hooks worked end-to-end and the REST API was
healthy.

Per OpenClaw's plugin SDK declarations
(plugin-sdk/src/plugins/types.d.ts), api.registerMemoryCapability
takes a MemoryPluginCapability and is the canonical way to occupy
the memory slot. All capability fields are optional. The
promptBuilder field has signature:
  (params: { availableTools: Set<string>; citationsMode? }) => string[]

This commit:

- Calls api.registerMemoryCapability({ promptBuilder }) at register
  time when the host exposes it. promptBuilder returns three lines
  describing agentmemory as the active memory provider, including
  the configured base_url so the agent knows where recall is
  coming from.
- Guards on `typeof api.registerMemoryCapability === "function"`
  so older OpenClaw builds (pre-capability API) still load via
  the existing hook-only path. Net behaviour on older hosts is
  unchanged.
- Does NOT register a runtime adapter. OpenClaw's current
  MemoryRuntimeBackendConfig type is exactly { backend: "builtin" }
  or { backend: "qmd"; qmd?: { command?: string } }; both are
  in-process backends and don't fit an external REST service.
  The hook-driven recall + capture flow is the working
  integration path; runtime registration is documented as a
  follow-up gated on an upstream OpenClaw change to add an
  "external" backend variant.

README troubleshooting block adds a specific entry for the
"unavailable slot" symptom, pointing at the registerMemoryCapability
fix. The README also documents the runtime-adapter scope decision
so users running deep host integrations aren't surprised.

3 new tests in test/openclaw-plugin.test.ts assert:
1. registerMemoryCapability is called with a promptBuilder when
   the host supports it.
2. Plugin still registers hooks and does not throw on older hosts
   that don't expose registerMemoryCapability.
3. promptBuilder respects the configured base_url.

871 / 871 tests pass.

Verified against the openclaw npm package surface:
- api.registerMemoryCapability is declared at
  plugin-sdk/src/plugins/types.d.ts:2028
- MemoryPluginCapability is declared at
  plugin-sdk/src/plugins/memory-state.d.ts
- promptBuilder signature matches MemoryPromptSectionBuilder
- No import from openclaw/extensions/* needed (and that path is
  not in the package's exports map anyway)

This is a fresh implementation. The closed PR #302 attempted the
same end goal but imported memoryRuntime from a non-existent
path (openclaw/extensions/memory-core/runtime-api) and would have
failed with ERR_PACKAGE_PATH_NOT_EXPORTED at load time.

* fix(openclaw): accept params arg in promptBuilder to match MemoryPromptSectionBuilder

Reviewer flagged that promptBuilder was declared as () => [...] which
doesn't match the MemoryPluginCapability contract verified from
openclaw@2026.5.7 (plugin-sdk/src/plugins/memory-state.d.ts):

  type MemoryPromptSectionBuilder = (params: {
    availableTools: Set<string>;
    citationsMode?: MemoryCitationsMode;
  }) => string[];

JavaScript would tolerate the zero-arg form at runtime (extra args
are ignored), but the signature was misleading and would fail a
TypeScript port. Updated to (_params) => [...] with a comment
explaining we accept-but-ignore. Underscore-prefix conveys
intentional non-use to lint/readers.

Validation: 3/3 openclaw tests still pass (they invoke
promptBuilder({ availableTools: new Set() }) — both old and new
signatures accept that call).
2026-05-12 23:39:24 +01:00
Rohit Ghumare c21410e287 feat(plugin): ship Codex plugin manifest + marketplace (#311)
* feat(plugin): ship Codex plugin manifest + marketplace (#codex-plugins)

OpenAI Codex shipped a plugin platform
(developers.openai.com/codex/plugins) with the same shape as Anthropic
Claude Code plugins: .codex-plugin/plugin.json manifest, optional
.mcp.json, hooks/hooks.json, and a skills/ directory.

Verified against openai/codex source
(codex-rs/hooks/src/engine/discovery.rs): Codex's hook engine
explicitly injects CLAUDE_PLUGIN_ROOT into hook subprocesses for
OOTB compat with existing Claude Code plugins. The wire-format
input schemas
(codex-rs/hooks/schema/generated/session-start.command.input.schema.json
and siblings) use the same field names as Claude Code
(session_id, cwd, hook_event_name, source, transcript_path, model,
permission_mode).

Net result: our existing plugin/ directory already works as a Codex
plugin with two small additions:

1. plugin/.codex-plugin/plugin.json — Codex manifest pointing at the
   shared ./.mcp.json, ./skills/, and a Codex-specific ./hooks/
   hooks.codex.json. Name (kebab-case), version, description match
   the Claude Code manifest.

2. plugin/hooks/hooks.codex.json — Codex-compatible hook subset.
   Drops SubagentStart, SubagentStop, SessionEnd, Notification,
   TaskCompleted, PostToolUseFailure (Claude-Code-only). Keeps
   SessionStart, UserPromptSubmit, PreToolUse, PostToolUse,
   PreCompact, Stop. Adds Codex-specific `statusMessage` decorations
   on the two hooks the user sees most (SessionStart + UserPromptSubmit).
   The script commands themselves still reference ${CLAUDE_PLUGIN_ROOT}
   because Codex's engine injects it.

3. .codex-plugin/marketplace.json at repo root — git-subdir source
   pointing at ./plugin so `codex plugin marketplace add rohitg00/
   agentmemory` works the same way `claude plugin marketplace add`
   does. Mirrors the existing .claude-plugin/marketplace.json shape
   but in Codex's marketplace schema (name + interface.displayName
   + plugins[].source.{source, url, path, ref} + plugins[].policy).

README updates:
- Codex CLI tile in the Works-with-every-agent grid now reads
  "6 hooks + MCP + skills" instead of "MCP server" to surface the
  upgraded surface.
- "Other agents" install table now distinguishes "Codex CLI (MCP
  only)" (existing codex mcp add path) from "Codex CLI (full
  plugin)" (new marketplace install).
- New Codex section in the "paste this prompt" block with the
  two-command install (server + marketplace add + plugin install),
  the list of registered surfaces, and the env-var-injection note
  citing the Codex source line.

6 new tests in test/codex-plugin.test.ts cover:
1. .codex-plugin/plugin.json present with kebab-case name and
   required references.
2. Manifest version matches main package.json (so future bumps
   don't drift the Codex side).
3. Every path referenced in the manifest resolves to a real file or
   directory on disk.
4. hooks.codex.json contains only events Codex supports — any
   future addition has to be added to a Codex allowlist or the
   test fails loudly.
5. Every ${CLAUDE_PLUGIN_ROOT}/scripts/* command references an
   existing script file (catches typos before users hit them).
6. .codex-plugin/marketplace.json declares git-subdir source with
   path: "./plugin" pointing at this repo.

874 / 874 tests pass.

* fix(plugin/codex): align manifest description + assert PreCompact hook

Two reviewer findings addressed:

1. plugin/.codex-plugin/plugin.json's description claimed "12 hooks"
   but Codex only registers 6 (the Codex hook input schemas don't
   define SubagentStart, SubagentStop, SessionEnd, Notification,
   TaskCompleted, PostToolUseFailure). Updated the description to
   "6 hooks, 51 MCP tools, 4 skills, real-time viewer." so the
   marketplace listing matches the registered surface.

2. test/codex-plugin.test.ts asserted five lifecycle events
   (SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop)
   but omitted PreCompact, leaving a regression hole: dropping
   PreCompact from hooks.codex.json would still pass the test
   because the allowlist loop only blocks UNKNOWN events. Added
   expect(events).toContain("PreCompact") so the suite enforces
   the full six-hook contract.

Skipped (with reason):

3. marketplace.json source.ref currently "main" — reviewer asked
   for a fixed tag/SHA. Skipping because pinning to a tag forces
   updating marketplace.json on every release; "main" matches our
   convention where releases tag from main HEAD and pre-release
   work is on feature branches. Users wanting an immutable cut
   can pass --ref vX.Y.Z to `codex plugin marketplace add` per
   Codex docs.

Validation: 6/6 codex tests pass; 874/874 full suite unchanged.
2026-05-12 23:39:10 +01:00
Rohit Ghumare 5ea01c162c fix(deploy): distroless volume perms + viewer proxy + budget loop (#299, #301) (#304)
@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 <host>: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.
2026-05-12 16:19:13 +01:00
Rohit Ghumare 03fb42db39 chore(release): v0.9.9 — pinned slot injection + MiniMax env loader (#290)
Two field-reported fixes landed since v0.9.8:

- #288 (closes #286) — wires pinned memory slots into mem::context so
  agentmemory pinned-slot content actually reaches SessionStart. The
  renderPinnedContext / listPinnedSlots helpers from #182 had zero
  callers; this lands the wiring behind isSlotsEnabled().
- #289 (closes #285) — MiniMax provider now reads MINIMAX_BASE_URL via
  the shared getEnvVar() loader (so ~/.agentmemory/.env values get
  picked up), and the default endpoint is bumped from the stale
  api.minimaxi.com to api.minimax.io/anthropic per MiniMax's current
  Anthropic-compatible docs.

Bumping 0.9.8 -> 0.9.9 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.
2026-05-11 19:38:20 +01:00
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 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).
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 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.
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 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 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 94fc1191ef chore: bump version to 0.9.4 + add CHANGELOG (#216) 2026-04-29 17:26:04 +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 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 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).
2026-04-22 16:38:37 +01:00