104 Commits

Author SHA1 Message Date
Rohit Ghumare 564c24bdb1 docs(readme): drop sponsor badge — sidebar widget covers it (#549)
GitHub auto-renders the "Sponsor this project" widget in the
right sidebar from .github/FUNDING.yml (Sponsor button + heart icon
+ "Learn more about GitHub Sponsors" link). The README badge was
redundant noise on the top badge row.

Sidebar widget is the canonical surface — one path, one click.
2026-05-19 19:05:06 +01:00
Rohit Ghumare 632fa3531d revert: drop GH Packages mirror, keep single canonical install path (#548)
Reverting the GH Packages publish from #545. GH Packages is a
separate registry from npmjs.com — anyone installing
`@rohitg00/agentmemory` from `npm.pkg.github.com` needs to point
their registry there and authenticate, which is friction users
don't have on the canonical `@agentmemory/agentmemory` install
from public npm.

The right-sidebar Packages widget on the repo page was the only
motivation for the mirror. Acceptable to leave it empty — the
single canonical install path is the better DX.

- Drop `publish-github-packages` job from `.github/workflows/publish.yml`
- Drop `packages: write` perm wording from the workflow comment block
- Remove "GitHub Packages mirror" badge from README

Manual follow-up (post-merge): delete the already-published
`@rohitg00/agentmemory@0.9.20` from GH Packages registry via
github.com/users/rohitg00/packages/npm/agentmemory/settings → Delete.
2026-05-19 18:58:55 +01:00
Rohit Ghumare b4259229a6 feat(repo): add Sponsor button + GH Packages mirror for sidebar surface (#545)
* feat(repo): add Sponsor button + GH Packages mirror for sidebar surface

Three additions that make the repo page surface clearer + give users
a single place to fund the project:

1. `.github/FUNDING.yml` — `github: [rohitg00]` renders the "Sponsor"
   button at the top of the repo + the Sponsor widget in the right
   sidebar. Requires GitHub Sponsors to be enabled at
   github.com/sponsors/accounts on the rohitg00 profile before the
   link resolves (currently 404s — enable before merging this PR).

2. `.github/workflows/publish.yml` — new `publish-github-packages`
   job runs after the existing public-npm publish completes.
   Republishes the main package as `@rohitg00/agentmemory` to
   `npm.pkg.github.com`. The repo's right-sidebar "Packages" widget
   only surfaces packages on GitHub Packages, not packages on the
   public npm registry, so this is what makes the sidebar widget
   non-empty. Public npm remains the canonical install source;
   GH Packages is purely a discovery surface.

   - Uses built-in GITHUB_TOKEN, no new secrets needed.
   - Rewrites package.json `name` + `publishConfig` in-runner via a
     small node one-liner, publishes, then restores the original so
     main isn't permanently scope-changed.
   - Skip-on-already-published guard mirrors the existing public
     publish steps.
   - Marked `|| echo "non-fatal"` so a GH Packages hiccup never blocks
     the canonical npm release.
   - `permissions: packages: write` added at workflow level.

3. README badge row — added `npm downloads`, `GitHub Packages mirror`,
   and `Sponsor rohitg00 on GitHub Sponsors` badges alongside the
   existing `npm version` / `CI` / `License` / `Stars` row. The
   sponsor badge is the same link the FUNDING.yml sidebar widget
   uses; surfacing it in-README means readers who don't notice the
   sidebar still see it.

Out of scope (asked, declined):
- Docker Hub / ghcr.io publish workflow. Not in this PR.

* ci(publish): scope write perms per-job + persist-credentials false

Inline review on #545 flagged that the workflow-level permissions
block granted `id-token: write` + `packages: write` to every job,
including ones that don't need them. Tightened to least-privilege:

- Workflow-level: only `contents: read`.
- `publish` job: adds `id-token: write` (required for `npm publish
  --provenance` to mint a Sigstore OIDC token). The GH Packages
  job doesn't inherit this.
- `publish-github-packages` job: adds `packages: write` (required
  to push to npm.pkg.github.com). The public-npm publish job
  doesn't inherit this.

Both `actions/checkout@v6` calls also pick up `persist-credentials:
false`. The publish steps never push back to the repo, so the
GITHUB_TOKEN doesn't need to land in `.git/config` after checkout.
Same posture both jobs.

Skipped from the same review pass:

- **Pin actions to commit SHAs.** Industry rule but introduces real
  maintenance friction — Renovate/Dependabot don't auto-bump
  SHA-pinned actions to new minors, so SHA pinning trades easy
  semver tracking for stale-action drift. We stay on `@v6` major-tag
  pins (GitHub publishes those via verified moving refs).
- **Disable setup-node cache.** `actions/setup-node@v6` defaults to
  cache-off (the `cache:` input is opt-in). `package-manager-cache`
  only auto-enables when `package.json` has a `packageManager` field
  — agentmemory's doesn't (verified via `grep`). The fix is a no-op
  on this workflow.
2026-05-19 18:28:55 +01:00
Trip 68fddd418e feat: OpenCode plugin with 22 auto-capture hooks (closes #236, #244) (#237)
* feat: OpenCode plugin with 22 auto-capture hooks (closes #156)

- 22 hook handlers across session lifecycle, messages, tool lifecycle, parts, files, permissions, tasks, commands, and config
- Two-layer enrichment pipeline: /context + /enrich via system.transform
- Two slash commands: /recall and /remember
- Full Claude Code hook parity documented with gap analysis

* fix(plugin): use prompt_submit hookType so sessions get firstPrompt

mem::observe checks hookType === "prompt_submit" to extract raw.userPrompt
and set session.firstPrompt. The plugin was using "user_prompt_submit" which
didn't match, so sessions were never named.

* fix: address CodeRabbit review feedback on OpenCode plugin

- Use ctx.worktree for projectPath instead of opaque project.id
- Add Array.isArray guards before output.system/.context.push()
- Only delete stashed files after successful enrich POST
- Defensive JSON.stringify for undefined tool inputs
- Per-session Map-based dedup sets to prevent unbounded growth
- Fix negative duration_ms when time.completed is unset
- Validate props.file and enforce MAX_STASHED_FILES in file.edited
- Deduplicate /summarize call on session idle
- Buffer early config events until session.created flushes them
- Move contextInjectedSessions.add after successful context fetch
- Add DEBUG-gated error logging to network helpers
- Guard config input.agent/mcp/provider against non-object types
- Fix MCP badge count 44→51 in plugin README

* fix: add language identifier to fenced code block in plugin README

* fix: store OpenCode session title as summary/firstPrompt on creation

* fix(plugin): add session instruction injection and consolidation pipeline (closes #233)

Three gaps from the Claude Code plugin port sweep:

- Inject agentmemory usage instructions (memory_save, memory_recall, etc.) into the system prompt on first turn via experimental.chat.system.transform, replacing the skills mechanism that OpenCode lacks
- Call /crystals/auto and /consolidate-pipeline on session.deleted, mirroring Claude's CONSOLIDATION_ENABLED behavior
- Document MEMORY.md vs AGENTS.md architecture comparison (two-hop file bridge vs one-hop direct injection)

Gap A (SubagentStop) is unfixable — OpenCode's SubtaskPart type has no completion/result fields.
Gap C (Claude MEMORY.md bridge) is intentionally skipped — OpenCode uses direct injection.

* fix(plugin): address adversarial code review — 7 critical/high fixes

- Guard instructions push with Array.isArray check + fix TOCTOU race
  by moving contextInjectedSessions.add() before the await
- Session-scope stashedFiles via stashFor() helper (was process-global,
  could cross-contaminate concurrent sessions)
- Fix tool prefix in instructions (agentmemory_memory_ not agentmemory_)
- Check typeof string before pushing .context into system arrays
- Change olderThanDays: 0 → 7 in consolidation fire-and-forget
- Increase fire-and-forget timeout from 5s to 30s (consolidation
  takes minutes, 5s was guaranteed to abort)

* fix(plugin): scope stashedFiles.delete(sid) not .clear() on session.deleted

* fix(plugin): address round 2 adversarial review — 3 critical fixes

- Fix session cross-contamination: file.edited and tool.execute.before
  now use props.sessionID/input.sessionID fallback instead of only
  activeSessionId, preventing subagent hijack of parent stash
- Fix contextInjectedSessions regression from round 1: move add(sid)
  to after instructions push (synchronous) but before context fetch
  (async), so failed /context calls don't permanently skip injection
- Fix Map memory leak: prune session entries (stashedFiles,
  seenSubtaskIds, seenToolCallIds) when session.status goes idle,
  preventing unbounded growth for crash-killed sessions

* fix(plugin): address round 2 remaining medium issues

- Enforce MAX_STASHED_FILES cap on chat.message (was missing, could
  grow unbounded from 50+ file-ref messages)
- Remove activeSessionId fallback from session.deleted; log warning
  when both info.id and sessionID are missing instead of guessing
- Add subtaskSetFor/toolCallSetFor lazy-init wrappers matching
  stashFor pattern; fixes dedup failures for subagents spawned
  without a preceding session.created event
- Guard chat.params against missing input.model (TypeError crash)

* fix(plugin): address round 3 adversarial review — 10 HIGH/MEDIUM fixes

- Add safeSlice() helper replacing all unsafe (v as string || "").slice()
  calls; handles objects/BigInt/circular refs via try/catch JSON.stringify
- Restore activeSessionId fallback on session.deleted (if both info.id
  and sessionID missing, fall back like every other handler)
- Fix message.updated to check info.id before info.sessionID (matches
  session.created/session.updated pattern)
- Add props.sessionID to message.part.updated fallback chain
- Guard undefined callID and subtask ID (prevent dedup silently
  dropping all subsequent undefined-ID events)
- Cap todo.updated at 100 entries (match session.diff's slice pattern)
- Fix duration_ms || → ?? (0ms genuine duration no longer falsifies)
- Fix extractErrorMessage || → ?? (0/false error values preserved)
- Replace extractErrorMessage.slice with safeSlice in retry handler

* fix(plugin): address round 4 adversarial review — 6 CRITICAL/HIGH fixes

CRITICAL:
- Revert message.updated session ID resolution: info.id is the message
  UUID, not session ID. In message.updated, info is the message object
  (has role/tokens/modelID), not session info like in session.created.
  Use props.sessionID || info.sessionID || activeSessionId instead.
- Fix post_tool_use_failure -> post_tool_failure: server types.ts
  defines "post_tool_failure" (no "use_"); compress-synthetic.ts
  classifies by that exact string. All error observations were
  misclassified as "other" instead of "error".
- Fix contextInjected ordering: move add(sid) after the context fetch
  completes, not before. If /context times out, session is no longer
  permanently marked injected, allowing retry on next transform.

HIGH:
- Add process.cwd() fallback for projectPath (was null, causing
  silent 400s on every REST call when no workspace)
- Guard session.start against null activeSessionId with early return
  (was sending sessionId: null to API)
- Fix duration_ms: use typeof number checks instead of || 0 defaults;
  missing timing data now correctly reports null instead of 0ms

* fix(plugin): address final adversarial blockers

---------

Co-authored-by: xuli500177 <62830942+xuli500177@users.noreply.github.com>
Co-authored-by: Trip <5579540+cl0ckt0wer@users.noreply.github.com>
Co-authored-by: Rohit Ghumare <ghumare64@gmail.com>
2026-05-19 00:22:42 +01:00
Rohit Ghumare 249d8ff032 feat(commits): link agent sessions to git commits (#498)
* feat(commits): link agent sessions to git commits

Adds a commit-link layer so memories captured during an agent session
can be traced back to the git commit they produced, and vice versa.

What ships:

- KV.commits namespace storing CommitLink records keyed by full SHA.
- Session.commitShas[] back-reference for fast forward lookup.
- POST /agentmemory/session/commit upserts a link (sha, branch, repo,
  message, author, authoredAt, files, sessionId); merges sessionIds on
  re-link and preserves linkedAt.
- GET /agentmemory/session/by-commit?sha= hydrates the link plus its
  sessions.
- GET /agentmemory/commits?branch=&repo=&limit= lists recent links,
  filtered and sorted desc by linkedAt, limit clamp 1..500 (default 100).
- src/hooks/post-commit.ts shells git rev-parse/log/diff-tree and POSTs
  to /agentmemory/session/commit. Honors AGENTMEMORY_URL,
  AGENTMEMORY_SECRET, AGENTMEMORY_CWD, AGENTMEMORY_SESSION_ID,
  AGENTMEMORY_COMMIT_SHA. Best-effort, 1500 ms timeout. Wired into
  tsdown hookEntries so it bundles to dist/hooks/ and plugin/scripts/.
- Two MCP tools: memory_commit_lookup, memory_commits.
- Four user-invocable plugin skills:
  - commit-context: traces a file/function/line via git blame plus
    memory_commit_lookup plus memory_recall.
  - commit-history: lists agent-linked commits; parses branch=/repo=/
    limit= from $ARGUMENTS.
  - handoff: resumes the most recent session for cwd; surfaces an
    unanswered user-facing question before the brief.
  - recap: summarizes the last N sessions for cwd, grouped by date;
    parses last <n> / today / this week.

Wire-up to capture from a real repo:

    ln -sf "$(realpath node_modules/agentmemory/dist/hooks/post-commit.mjs)" .git/hooks/post-commit
    chmod +x .git/hooks/post-commit

No new dependencies. Additive only.

* fix(commits): address CI + review findings on commit-link feature

- Update mcp-standalone.test.ts to expect 14 CORE_TOOLS (was 12) now that
  memory_commit_lookup and memory_commits are registered. Restores CI on
  Node 20 and 22.
- Wrap CommitLink upsert and session.commitShas mutation in withKeyedLock
  to prevent lost updates when concurrent post-commit hooks race against
  each other or against api::session::commit callers. Locks keyed by
  `commit:<sha>` and `session:<sessionId>` so unrelated writers do not
  serialize.
- Parallelize hydration of linked sessions via Promise.all in both
  api::session::by-commit and the memory_commit_lookup MCP handler;
  serial kv.get loops dragged response time for commits with many
  sessions.
- Sharpen handoff SKILL.md project-path matching: resolve relative
  $ARGUMENTS to an absolute normalized path and require directory-
  boundary equality, not raw string prefix, so sibling repos sharing a
  prefix do not collide.
- Sharpen commit-history SKILL.md HTTP fallback: build the
  /agentmemory/commits URL with URL-encoded query values so branch and
  repo values containing `?`, `&`, or `#` cannot corrupt the request.

* chore(consistency): bump documented tool and endpoint counts

Bring README.md, AGENTS.md, and the boot log in src/index.ts in line
with the live tool registry (53 MCP tools, +2 from this branch) and
the live REST registration (124 endpoints, +3 from this branch).

The Consistency test in test/consistency.test.ts derives both counts
from source and asserts the markdown matches; this commit only updates
the literal numbers, no behavior change.
2026-05-18 11:19:47 +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
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
VantHoff 3166780d2c feat: add OpenAI-compatible LLM provider (#307)
* feat: add OpenAI-compatible LLM provider

- Add OpenAIProvider using raw fetch (no SDK dependency)
- Supports any /v1/chat/completions endpoint: OpenAI, DeepSeek,
  SiliconFlow, Azure OpenAI, vLLM, LM Studio, Ollama
- Auto-detects OPENAI_API_KEY with OPENAI_API_KEY_FOR_LLM opt-out
- Add OPENAI_REASONING_EFFORT passthrough for thinking models
  (e.g. Ollama Cloud kimi-k2.6) to ensure content is populated
- Update README with OpenAI provider table, env vars, and reasoning config

* fix(config): honor OPENAI_API_KEY_FOR_LLM=false in detectLlmProviderKind

`detectProvider()` correctly gates OpenAI auto-detection on
OPENAI_API_KEY_FOR_LLM !== "false", but `detectLlmProviderKind()` did
not — so users who set OPENAI_API_KEY only for embeddings (via the
existing OPENAI_BASE_URL + OPENAI_EMBEDDING_MODEL flow from #186)
would see /agentmemory/config/flags report `provider: llm` even
though detectProvider() routed them to the noop provider.

Also clarify in the README that OPENAI_REASONING_EFFORT is honored
only by reasoning models (o1, o3, gpt-*-reasoning) and providers
that mirror that schema (Ollama Cloud thinking models). Standard
chat models reject the field with 400.

Verified:
- OPENAI_API_KEY=sk-... + OPENAI_API_KEY_FOR_LLM=false now returns
  "noop" from detectLlmProviderKind (was "llm" before the fix).
- OPENAI_API_KEY=sk-... alone still returns "llm" (intended default).
- npm run build clean.

Note: 10 pre-existing test failures on test/mcp-standalone.test.ts
are a stale-branch artefact — this branch is 10 commits behind main
and is missing the MCP shim fixes that landed via #311 / #327.
Recommend rebasing on main (or "Update branch" via the GitHub UI)
before merge.

* feat(providers): openai — fetch timeout, Azure auto-detect, OPENAI_API_KEY scope hint

Three follow-ups against the v2 PR so reviewers can see what's left
before merge:

1. Outbound fetch timeout. The other raw-fetch providers
   (anthropic / gemini / openrouter / minimax) all lack one too —
   that's a same-pattern repo-wide concern tracked as a follow-up
   issue. This PR fixes the bound on the new surface only:
   AbortController + setTimeout, default 60s, overridable via
   OPENAI_TIMEOUT_MS. Abort messages explain how to raise the bound.

2. Azure OpenAI auto-detection. Azure is in the README's supported
   list but the code only emitted the standard OpenAI shape. Now
   detects `.openai.azure.com` hostnames at construction time and:
     - swaps `Authorization: Bearer` for `api-key: <KEY>`
     - drops the `/v1` path prefix (deployment is baked into base URL)
     - appends `api-version=<version>` query param (env-overridable)
   Default api-version is `2024-08-01-preview` per Azure docs.
   Existing users of the standard OpenAI shape are unaffected — the
   detection is purely hostname-driven.

3. README clarity on `OPENAI_API_KEY` shared use. Embeddings (#186)
   and this PR's LLM both auto-activate from the same key, which
   surprises users who only meant one or the other. Added an
   explicit callout above the LLM section pointing at the
   `OPENAI_API_KEY_FOR_LLM=false` opt-out. Also documents the new
   timeout + Azure base-URL shape inline.

Build clean. Tests 954/954 (10 pre-existing mcp-standalone failures
on main are not in this branch's snapshot).

---------

Co-authored-by: Rohit Ghumare <ghumare64@gmail.com>
2026-05-16 18:40:00 +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
Rohit Ghumare 9344cfd502 fix(cli): rerender splash banner + README npx caveat (#411)
* fix(cli): rerender splash banner with clean figlet output + README npx caveat

Two small fixes from local v0.9.15 testing:

1. The hand-drawn ASCII in `src/cli/splash.ts` was rendering "agentmemory"
   with broken middle glyphs — `_ _` instead of `_ __` around the 'n'
   and merged 'tm/me' segments. Replaced with verified `figlet
   agentmemory` standard-font output (70 cols, 6 rows, regenerable).
   Compact + minimal banners untouched.

2. Added a "Recommended: install globally" section to README's Quick
   Start. Explains the npx per-version cache gotcha (running
   `npx ...@0.9.14` then `npx @agentmemory/agentmemory` may serve the
   stale cached version) and the three workarounds:
     - `npm install -g @agentmemory/agentmemory` (recommended)
     - `npx -y @agentmemory/agentmemory@latest` (forces fresh fetch)
     - `rm -rf ~/.npm/_npx && npx ...` (one-shot cache clear)
   Notes that v0.9.16 ships an interactive global-install prompt
   inline on first npx run.

Build clean (44.46 kB cli.mjs). All 954 tests pass.

* docs: hoist install section to top of README + update website Install card

README: install was buried at line 306 below benchmarks, comparison
tables, and the agents grid. Moved a 4-line install block right under
the nav anchors at the top, with the bare `agentmemory` command from
the global install path leading. Kept the deeper Quick Start section
below — this is the "front door" version.

website (Install.tsx): the three-card install flow now leads with
`npm install -g @agentmemory/agentmemory` (step 1), then bare
`agentmemory` + `agentmemory demo`. Section title changed from "THREE
COMMANDS. ANY AGENT." to "ONE INSTALL. ANY AGENT." and the lede notes
the npx fallback for users who want zero-install.

website/lib/generated-meta.json refreshed by the build (auto-derived
from package.json + walked test count; pre-existing feedback rule).

Build clean (website + main both pass).

* docs: address coderabbit PR #411 review

README: annotate `rm -rf ~/.npm/_npx` as POSIX-only, point Windows
users at `Remove-Item ...\npm-cache\_npx`. Mark `npx -y @...@latest`
as the cross-platform option. Applies to both install-section and
quick-start blocks (lines 87, 363).

website Install.tsx: hint on step 1 now says "STEPS 2/3 NEED THIS"
so users who skip can't get stuck on `agentmemory` not found. Added
a fourth card "PREFER ZERO-INSTALL? USE NPX" with the bare npx
command and a pointer to the README caveat — covers the skip path
without making step 1 ambiguous.

Skipped (verified-false): coderabbit flagged the splash banner's
"70 columns wide" comment as wrong (claimed rows 3+5 are 76/77). They
counted source-string chars including the `\\` escapes; runtime is 70
on all 6 rows (node length check), which is what alignment cares
about. Comment is accurate, no change needed.
2026-05-15 18:02:33 +01:00
Rohit Ghumare 471cc21492 docs(readme): move OpenHuman after pi in agents grid (#397)
Reorder row 1 of the works-with-every-agent table so OpenHuman sits in
the sixth slot (after pi) instead of the third. Keeps the
native-plugin agents grouped left, leaves OpenHuman next to pi so the
native-Memory-trait callout reads cleanly alongside the other
native-integration agents.
2026-05-15 14:17:24 +01:00
Rahul ff3e024d7d docs(readme): clarify MCP shim vs full server tool count (#233 gap 1) (#391)
The README implied the `@agentmemory/mcp` shim exposes "all 51 tools when
AGENTMEMORY_TOOLS=all".  In reality:

- `AGENTMEMORY_TOOLS` is a server-side flag (read by iii-engine), not by
  the shim — setting it in the shim's MCP `env` block does nothing.
- The shim has two modes: proxy (when `AGENTMEMORY_URL` reaches a running
  agentmemory server, all 51 tools are forwarded) and local fallback
  (no server reachable → 7 hardcoded tools from
  `src/mcp/standalone.ts`'s `IMPLEMENTED_TOOLS`).

This caused users (Cursor / OpenCode / Gemini CLI) following the README
to assume they had misconfigured something when they only saw 7 tools.

Two changes:
- Codex plugin bullet: replace the misleading `AGENTMEMORY_TOOLS=all`
  qualifier with an accurate proxy/local mode description.
- MCP Server section header: add a callout that explicitly enumerates the
  7-tool local fallback set and tells users how to unlock the full 51.

Addresses gap #1 in #233.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 11:04:35 +01:00
Rohit Ghumare eb5f7e6895 chore(deps): bundle 10 Dependabot bumps (claude-agent-sdk 0.3, dotenv 17, vitest 4, ts 6, types/node 25, react 19.2.6, checkout/setup-node v6) (#390)
* chore(deps): bundle 10 Dependabot bumps + README agents grid refresh

Closes the open Dependabot wave in one PR.

Root npm bumps:
- @anthropic-ai/claude-agent-sdk ^0.2.56 → ^0.3.142 (closes #389 group)
- dotenv ^16.4.7 → ^17.4.2 (closes #356)
- @types/node ^22 → ^25.7.0 (closes #357)
- typescript ^5.7 → ^6.0.3 (closes #358)
- vitest ^3 → ^4.1.6 (closes #359)

Website npm bumps:
- @types/node 22.10.2 → ^25.7.0 (closes #354)
- typescript 5.7.2 → ^6.0.3 (closes #353)
- react/react-dom ^19.2.5 → ^19.2.6 (closes #352 group)

github-actions bumps:
- actions/checkout v4 → v6 (closes #351)
- actions/setup-node v4 → v6 (closes #350)

Verified:
- npm run build clean against new TypeScript 6 + vitest 4 stack
- npm test 903 / 903 pass
- website npm run build clean (next 16.2.6 + react 19.2.6 + ts 6 stack
  generates 5 static pages, finalises optimisation)
- No source changes were needed for any breaking-version bump — the
  agent-sdk 0.3.x removal of unstable_v2_createSession etc. is not on
  any path agentmemory exercises (we use query() + the existing
  AgentSDKProvider shape).

Plus README refresh on the Works-with-every-agent grid:
- Added OpenHuman entry (tinyhumansai/openhuman) pointing at the
  recently-landed Memory trait backend (PR tinyhumansai/openhuman#1743)
- Added pi as a native-plugin entry (we already ship
  integrations/pi/security.ts and the plugin file in-tree)
- Reordered the grid so native-plugin agents sit in row 1 and
  MCP-only / REST-only agents sit in row 2
- Updated sub-text on Claude Code / Codex CLI / OpenClaw / Hermes / pi
  to consistently say "native plugin" rather than mixing "plugin",
  "skills", "hooks". Counter on assets/tags/section-agents.svg moved
  from "16 integrations" to "15 integrations" because we dropped the
  meta "Any agent" + "Claude SDK" cells that double-counted REST and
  the AgentSDKProvider path already represented by Claude Code.

* docs(readme): swap pi logo to first-party SVG

Drops the generic GitHub-org avatar for the brand-mark SVG. Lives in
assets/agents/ so the README + future grid renders reach it directly.
2026-05-15 11:03:36 +01:00
Rohit Ghumare 4b354b7bd8 chore(providers): bump Gemini defaults to current GA models (#370)
* chore(providers): bump Gemini defaults to current GA models

Bundles two upstream PRs into one chore — both are blocking real users
today and both are simple default-string bumps with no API contract
change.

LLM default (was PR #368, @yut304)
- `gemini-2.0-flash` is deprecated in Google's Gemini API and returns
  429 rate-limit errors under load. Replace the default with
  `gemini-flash-latest`. Users on a pinned `GEMINI_MODEL` in
  `~/.agentmemory/.env` are unaffected.

Embedding default (was PR #246, @AmmarSaleh50)
- `text-embedding-004` is deprecated (shutdown Jan 14 2026). Replace
  with `gemini-embedding-001` (GA): 100+ languages, MRL dims
  (768 / 1536 / 3072), 2048-token input.
- URL path changes from `:batchEmbedContent` to `:batchEmbedContents`
  (plural — the new model's batch endpoint).
- Each request now sends `outputDimensionality: 768` so the returned
  vectors match the existing index dim guard from #248 — no
  reindex needed.
- L2-normalize each returned vector before pushing to the result
  array. `gemini-embedding-001` does not normalize by default,
  unlike `text-embedding-004`. Without this the cosine-similarity
  math elsewhere in the search pipeline (which assumes unit-length
  vectors) collapses.

Verified
- `npm test` clean: 903 / 903.
- `npm run build` clean.

Closes #368, closes #246.

* fix(gemini): pin LLM default to gemini-2.5-flash + warn-once on zero-norm

Addresses CodeRabbit findings on PR #370.

1. Pin Gemini LLM default to gemini-2.5-flash.

   `gemini-flash-latest` is a moving alias that points to whatever
   Google promotes next. Production behaviour should be deterministic
   from a release perspective — users who upgrade agentmemory should
   not also get a Gemini model rotation in the same step. Switch the
   default to the current stable GA model `gemini-2.5-flash`.
   Users who want the moving alias keep getting it via
   `GEMINI_MODEL=gemini-flash-latest` in `~/.agentmemory/.env`.

2. Warn-once on zero-norm embedding in l2Normalize.

   `gemini-embedding-001` can return a zero-norm vector for
   degenerate input. The previous code silently returned the zero
   vector — downstream cosine-similarity math then divides by zero
   and the call site sees `NaN` scores with no signal as to why.

   Emit a one-time stderr warning naming the model + vector length
   so operators can correlate index quality dips with upstream
   embedding regressions. Behaviour otherwise unchanged: return the
   zero vector and let BM25 carry the search signal.

   Throwing was the other option — rejected because a single bad
   embedding in a 100-item batch would abort the whole batch and
   surface as an indexing pipeline halt. Soft-fail + warn matches
   the rest of the embedding provider error handling.

Skipped finding:

- `outputDimensionality` → `output_dimensionality` snake_case rename.
  CodeRabbit asserts the REST API expects snake_case. The Gemini
  REST API actually uses camelCase on the wire — confirmed against
  ai.google.dev/api/embeddings (field labelled
  `outputDimensionality` in the REST schema; the Python SDK alone
  uses snake_case and translates internally). Current code is
  correct as-shipped; the snake_case rename would silently break
  the dim override.

Verified: 903 / 903 tests pass; build clean.
2026-05-14 13:35:02 +01:00
Rohit Ghumare a9c3a59da8 feat(deploy): fly.io / Railway / Render one-click templates (#361)
* feat(deploy): add fly.io / Railway / Render one-click templates

Closes #343.

Lets operators stand up agentmemory on managed infrastructure without
rolling their own Docker host. Each template extends the published
rohitghumare64/agentmemory:latest image, mounts persistent storage at
/data, generates the HMAC secret on first boot via openssl rand into
/data/.hmac (chmod 600, printed once to stdout), and ships
AGENTMEMORY_REQUIRE_HTTPS=1 by default so the v0.9.12 plaintext-bearer
guard refuses to leak tokens over non-loopback HTTP if a TLS upstream
gets misconfigured.

Only port 3111 (REST API) is exposed publicly. The viewer on 3113
stays bound to localhost inside the container; every README documents
the SSH-tunnel pattern. The main README gains a Deploy section with
three Deploy-to-platform shield buttons linking to the platform's
URL-encoded one-click flow, plus the deploy/ subtree with per-platform
docs covering HMAC capture, rotation, /data backup, viewer access,
cost floor, and known caveats.

* fix(deploy): run as non-root, placeholder app names, drop invalid Render badge

Addresses CodeRabbit findings on PR #361.

Dockerfiles (fly/railway/render): drop `USER root` + RUN chmod — those
left the container running as root after build. Replaced with
`COPY --chmod=0755 --chown=65532:65532` so the entrypoint script lands
with the correct permissions and owner without ever switching off the
distroless image's default nonroot user. Added an explicit
`USER 65532:65532` directive after COPY to make the runtime user
intent-visible to reviewers.

fly/README.md: every command referenced the hardcoded app name
`agentmemory`. Since the global `agentmemory` slug on Fly is almost
certainly taken, the first `fly launch --name agentmemory` fails and
the rest of the volume/log commands point at a non-existent app. Walk
users through setting `APP="agentmemory-$(whoami)"` once at the top and
reference `$APP` everywhere (launch / volumes / logs / proxy / ssh /
backup) — and derive the volume name (`agentmemory_data` style) from
`$APP` so it stays consistent.

railway/README.md: the previous `railway ssh --service agentmemory --
-L 3113:localhost:3113` snippet was invalid — Railway's CLI ssh does
not support port forwarding. Replaced with a quick in-container curl
check plus two valid browser paths: Railway TCP Proxy (the recommended
option, dashboard → Networking → TCP Proxy on container port 3113) and
an in-container sshd over a TCP Proxy if true SSH tunneling is needed.

README.md: removed the "Deploy to Render" badge. Render's deploy URL
requires `render.yaml` at the repository root, which we keep clean.
The `deploy/render/` blueprint stays in-tree for users who set up the
Render Blueprint flow manually — the deploy section now points readers
there explicitly.

Skipped: the entrypoint.sh SECRET-shape regex check. `openssl rand
-hex 32` under `set -eu` already exits non-zero on any failure path
and always emits exactly 64 hex characters when it succeeds — a
post-hoc regex would only fire on a kernel-level surprise we can't
recover from anyway.

* fix(deploy): self-contained Dockerfiles (no phantom base image)

Templates previously did `FROM rohitghumare64/agentmemory:latest`, an
image that does not exist on Docker Hub. agentmemory ships via npm
(`@agentmemory/agentmemory`) and runs against the `iii` engine binary
fetched from the `iii-hq/iii` GitHub release. There is no first-party
agentmemory Docker image yet.

Rewrote all three Dockerfiles to be self-contained against
`node:22-slim`:

- `apt-get` curl / openssl / ca-certificates / tini for the runtime
  prereqs (openssl for the first-boot HMAC, tini for clean PID 1).
- Download the iii binary from
  `github.com/iii-hq/iii/releases/download/iii/v${III_VERSION}/...`
  matching the build host's arch (uname -m).
- `npm install -g @agentmemory/agentmemory@${AGENTMEMORY_VERSION}` with
  `--omit=optional` so the CJK native deps (added in PR #362) don't
  fail the build on platforms without musl/glibc prebuilds.
- `mkdir -p /data && chown node:node /data` so the existing
  first-boot entrypoint can write `/data/.hmac` without root.
- `USER node:node` (UID 1000, the standard non-root user in the node
  image) instead of the made-up `65532:65532` that assumed a
  distroless base.

Both `AGENTMEMORY_VERSION` and `III_VERSION` are `ARG`s so platform
operators can pin a specific release through their dashboard's
build-args UI without editing the Dockerfile.

README updates:

- Top-level `README.md` and `deploy/README.md` no longer claim users
  pull a pre-published image; the wording now matches what each
  template actually does (build from source on the platform's builder).
- Per-platform "Known caveats" sections drop the
  `rohitghumare64/agentmemory:latest` line and replace it with notes
  about build-time, cache reuse, and how to pin via build args.
- `deploy/render/README.md` drops the obsolete `&imgURL=...` Deploy
  Hook query-string trick and replaces it with
  `AGENTMEMORY_VERSION` build-arg guidance.
- `deploy/README.md` switches "Integration clients (Hermes, OpenClaw,
  pi)" → "Integration plugins" to match the scrub convention from the
  earlier PR-body edit.

* feat(deploy): add Coolify self-hosted template

Adds deploy/coolify/ alongside the existing fly / Railway / Render
templates. Coolify (https://coolify.io/self-hosted) is the
self-hosted-on-your-own-VPS option for operators who don't want their
memories on a third-party managed plane.

Same self-contained Dockerfile pattern as the other three (node:22-slim
+ iii binary + npm package, USER node, build-args for version pinning)
plus a docker-compose.yml that Coolify's "Docker Compose" build pack
consumes directly. Compose layer adds a HEALTHCHECK, a named volume
for /data, and json-file log rotation matching the rest of the deploy
surface.

README walks the operator through:

- Coolify dashboard flow (New Application -> Public Repository ->
  Docker Compose build pack -> Base Directory: deploy/coolify).
- Capturing the first-boot HMAC secret from the Logs tab.
- Viewer access via SSH tunnel from the Coolify host (port 3113 stays
  internal by default) and the optional second-domain + basic-auth
  pattern for sharing it.
- HMAC rotation, /data backup paths (Restic / Borg / rsync / Coolify
  Backups), and VPS cost-floor pointers (Hetzner CX22, DO Basic
  Droplet, Vultr).

Top-level deploy/README.md and README.md updated to list the new
template and explain when to pick it (already running a VPS, want a
self-hosted control plane, don't want a third-party host holding your
memories).

* fix(deploy): COPY iii from iiidev/iii image, drop tarball download

Replaces the curl + tar + chmod sequence in all four Dockerfiles
(fly / railway / render / coolify) with a single multi-stage
COPY --from=iiidev/iii:${III_VERSION} /app/iii /usr/local/bin/iii.

Why:

- Drops the supply-chain risk CodeRabbit flagged on PR #361
  (curl-and-extract without checksum verification). iiidev/iii is the
  same image agentmemory's docker-compose.yml already pulls — using
  Docker Hub's content-addressed manifest as the integrity boundary
  is the same trust we already extend on the local Compose path.
- Drops the arch case statement (BuildKit picks the right manifest
  entry automatically from the multi-arch tag).
- Drops 3 of the 4 apt-get packages from the bookkeeping side; curl
  stays for the eventual HEALTHCHECK probe.

The iii binary lives at /app/iii inside the distroless iiidev/iii
image (per github.com/iii-hq/iii engine/Dockerfile), and COPY into
node:22-slim lands it at /usr/local/bin/iii with 755 permissions so
any USER can exec it.

ARG III_VERSION is now declared before the first FROM so it can be
interpolated into the iii-image stage tag. ARG AGENTMEMORY_VERSION
stays in the final stage where the npm install consumes it.

* fix(deploy): rewire iii config + /data perms + platform port binding

Audit against fly.io / Railway / Render / Coolify docs and the
agentmemory CLI source surfaced four bugs that would prevent every
template from actually serving traffic on a managed host. This commit
addresses all four together because they share the same fix surface
(entrypoint + Dockerfile + platform config) and shipping them
independently would leave intermediate states broken.

1. iii config bound 127.0.0.1 + used relative ./data paths.

   The npm-bundled @agentmemory/agentmemory/dist/iii-config.yaml hard-
   codes `host: 127.0.0.1` on iii-http and `file_path: ./data/...` on
   iii-state / iii-stream. agentmemory CLI's findIiiConfig() returns
   the first existing candidate from a 3-item list with the npm bundle
   at position 0, so a sibling iii-config.yaml in cwd does not
   override. On any managed platform the result is the REST API
   binding loopback (router can't reach) and state writing to a
   directory that's not the persistent mount.

   Fix: the entrypoint overwrites the bundled file at boot with a
   container-tuned config (host 0.0.0.0, /data/... absolute paths,
   no iii-exec because the CLI orchestrates the worker over WS).
   This needs the entrypoint to run as root, hence the Dockerfile
   change below.

2. /data volume permissions root:root vs USER node.

   Every managed platform mounts persistent volumes root-owned 0755.
   The earlier Dockerfile ran `RUN mkdir -p /data && chown -R node`
   at build time, but the volume overlay at runtime replaces /data
   with the platform's empty root-owned mountpoint, leaving the
   `node` USER unable to write state_store.db / .hmac.

   Fix: stay USER root through the image, add `gosu` to apt, do the
   chown in entrypoint.sh against the *runtime* /data, then drop to
   the unprivileged `node` user via `exec gosu node:node agentmemory`
   before exec'ing the CLI. Symmetric to the iii-init busybox sidecar
   pattern in the repo-root docker-compose.yml.

3. Render's PORT injection broke the published port.

   Render auto-sets PORT (default 10000) and routes the public proxy
   to that port; the container then must bind 0.0.0.0:$PORT. Our
   container always binds 3111 (Dockerfile EXPOSE + iii-http config),
   so Render's proxy was forwarding to 10000 and getting connection
   refused.

   Fix: render.yaml now sets envVars PORT=3111 to override Render's
   default, plus exposes AGENTMEMORY_VERSION / III_VERSION as envVars
   (Render translates envVars into docker build args automatically).
   Also drops the AGENTMEMORY_REQUIRE_HTTPS / AGENTMEMORY_HMAC_FILE /
   AGENTMEMORY_DATA_DIR entries — server-side code never reads any of
   them; entrypoint reads HMAC_FILE / DATA_DIR via ${VAR:-default}
   so defaults already apply.

4. Coolify compose `ports:` bypassed Traefik.

   Per Coolify docs, `ports: ["3111:3111"]` binds the port directly on
   the host outside of the proxy network. The deploy would have been
   reachable on `http://<host>:3111` with no TLS termination and no
   domain routing.

   Fix: replaced `ports:` with `expose:` so the port is only
   reachable on the internal proxy network. Operator now sets the
   service domain in the Coolify UI as `https://<fqdn>:3111` (the
   `:3111` is Coolify's proxy hint, not a public port). Also added
   `SERVICE_FQDN_AGENTMEMORY_3111` to the environment so the
   container can know its public URL via Coolify's magic-env
   convention.

Other minor fixes:

- railway.json adds `requiredMountPath: /data` so Railway fails fast
  if the operator forgets to attach a volume.
- fly.toml drops the dead-letter [env] block (server code does not
  read AGENTMEMORY_REQUIRE_HTTPS / AGENTMEMORY_HMAC_FILE /
  AGENTMEMORY_DATA_DIR; entrypoint defaults handle the latter two).
- All 4 entrypoints unified — same script, same heredoc, copy with
  `cp -p` at build time.
- READMEs updated to reflect the new bound port story, the manual
  Render Blueprint flow (Render's deploy-button auto-detection only
  scans repo-root render.yaml), the Coolify proxy / domain pattern,
  and the gosu privilege drop.

Best-practice audit results (fly.toml, railway.json, render.yaml,
docker-compose.yml) verified against current platform docs — fly.toml
schema is clean, railway.json gets the requiredMountPath addition,
render.yaml + coolify compose get the rewires above.

* fix(deploy): pin iii-sdk to 0.11.2 via npm overrides

agentmemory@0.9.12 declares `iii-sdk: ^0.11.2`, which `npm install`
caret-resolves to **0.11.6** as of writing. That bumps the worker
SDK ahead of the engine the agentmemory repo pins (iii v0.11.2).
The two versions are wire-compatible in the calls agentmemory uses
today, but the policy intent in the repo is single-version lockstep
until the v0.11.6 sandbox-everything model is wired through — running
SDK 0.11.6 against engine 0.11.2 in production silently re-introduces
the very drift the AGENTMEMORY_III_VERSION pin exists to prevent.

`npm install -g` ignores the `overrides` field, so we cannot fix this
with the existing global install. Switch each Dockerfile to a local
install under /opt/agentmemory with a one-line package.json carrying:

    overrides: { iii-sdk: 0.11.2 }

then symlink the produced `node_modules/.bin/agentmemory` into
`/usr/local/bin/agentmemory` so the entrypoint invocation stays
identical. Verified locally that this resolves
`node_modules/iii-sdk/package.json` to version 0.11.2 (was 0.11.6
without the override).

Side effects:
- Entrypoint III_CONFIG path moves from
  `/usr/local/lib/node_modules/@agentmemory/agentmemory/dist/iii-config.yaml`
  (global) to
  `/opt/agentmemory/node_modules/@agentmemory/agentmemory/dist/iii-config.yaml`
  (local). agentmemory CLI's findIiiConfig() resolves __dirname
  through the symlink to the local install so candidate-0 lookup
  still hits the file we overwrite.
- New ARG `III_SDK_VERSION=0.11.2` declared in every Dockerfile,
  surfaced as a `build.args` field in coolify's compose and as an
  envVar in render.yaml so operators can pin a different SDK version
  if they manually migrate to engine 0.11.6+ down the line.
- AGENTMEMORY_III_VERSION env now baked into the image (matches the
  engine pin) so the CLI's iii-binary download fallback path also
  resolves to the right version if PATH lookup ever misses.

* fix(deploy): set TINI_SUBREAPER=1 so tini reaps zombies on Fly

End-to-end deploy to fly.io surfaced the warning:

    [WARN  tini (645)] Tini is not running as PID 1 and isn't
    registered as a child subreaper. Zombie processes will not be
    re-parented to Tini, so zombie reaping won't work.

Fly's init system holds PID 1, so tini lands as a child process and
the agentmemory CLI (which spawns iii detached) ends up with no
process reaping its eventual zombies. Setting TINI_SUBREAPER=1
triggers prctl(PR_SET_CHILD_SUBREAPER, 1) so tini still reaps
descendant zombies even when it isn't PID 1. Same fix applies to
Railway and Render (which also wrap our entrypoint under their own
init) and to Coolify when the operator runs the container under any
PID-1-grabbing supervisor like systemd-nspawn.

Verified end-to-end against fly.io:

- App agentmemory-ghumare64 deployed via remote builder
- 114 MB image (iii engine + node:22-slim + npm packages)
- 1 GB volume in iad provisioned and mounted
- First-boot HMAC generated and persisted to /data/.hmac
- iii-engine + agentmemory worker came up clean
- Worker registered with iii engine over ws://localhost:49134
- /agentmemory/livez returned 200 in 220 ms
- Bearer auth gating verified: 401 without secret, 200/201 with it
- POST /agentmemory/observe ingested a synthetic observation
- POST /agentmemory/smart-search returned a valid response envelope

Only outstanding log noise after this commit is a Node punycode
deprecation warning from a transitive dep — not actionable from this
side.

* fix(deploy): backfill TINI_SUBREAPER=1 on railway + render Dockerfiles

Previous commit missed these two — same reasoning applies (Railway and
Render both wrap our entrypoint under their own platform init, so tini
lands as a non-PID-1 child and zombie reaping needs the prctl
subreaper bit set).

* fix(deploy): apply live-fly-deploy findings across all templates

Real end-to-end deploy on fly.io surfaced four refinements that
generalise across the other three templates. This commit ports them.

1. Health-check grace_period: 10 s -> 30 s.

   Measured cold start on a fresh fly.io machine in `iad`:

       machine image prepared :  5.1 s
       volume mount + format  :  2.5 s
       firecracker boot       :  1.0 s
       entrypoint + chown     :  0.5 s
       iii-engine ready       :  3.0 s
       agentmemory worker reg :  2.0 s
       --------------------------------
       healthcheck passes     : ~9-10 s

   `grace_period = "10s"` (fly.toml) and `start_period: 20s` (coolify
   compose + Dockerfile HEALTHCHECK) were both within the noise of the
   measured time. Bumped both to 30 s — gives a 3x safety margin
   without affecting normal-operation health-check cadence.

   Railway and Render manage their own initial-deploy grace windows
   server-side, so railway.json's `healthcheckTimeout: 30` (per-attempt)
   and render.yaml's `healthCheckPath` stay as-is.

2. Documented LLM + embedding provider env-var menu in deploy/README.md.

   Live deploy logs surface a clear warning:

       [agentmemory] No LLM provider key found
       (ANTHROPIC_API_KEY, GEMINI_API_KEY, OPENROUTER_API_KEY,
        MINIMAX_API_KEY). LLM-backed compression and summarization are
       DISABLED -- using no-op provider.

   The deploy READMEs never told operators how to opt in. Added a
   table covering ANTHROPIC / GEMINI / OPENROUTER for LLM,
   OPENAI / VOYAGE for embeddings, plus AGENTMEMORY_AUTO_COMPRESS and
   AGENTMEMORY_INJECT_CONTEXT for the corresponding behaviour flags.
   Each platform's variable-injection surface (flyctl secrets,
   dashboard Environment tab) is named in the same table.

3. Captured cold-start budget in deploy/README.md.

   Same step-by-step measured timing above is now in the shared
   README so operators have a concrete expectation rather than
   guessing why first-byte after `fly deploy` takes ten seconds. Notes
   that every template's start window is sized for 3x of this.

4. fly README: documented shared-IPv4 + dedicated-IPv6 default.

   Fly assigns one of each by default at no charge. Legacy clients
   without SNI that need a dedicated IPv4 can buy one for $2/month
   via `fly ips allocate-v4`. Added the build-time stats observed in
   the real deploy (~30 s first build, ~10 s cached rebuild, 114 MB
   image) to the Known Caveats section so operators sizing
   build-minute budgets have real numbers.

No code paths changed -- only TOML / YAML / Markdown surface tweaks
and one HEALTHCHECK `start_period` bump in the coolify Dockerfile.
2026-05-14 11:22:58 +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 f365d38c75 docs(python): iii-sdk example replaces standalone REST client (#364)
Closes #342.

agentmemory functions register as mem::remember, mem::observe,
mem::context, mem::smart-search, mem::forget via sdk.registerFunction.
iii-sdk already ships in Python, Rust, and Node — any of them can call
those functions directly over the iii WebSocket transport. A standalone
Python REST client would duplicate the transport layer and fragment the
cross-language story.

This patch adds:
- examples/python/quickstart.py — minimal save + smart-search loop.
- examples/python/observe_and_recall.py — observation ingest + context
  rendering at a token budget.
- examples/python/README.md — function map + install + usage.
- README "Programmatic access (Python / Rust / Node)" section pointing
  at the example and the three SDK install commands.

REST on :3111 is unchanged for hosts without an iii runtime.
2026-05-13 21:08:31 +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 292e9f6af1 docs(readme): add Trendshift badge + star history graph + tagline fix (#305)
* docs(readme): add Trendshift badge + star history graph + tagline fix

Three small README polishes flagged on the live README screenshot:

1. Add the Trendshift badge (repo #25123) directly under the tagline
   so the trending signal sits at eye level with the banner.
2. Add the star-history.com graph under the badge so the growth
   curve is visible without scrolling — uses <picture> with
   dark/light variants matching the rest of the README's
   prefers-color-scheme convention.
3. Tagline missed two first-party integrations that already ship
   plugin folders in this repo (integrations/hermes,
   integrations/openclaw); added Hermes + OpenClaw to the agent
   list so the supported-clients sentence matches what the
   integration index actually advertises.
4. Reflow the Karpathy-gist sentence onto a single italic line —
   the previous block-strong + <br/> shape wrapped awkwardly on
   narrow viewports.

Not in this PR (deferred): the stat-tag row redesign flagged on
the same screenshot. That needs new SVG assets in assets/tags/
and is worth doing as a separate visual pass rather than a quick
edit.

* Update README.md

* docs(readme): simplify star history to plain img link

GitHub's image proxy doesn't reliably render <picture> with external
SVG sources from api.star-history.com — the entire block came back
empty in rendered README. Switched to the standard plain img inside
an anchor pattern that every other widely-used repo (k8s, react,
deno, etc) uses for star-history embeds. Single URL, no
prefers-color-scheme variants, single fetch path for the camo
proxy.

* docs(readme): switch star history to markdown form (&amp; escaping bug)

HTML form had unescaped `&type=Date` in src attribute. Browser/GitHub
parsed `&type` as an HTML entity attempt, so the URL camo fetched was
truncated to `?repos=rohitg00/agentmemory` — invalid endpoint, broken
image. Markdown form auto-escapes ampersands, matching every other
repo's star-history embed.

* docs(readme): use canonical star-history chart endpoint with theme variants

User-provided exact snippet. Switches:
- /svg -> /chart endpoint (canonical star-history.com API)
- type=Date -> type=date (lowercase, matches docs)
- adds legend=top-left
- <picture> with prefers-color-scheme dark/light sources
- outer link uses query-param form (?repos=...) not fragment (#repos=...)

Matches the pattern star-history.com's own embed-generator hands out.
2026-05-12 16:24:59 +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 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 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
Mandeep Singh 2daeb5afda Add tested Pi/OpenClaw/Hermes integration fixes (#230) 2026-05-07 15:41:52 +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 1a8c0f325c Update README.md 2026-04-28 10:46:58 +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 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 02d13c59a7 fix(slots): resolve rebase artifacts (stray marker + tool count bump to 51) 2026-04-21 19:54:11 +01:00
Rohit Ghumare b03e9702f2 fix(slots): address CodeRabbit review on #182
## Concurrency

- mem::slot-create now wraps the duplicate check + set + audit inside
  withKeyedLock. Previously a racing slot-append could create a slot
  that slot-create assumed was absent.
- mem::slot-delete acquires the same slot-level lock writers use, so
  an in-flight append/replace cannot resurrect a deleted slot.
- mem::slot-reflect now reads pending_items / session_patterns /
  project_context INSIDE their per-slot locks, computes the new
  content under the lock, and writes back under the same lock.
  The old path read-then-locked-then-wrote, which could clobber a
  concurrent slot-replace on the same label.

## Scope + validation

- mem::slot-create duplicate check is now scope-local via
  readSlotInScope(). A project slot with the same label as a global
  slot is allowed and shadows the global on read (matches the read
  precedence). Before, create rejected any label that already existed
  in either scope.
- validateScope() returns null on unknown scope values; handler
  returns "scope must be 'project' or 'global'" 400 instead of
  silently defaulting.
- validateSizeLimit() returns null on non-integers, negatives, or
  values > 20000; handler returns "sizeLimit must be an integer
  between 1 and 20000" 400 instead of silently falling back to
  DEFAULT_SIZE_LIMIT.
- Timestamp captured once (ts) then used for createdAt + updatedAt,
  both in the create path and in seedDefaults.

## HTTP surface

- api::slot-create now rejects bad types up front:
  content / description must be strings, pinned must be a boolean,
  scope must be "project" or "global", sizeLimit must be a positive
  integer <= 20000. Malformed inputs get a 400 with a specific error
  message instead of being silently dropped.

## MCP surface

- memory_slot_create accepts both boolean and string-boolean forms
  for pinned (MCP clients bind either depending on their schema
  wrapper). Same for scope, which now also accepts explicit "project".

## Other cleanup

- event::session::stopped uses isReflectEnabled() helper instead of
  poking process.env directly. triggerVoid call wrapped in try/catch
  with a warn log so a reflect failure doesn't surface as an
  unhandled rejection.
- src/index.ts boot banner computes MCP tool count via
  getAllTools().length instead of hardcoding 44 — banner now tracks
  the actual registry size.
- README counts bumped: "all 43 memory tools" → "all 50 memory tools"
  (x2), "### 44 Tools" → "### 50 Tools", "Extended tools (44 total" →
  "(50 total", "646 tests" → "800 tests" (x2).

## Tests

- test/slots.test.ts: shadow test now goes through the public handler
  API instead of direct kv.set. Two new cases: invalid sizeLimit
  (over max, negative, non-integer) rejected; unknown scope rejected.
- Full suite: 802/802 pass (+2 new).
2026-04-21 18:57:17 +01:00
Rohit Ghumare c70b60211a feat(slots): add editable pinned memory slots + idle reflection + global scope
Introduces memory slots as a new first-class memory primitive, orthogonal
to observations / memories / graph. A slot is a labelled, size-limited,
agent-editable chunk of context. 8 defaults seed on first run
(persona, user_preferences, tool_guidelines, project_context, guidance,
pending_items, session_patterns, self_notes). Agent reads/writes via
MCP tools; pinned slots serialise cleanly into SessionStart context.

Functions (`src/functions/slots.ts`):
- mem::slot-list
- mem::slot-get
- mem::slot-create
- mem::slot-append   (refuses writes over sizeLimit)
- mem::slot-replace  (refuses content over sizeLimit)
- mem::slot-delete
- mem::slot-reflect  (used by idle reflection)

REST (`src/triggers/api.ts`):
- GET    /agentmemory/slots
- GET    /agentmemory/slot?label=...
- POST   /agentmemory/slot
- POST   /agentmemory/slot/append
- POST   /agentmemory/slot/replace
- DELETE /agentmemory/slot?label=...
- POST   /agentmemory/slot/reflect

MCP tools (`src/mcp/tools-registry.ts`, `src/mcp/server.ts`):
- memory_slot_list
- memory_slot_get
- memory_slot_create
- memory_slot_append
- memory_slot_replace
- memory_slot_delete

event::session::stopped fires mem::slot-reflect via triggerVoid when
AGENTMEMORY_REFLECT=true. The reflector reads the last N observations,
extracts TODO-flavoured titles into pending_items, counts error /
command types into session_patterns, and records touched files in
project_context. Fire-and-forget: does not block summarise.

Slots carry a scope field ("project" | "global"). Global slots live at
KV.globalSlots ("mem:slots:global") — one brain across all projects for
things like persona / user_preferences / tool_guidelines. Project-
scoped slot with the same label shadows the global one on read.

- AGENTMEMORY_SLOTS=true    — enable the whole primitive
- AGENTMEMORY_REFLECT=true  — enable Stop-hook reflection (requires SLOTS)

Both default OFF. Matches the opt-in convention of auto-compress (#138),
inject-context (#143), vision embeddings (#179).

44 MCP tools → 50 across README (stats badge, install instructions,
config block) and plugin/.claude-plugin/plugin.json description. New
AGENTMEMORY_SLOTS and AGENTMEMORY_REFLECT entries in the config block.

test/slots.test.ts — 12 new cases: default seeding by scope, label
validation, create/get/append/replace/delete round-trip, duplicate-
create rejection, sizeLimit enforcement on append + replace, project
shadows global, listPinnedSlots + renderPinnedContext, reflect no-op
on empty session, reflect updates pending_items / session_patterns /
project_context from TODO + error + file signals.

Full suite: 800/800 pass (+12 new). Build clean.
2026-04-21 18:57:17 +01:00
Rohit Ghumare 241be2669c fix(vision): address CodeRabbit review on #179
- src/functions/image-refs.ts: delete imageEmbeddings BEFORE imageRefs
  and drop the silent .catch() so transient KV errors surface. Stops
  orphan searchable embeddings when the ref delete fails halfway.
- src/functions/vision-search.ts: record an audit row for every
  successful mem::vision-embed write (new "vision_embed" op). State-
  changing functions must leave an audit trail per the repo policy.
- src/functions/vision-search.ts: mem::vision-search now wraps raw
  queryImageBase64 into a data: URL before handing to embedImage, so
  the provider contract stays "data: URL or filesystem path".
- src/providers/embedding/clip.ts: drop the raw-base64 heuristic in
  loadImage. The old regex whitelisted "/" in the char class but the
  !src.includes("/") guard rejected any realistic base64 blob,
  falling through to readFile and throwing ENOENT. Now: data: URL or
  filesystem path only; callers with raw base64 pass a data: URL.
  Also drop unused RawImage.read type and collapse the redundant
  Float32Array copy in embedImage.
- src/triggers/api.ts: both handlers now whitelist fields with
  asNonEmptyString and parseOptionalPositiveInt before constructing
  the payload. Matches api::observe / api::search style, stops
  forwarding the raw req.body, clamps topK at the boundary.
- src/types.ts: add "vision_embed" to AuditEntry.operation union and
  rename EmbeddingProvider.embedImage's param to src (matches the
  implementation now that raw base64 isn't accepted).
- README.md, plugin/.claude-plugin/plugin.json: document
  AGENTMEMORY_IMAGE_EMBEDDINGS and bump tool count string.

Full suite still 794/794.
2026-04-21 12:07:47 +01:00
Rohit Ghumare bf6f33f48d feat(multimodal): Phase 1 — CLIP visual embeddings + vision-search
Builds on PR #111's image store. Adds a dedicated visual embedding
path so agents can search screenshots by natural language or by
similar image, without routing through VLM-describe → BM25-text.

## Additions

- `EmbeddingProvider.embedImage?(src)` slot (non-breaking, optional)
- `ClipEmbeddingProvider` — CLIP ViT-B/32 via `@xenova/transformers`
  (already a dep). 512d, normalized. Supports base64, data URL, and
  file paths. Lazy model load; same pattern as the existing local
  text provider.
- `createImageEmbeddingProvider()` gated on
  `AGENTMEMORY_IMAGE_EMBEDDINGS=true` — opt-in, matches the
  auto-compress/context-injection opt-in pattern from #138 and #143.
- `KV.imageEmbeddings` scope keyed by imageRef path.
- `mem::vision-embed` + `mem::vision-search` functions. Cosine
  similarity, brute-force over the stored set (fine at image-memory
  scales; ANN can come later).
- `POST /agentmemory/vision-embed` + `POST /agentmemory/vision-search`
  HTTP triggers with auth + 400/503 error shapes.
- MCP tool `memory_vision_search` exposed via the running-server
  handler. Accepts queryText, queryImageRef, queryImageBase64, topK,
  sessionId.
- Observe pipeline: when an image is stored and the flag is on,
  fires `mem::vision-embed` via triggerVoid so embedding happens off
  the critical path.
- `decrementImageRef` sweeps the embedding too when refcount hits
  zero, so forget/evict/retention stop orphaning vectors.

## Why

Mem0/Zep/Letta are text-only in 2026. AgenticVision ships CLIP but
needs manual capture. agentmemory already auto-captures via hooks —
bolting CLIP onto that pipeline makes it the only memory system
where the agent sees what it sees and can cross-modal retrieve
without manual invocation.

Full plan: local spec at ~/specs-backup/agentmemory-multimodal-plan.md.

## Tests

- test/vision-search.test.ts: 6 new cases covering embed→store, text
  query ranking, image-to-image query, sessionId filter, provider-
  absent error, missing-query rejection.
- Full suite: 794/794 pass (+6 new).
2026-04-21 11:20:26 +01:00
Rohit Ghumare ca626f3bed fix(version): sync version across viewer + README, drop historical union
- src/version.ts: collapse literal union to single `VERSION = "0.9.0"`;
  the historical enum was regenerated on every release and served no
  runtime purpose. Export-import keeps its supportedVersions set, which
  is the actual compat contract.

- src/viewer/index.html: replace hardcoded `v0.7.0` brand badge with
  `__AGENTMEMORY_VERSION__` placeholder.

- src/viewer/document.ts: substitute the placeholder with VERSION at
  render time (same mechanism as the CSP nonce).

- README.md: "New in v0.8.2" banner was two releases stale. Replace with
  v0.9.0 highlights linking to CHANGELOG.
2026-04-20 22:11:15 +01:00
Rohit Ghumare ca578ce8d8 Update README.md 2026-04-19 01:19:44 +01:00
Rohit Ghumare 329e7ca082 Merge pull request #157 from rohitg00/docs/iii-console-readme
docs: document iii console for trace-level inspection
2026-04-17 17:34:42 +01:00
Rohit Ghumare 75c994dc43 Update README.md 2026-04-17 17:34:30 +01:00
Rohit Ghumare d87cfa6921 docs: embed iii console screenshots in README
Add two screenshots sourced from iii.dev/docs/console and vendored into
assets/iii-console/ so they don't depend on CDN signed URLs:

- dashboard.png — hero shot showing counters, application flow, and
  registered triggers.
- traces-waterfall.png — trace waterfall with per-span durations,
  placed above the traces setup block.

states.png is checked in for a future edit but not yet referenced.
2026-04-17 17:31:58 +01:00
Rohit Ghumare ebacb20b13 docs: document iii console for trace-level inspection
agentmemory runs on the iii engine, so the official iii console gives
users OTEL traces, the raw KV state browser, the stream monitor, and a
direct function invoker for every piece of memory machinery without
wiring up MCP or a custom client.

- Install and launch instructions, with --port 3114 to avoid the
  agentmemory viewer already holding 3113.
- Page-by-page guide covering Functions, Triggers, States, Streams,
  Traces, and Logs with agentmemory-specific use cases.
- Notes that the iii-observability worker is already enabled in
  iii-config.yaml with exporter: memory, so Traces and Logs light
  up by default.
- Warns that the console has no built-in auth and must stay on
  127.0.0.1.
2026-04-17 17:24:57 +01:00
Rohit Ghumare 0e8157eaa6 Update README.md 2026-04-17 13:16:22 +01:00
Rohit Ghumare 50817adb8a fix: address CodeRabbit review on session replay
Security:
- Tighten sensitive-path guard to boundary-aware regexes (no more
  false positive on project names containing "token", "secret", etc.)
- Re-check every walked JSONL file against the guard, not just the
  root argument
- Expand leading "~" in user-supplied paths before resolve()

Correctness:
- Update existing Session.observationCount on re-import instead of
  silently leaving it stale
- Parallelize per-session observation writes via Promise.all
- Capture a single fallback timestamp in the parser so startedAt and
  endedAt agree when metadata is missing
- Trim incoming path in the import REST handler

Robustness:
- Add AbortSignal.timeout(120s) to the CLI import fetch and surface
  TimeoutError cleanly
- Stop the replay interval when switching away from the replay tab
- Style the Replay tab empty state with a new .empty rule

Sync:
- Bump REST endpoint count (104 -> 107) in startup banner + README
- Add 0.7.9 and 0.8.0 to VERSION union so it matches ExportData.version

Tests:
- Add replay-sensitive.test.ts covering false-positive avoidance
- Add parser test for shared fallback timestamp
2026-04-17 00:38:23 +01:00
Rohit Ghumare 11bef18927 feat: session replay tab + JSONL transcript import
Adds a Replay tab to the viewer that plays any stored session as a
scrubbable timeline (prompts, tool calls, tool results, responses)
with play/pause, speed control, and keyboard navigation.

Adds `agentmemory import-jsonl [path]` CLI subcommand and a matching
POST /agentmemory/replay/import-jsonl endpoint to backfill sessions
from Claude Code JSONL transcripts (default ~/.claude/projects).

All new logic routes through iii-sdk primitives: registerFunction for
mem::replay::load / mem::replay::sessions / mem::replay::import-jsonl,
and the existing HTTP trigger in api.ts for REST exposure.

Security: JSONL import rejects symlinks, paths containing sensitive
terms, and tolerates malformed lines without aborting the batch.
All imports are recorded in the audit log.
2026-04-17 00:26:14 +01:00
Rohit Ghumare 14da6c816c feat: add token-efficient recall and file compression tool (#150)
* feat: add token-efficient recall and file compression tool

Add memory_recall format and token budget support, plus a new memory_compress_file tool with API/MCP wiring, tests, and updated tool/endpoint docs.

* chore: remove temporary architecture assets

Delete temporary architecture and memory-cycle asset files added during implementation.
2026-04-16 10:36:17 +01:00
Rohit Ghumare aad6b7a70f Update GitHub Gist badge in README
Updated GitHub Gist badge to reflect new star and fork counts.
2026-04-15 21:53:09 +01:00
Rohit Ghumare 5d70ecfb3c feat: migrate agentmemory to iii v0.11 and add upgrade command (#116)
* feat: migrate agentmemory to iii v0.11 and add upgrade command

Migrate the codebase from legacy iii-sdk v0.3 APIs to v0.11 trigger/register patterns, update runtime configs, and align stream/state behavior with newer engine semantics. Add a new `agentmemory upgrade` CLI command so users can refresh dependencies and iii runtime components with one entrypoint.

* chore: remove pnpm lockfile from migration PR

Drop pnpm-lock.yaml from this branch to keep the migration PR focused on source and config changes only.

* fix(ci): sync npm lockfile with iii-sdk v0.11 dependency

Update package-lock.json so npm ci in CI matches package.json after the iii-sdk migration.

* fix(ci): resolve build parse errors after v0.11 migration

Fix malformed braces introduced during API migration in triggers/health/branch-aware files so tsdown build passes in CI.

* fix: harden migration follow-up fixes and audits

Apply the reviewed v0.11 follow-up fixes across runtime, docs, and tests by tightening validation, correcting upgrade/error handling, and adding missing audit coverage on state mutations. This also addresses stream fallback behavior, lock consistency, retention source-bucket cleanup, and test/mock alignment so build and test remain green.

* fix: address 9 unresolved CodeRabbit findings on iii v0.11 migration

CodeRabbit flagged 9 remaining issues on #116 — all real. Each was
verified against the current branch code before applying. Two
findings were deliberately skipped as policy/scope issues and are
documented at the bottom.

### Applied (9 real findings)

- src/triggers/api.ts — api.ts numeric query param validation:
  multiple sites forwarded `parseInt(params.limit)` result to the
  downstream function without a Number.isFinite check. A non-numeric
  query value produced NaN at the iii-sdk trigger boundary. Added
  parseOptionalInt and parseOptionalFloat helpers at the top of the
  file, wired into api::crystal-list, api::lesson-list, and
  api::insight-list (5 sites total).

- src/triggers/api.ts — api::observe, api::context, api::session::start,
  api::session::end were forwarding req.body verbatim to sdk.trigger
  with no validation. Added explicit type checks mirroring the existing
  api::search pattern, construct a sanitized payload object before
  triggering.

- src/cli.ts — p.confirm() can return a cancel Symbol on Ctrl+C, which
  is truthy, so the upgrade path ran even when the user cancelled.
  Added p.isCancel() check and explicit boolean comparison.

- src/cli.ts — removed dead `failed` flag in runUpgrade: every caller
  already calls process.exit(1) immediately, so the final `if (failed)`
  branch was unreachable. Simplified requireSuccess accordingly.

- src/functions/leases.ts — mem::lease-renew recorded audit events as
  "lease_acquire", which conflated renewals with first claims. Renamed
  the audit event to "lease_renew" so downstream audit filters can tell
  the two operations apart.

- src/functions/relations.ts — the pair lock
  `mem:${firstId}:${secondId}` serialized concurrent relate(A,B) calls
  with identical pairs, but did NOT protect concurrent relate(A,B) +
  relate(A,C). Both modify memory A's `relatedIds` array, which is a
  classic last-writer-wins race producing lost relation edges. Fixed
  by replacing the pair lock with nested per-entity locks in canonical
  sort order: withKeyedLock(mem:firstId) wrapping withKeyedLock(mem:secondId).
  Since the ids are sorted deterministically, no deadlock is possible.

- src/functions/sketches.ts + src/functions/summarize.ts + new
  src/functions/audit.ts safeAudit helper — recordAudit was awaited
  after kv.set calls. An audit write failure would reject and the
  caller would see an error even though the target state was already
  persisted. New safeAudit wrapper swallows audit errors and logs them
  via ctx.logger.warn, preserving the mutation's success. Applied to
  all 11 audit sites in sketches.ts and the single site in summarize.ts.

- src/mcp/server.ts — three issues in the MCP handler:
  1. memory_profile's refresh flag used `args.refresh === "true"`,
     ignoring boolean `true` from MCP clients that send proper types.
     Now accepts both.
  2. memory_sketch_create built sketchPayload with
     `asNonEmptyString(args.title)` which could return undefined,
     then forwarded that to the downstream function. Added explicit
     validation + 400 response at the MCP boundary.
  3. memory_recall, memory_team_feed, memory_audit_query used
     `(args.limit as number) || 10` which replaced an explicit 0 with
     10. Changed to typeof check so explicit 0 (rare but legal) is
     preserved.

- src/functions/governance.ts — mem::governance-bulk used Promise.all
  for the delete batch, which fails fast on the first error. The audit
  record still said `deleted: candidates.length` even if only half
  actually succeeded. Switched to Promise.allSettled, split results
  into successfulIds and failures arrays, record audit with both counts
  plus the per-failure details for traceability.

- src/functions/mesh.ts — mem::mesh-register, mem::mesh-sync,
  mem::mesh-receive, mem::mesh-remove all dereferenced `data.*` without
  checking that data was passed. A TypeError would bubble up on null
  payload. Added early null/type guards returning structured errors.

- src/functions/obsidian-export.ts — resolveVaultDir(data.vaultDir)
  was called without validating that vaultDir was a string, and
  `new Set(data.types)` without validating types was an array of
  strings. Added explicit validation returning 400-style error
  responses.

- src/functions/retention.ts — the eviction loop silently swallowed
  kv.delete errors via a bare `continue`. Added ctx.logger.warn on
  catch, included memoryId and sourceBucket in the log, and now
  returns `failed` count alongside `evicted` in the response.

- src/viewer/index.html — two WebSocket bugs:
  1. connectWs assigned to the mutable global state.ws before binding
     handlers, so old sockets could have their callbacks fire and
     mutate retry/direct state after state.ws was overwritten by a
     new socket. Fixed by creating a local `ws` variable, binding
     all handlers to it, only then assigning state.ws = ws. Each
     handler guards `if (state.ws !== ws) return` so stale callbacks
     are dropped.
  2. handleStreamEvent routed EVERY incoming event to routeWsMessage,
     so non-observation events (session.activity, etc.) were being
     treated as timeline observations and causing UI confusion. Added
     a looksLikeObservation helper + event_type gate that only routes
     real observation payloads.

- test/retention.test.ts — added an explicit sourceBucket eviction
  test that seeds a semantic memory at high age, runs retention-score,
  then runs retention-evict at high threshold and asserts BOTH
  KV.semantic and KV.memories are empty. Proves the candidate.sourceBucket
  branch added in this PR actually routes to the right bucket.

- src/types.ts — side fix: the ExportData.version union on line 254
  used comma separators instead of pipes in 3 positions (0.7.9,
  0.8.0, 0.8.1 → needed | between them). tsdown strips types so the
  build passed, but tsc --noEmit would have thrown and any IDE showed
  squiggles. Pre-existing latent bug, fixed while in the file.

### Deliberately skipped

- retention.ts eviction audit (CodeRabbit asked to add recordAudit
  to the eviction loop): policy change, not a bug fix. Verified:
  auto-forget.ts, evict.ts, retention-evict, remember-forget all
  skip audit by convention — only governance audits. Adding audit
  everywhere is a policy decision tracked in issue #125.

- file-index.ts sequential → parallel session lookup: micro-opt,
  the loop is already cache-backed, negligible savings, not worth
  the readability cost.

Tests: 655/655. Build clean.

* fix(api): harden request validation at HTTP boundaries

Validate and sanitize session, observe, and context inputs plus numeric query params before forwarding to memory functions, returning 400 for invalid values instead of propagating malformed payloads. Also remove duplicate CLI command registration introduced during merge resolution.

* fix: address 6 new CodeRabbit findings on iii v0.11 migration (#116 round 2)

CodeRabbit's second review pass on #116 flagged 6 real issues introduced
by the previous round of fixes (commit 3e3ac7e). Each was verified
against the current code before applying.

### Fixed

- src/functions/governance.ts — mem::governance-bulk switched to
  Promise.allSettled in the previous round but fanned out every
  kv.delete concurrently. The governance endpoint can target tens of
  thousands of memories in a single call; firing them all at once
  overwhelms the state worker. Batched with BATCH_SIZE=50 chunks,
  awaited serially while preserving the per-batch allSettled so a
  single failure doesn't abort the rest.
- src/functions/governance.ts — the recordAudit call AFTER the bulk
  delete is already committed was still using `await recordAudit`, so
  an audit write failure would bubble up as a "failed" response even
  though the rows were actually deleted. Switched to safeAudit so the
  deletes are reflected faithfully in the response regardless of
  audit health.
- src/functions/relations.ts — the mem::relate handler was emitting
  three recordAudit calls INTERLEAVED with three kv.set writes:
  relation → audit → source update → audit → target update → audit.
  A thrown audit in the middle would leave KV.relations and
  source/target.relatedIds in a partial state. Restructured to
  complete all three durable writes first, then emit a single
  safeAudit at the end. Also changed the operation from the generic
  "evolve" (which made audit queries indistinguishable from actual
  version evolutions) to a new "relation_create" event, added to the
  AuditEntry.operation union in src/types.ts.
- src/functions/retention.ts — mem::retention-score used
  `{ ...DEFAULT_DECAY, ...data.config }` which is only a shallow
  merge. A caller passing `{ config: { tierThresholds: { cold: 0.2 } } }`
  would drop the hot and warm thresholds, causing every downstream
  `s.score >= config.tierThresholds.hot` comparison to produce NaN
  and misclassify every tier bucket. Deep-merged tierThresholds
  explicitly.
- src/functions/retention.ts — mem::retention-evict previously
  used `candidate.sourceBucket || KV.memories` as the delete bucket.
  That's correct for new retention scores (which now store
  sourceBucket) but wrong for pre-migration scores whose sourceBucket
  is undefined: the fallback would try to delete semantic ids from
  KV.memories and silently no-op, leaving the real semantic memory
  alive. Fixed by splitting the delete path: if sourceBucket is set,
  use it; otherwise attempt both KV.memories and KV.semantic with
  individual .catch(() => {}) wrappers so whichever bucket actually
  contains the id succeeds and the other is a no-op. Legacy rows
  retire naturally on the next scoring run, which writes sourceBucket.
- src/triggers/api.ts — api::obsidian-export was validated at the
  downstream mem::obsidian-export layer in the previous round, but
  the API boundary handler was still forwarding body.vaultDir without
  a type check. Added the same 400 response pattern used by
  api::search / api::observe / api::context for consistency.

### Skipped from CodeRabbit's suggestions

- CodeRabbit's proposed fix for retention.ts:249 was
  `candidate.sourceBucket || KV.retentionScores` which would try to
  delete the memory from the retention scores namespace — that's
  where the scoring entry lives, not where the memory lives. That
  fix is wrong. The two-bucket fallback approach above is the
  correct one.

Tests: 655/655. Build clean.

* fix(agentmemory): harden deletion, relation, and API validation paths

Bound retention/governance side effects and make audit logging best-effort so successful state writes are not masked by telemetry failures. Also tighten boundary validation for MCP/API inputs, resolve relation lock edge cases, and align retention test mocks with v0.11 trigger/registerFunction call shapes.

* fix: address remaining CodeRabbit validation and eviction issues

Remove duplicate CLI help examples, harden eviction/delete bookkeeping, allow file-context without sessionId, sanitize governance failure output, validate retention-evict inputs with bounded limits, and tighten summarize/observations/obsidian-export input validation.

* fix(v0.11): align stream send payloads and migration docs

Use the engine-compatible stream payload key `type` for stream::send events and update docs/plugin examples to reflect v0.11 function registration and trigger request shapes.
2026-04-15 14:02:22 +01:00
Rohit Ghumare 3bad5430ed fix: SessionStart context gate (#143) + retention-evict semantic leak (#124) (#145)
* fix: stop burning Claude Pro tokens on every tool call (#143)

0.8.8 fixed the agentmemory-side Claude API burn (where the engine
called Claude via the user's ANTHROPIC_API_KEY for per-observation
compression). That addressed #138 for users with API keys, but it
missed the second and much larger token-burn path: the PreToolUse
hook writing context to stdout.

Claude Code reads PreToolUse stdout and prepends it to the model's
next turn. src/hooks/pre-tool-use.ts was POSTing /agentmemory/enrich
on every Edit/Write/Read/Glob/Grep tool call and piping up to 4000
chars of response context into stdout. At ~20 tool calls per user
message this silently injected ~20K tokens per message into Claude
Code's input window — all charged against the user's Claude Pro
allocation because Claude Code was the one sending them to Anthropic.
4 messages drained the cap, which matches @adrianricardo's report.

session-start.ts had the same pattern (injected once per session,
smaller blast radius).

Fix: gate both hooks on AGENTMEMORY_INJECT_CONTEXT, default false.

- pre-tool-use.ts: when disabled, exit immediately — no stdin read,
  no fetch, no stdout write. The hot path (~20x per message) becomes
  a no-op Node startup.
- session-start.ts: when disabled, still POST /agentmemory/session/start
  so the session gets registered for observation tracking, but never
  write context to stdout. The session registration is cheap and
  doesn't touch Claude Code's input window.
- src/config.ts: new isContextInjectionEnabled() helper.
- src/index.ts: startup banner prints 'Context injection: OFF
  (default, #143)' on normal startup and a loud WARNING when
  opt-in is enabled.
- test/context-injection.test.ts: 5 subprocess tests that spawn the
  compiled pre-tool-use.mjs and session-start.mjs hooks, feed real
  JSON payloads via stdin, and assert stdout is empty in all the
  off/default paths. Also asserts the disabled path exits under 1s
  and the opt-in path with an unreachable backend still exits
  cleanly.
- README .env section: new AGENTMEMORY_INJECT_CONTEXT entry.
- CHANGELOG [0.8.10] with prominent 'Behavior change' banner.

Observations are still captured via PostToolUse regardless of the
flag — the memory store and MCP search tools are completely
unaffected by this change. The fix only severs the path where
agentmemory silently shoves memory context into the user's Claude
Code conversation.

Bumps to 0.8.10 (main + @agentmemory/mcp shim). Test count:
724 passing (was 719 + 5 new).

* chore: rewrite #143 CHANGELOG entry with corrected diagnosis

PreToolUse stdout is NOT injected into the model context — per
the Claude Code hook docs, only UserPromptSubmit and SessionStart
stdout are injected. My initial #143 PR description and CHANGELOG
claimed PreToolUse was the smoking gun behind 'Pro allocation
burned in 4 messages', which is wrong.

What's actually true:
- SessionStart stdout injection IS real (~1-2K tokens per session)
- PreToolUse stdout goes to debug log only — no tokens
- Claude Pro's Claude Code quotas are tight by design (Anthropic
  has publicly acknowledged this); 4 messages to burn is plausible
  with or without agentmemory installed

The gate on pre-tool-use.ts is still worth keeping as a resource
cleanup (skips a 20x-per-message Node+HTTP hot path) and as
forward-compat protection in case Claude Code ever changes
PreToolUse hook contract. But the CHANGELOG entry has to stop
claiming it saves tokens when it doesn't.

* fix: mem::retention-evict no longer leaks semantic memories (#124)

The eviction loop was unconditionally calling kv.delete(KV.memories,
id) for every below-threshold candidate, but retention scores are
computed for both episodic (KV.memories) and semantic (KV.semantic)
memories. When a candidate came from KV.semantic, the delete silently
became a no-op (key wasn't in mem:memories to begin with) and the
semantic row stayed alive forever with a sub-threshold score. Semantic
memories could not be evicted by this path at all.

Fix:
- Add a source: "episodic" | "semantic" discriminator to RetentionScore
- Tag it at score creation in both loops of mem::retention-score
- Branch the delete in mem::retention-evict on candidate.source,
  routing to KV.memories or KV.semantic accordingly
- Pre-0.8.10 retention rows with no source field are treated as
  episodic for backwards-compat so upgraded stores continue to evict
  their old rows without re-scoring first
- Response now includes evictedEpisodic and evictedSemantic counts
  so callers can see what was removed from each scope

Adds 3 regression tests to test/retention.test.ts:
- Scoring tags rows with the correct source
- Evicting a mixed set of below-threshold episodic + semantic
  candidates removes both from their respective scopes
- Legacy-shape score rows with no source field still evict
  to mem:memories (backwards-compat)

Full suite: 727 passing (was 724 + 3 new).

* review: probe namespaces for legacy retention rows (#124, round 2)

CodeRabbit caught a real backwards-compat hole in the #124 fix:
pre-0.8.10 stores already contain semantic retention rows with
no source field (because the old mem::retention-score scored
KV.semantic before the discriminator existed). My first fix
defaulted missing source to episodic, which meant those legacy
semantic rows still got delete-routed to KV.memories — the exact
no-op that stranded them in the first place.

Fix: when candidate.source is undefined, probe KV.memories first
for the memoryId; if it's there, route to episodic, otherwise
route to semantic. Count the resolved source in the response.

Adds one new test case: a pre-0.8.10 semantic memory with a
legacy-shape retention row (no source field) gets evicted from
mem:semantic, not silently no-op'd. Existing 'defaults to
episodic' test is kept and retargeted to the genuinely-episodic
legacy case.

Also fixes a README nit: the AGENTMEMORY_INJECT_CONTEXT comment
previously implied SessionStart fires on every tool turn. It's
once per session. Now broken out into two bullets explaining
what each hook does differently, with the note that only
SessionStart actually reaches the model (PreToolUse stdout is
debug-log only per Claude Code docs).

Full suite: 728 passing (was 727 + 1 new).

* review: audit retention evictions + assert persisted source (#124 round 3)

CodeRabbit round 3 findings, both real:

1. retention-evict performs structural deletes (memories / semantic /
   retention scores / access logs) but was not calling recordAudit().
   Repo learnings say state-changing functions must be auditable except
   for read-path bookkeeping. Now emits one batched audit row per
   non-zero eviction sweep:

     operation: 'delete'
     functionId: 'mem::retention-evict'
     targetIds: every evicted memoryId
     details: { threshold, evicted, evictedEpisodic, evictedSemantic,
                reason: 'retention score below threshold' }

   Zero-eviction sweeps intentionally do NOT write an audit row (no
   state change, no need to flood the audit log during health checks).

2. The #124 scoring test only checked result.scores (transient
   response) but not the persisted mem:retention rows. Eviction reads
   back from stored rows, so a regression in kv.set/serialization
   would have still passed the old assertion. Now also does
   kv.get('mem:retention', id) and asserts { source: ... }.

Two new tests:
- Retention evict with a mixed set of 2 episodic + 1 semantic
  candidates writes exactly one audit row with all 3 ids in targetIds
  and the correct evictedEpisodic/Semantic breakdown in details.
- Retention evict with zero candidates writes zero audit rows.

Full suite: 730 passing (was 728 + 2 new).

* review: audit retention-score + parallelize writes (#124 round 4)

CodeRabbit round 3 outside-diff findings, both addressed:

1. mem::retention-score was persisting schema-relevant writes to
   KV.retentionScores (1000+ rows in a mature store) but never called
   recordAudit(). Per the repo audit-coverage policy, state-changing
   functions need an audit row. Added a single batched audit event
   per rescore:

     operation: 'retention_score' (new audit op — added to the
                                    AuditEntry union in types.ts)
     functionId: 'mem::retention-score'
     targetIds: [] (intentionally empty — a mature store can have
                    1000+ ids per sweep; flooding the audit log
                    with every memoryId on every cron tick is
                    worse than recording just the summary counts)
     details: { total, episodic, semantic, tiers, config }

   Zero-memory stores intentionally skip the audit call.

2. The per-memory kv.set inside the score loop was O(n) sequential
   round-trips. Refactored to collect pendingWrites: [id, entry][]
   while iterating, then flush with Promise.all at the end. On a
   mature store with 1000+ memories this is ~10x faster (depends on
   backend pipelining).

Test updates:
- Added 'mem::retention-score emits audit row per rescore' covering
  the new audit call, targetIds=[], and details.episodic/semantic.
- Existing '#124 audit evict' and 'zero-evict skip audit' tests now
  filter the audit log by functionId === 'mem::retention-evict'
  because retention-score also writes one row per sweep now.

Full suite: 731 passing (was 730 + 1 new, existing tests retargeted).
2026-04-15 12:06:43 +01:00