Commit Graph

485 Commits

Author SHA1 Message Date
CauchYoung 5649e9c647 fix: pass plugin mcp env overrides (#386)
* fix: pass plugin mcp env overrides

Signed-off-by: laplace young <yangqk12@whu.edu.cn>

* fix: simplify plugin mcp env templates

---------

Signed-off-by: laplace young <yangqk12@whu.edu.cn>
Co-authored-by: laplace young <yangqk12@whu.edu.cn>
2026-05-20 21:44:37 +01:00
kedar-1 f5e67ea133 Add sudo for global installation command (#454)
* Add sudo for global installation command

* Update README.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update README with EACCES retry instructions

Added installation instructions for macOS/Linux users.

* docs: drop backticks around package name inside bash code fence

Backticks inside a ```bash fenced block are still copy-pasted literally
by users, and bash interprets them as command substitution. The package
name in the install line had decorative backticks that turn into a shell
syntax error when pasted as-is.

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Rohit Ghumare <ghumare64@gmail.com>
2026-05-20 21:38:53 +01:00
Aqil Aziz ec6282382b docs: clarify env file setup (#321)
Signed-off-by: aqilaziz <gonzes7@gmail.com>
2026-05-20 18:02:58 +01:00
Tanmay Shirbhayye edd1ceb916 fix(cli): accurately display bound viewer port on splash screen (#560)
* fix(cli): accurately display bound viewer port on splash screen
- Expose viewerPort and viewerSkipped state in /agentmemory/livez endpoint.
- Update CLI readiness check to poll until the viewer port is bound or explicitly skipped.
- Prevents misleading default port (3113) display on splash screen when the viewer falls back to another port.

* fix(viewer): address CodeRabbitAI review
2026-05-20 16:34:57 +01:00
Kaushal Reddy 8e313271c9 Update index.html (#542) 2026-05-20 16:34:37 +01:00
Ptah a9945144a3 fix(providers/openai): send explicit stream:false in chat completion body (#526)
OpenAI API spec defines `stream` as defaulting to false when absent, so
the current code (which omits it) should yield JSON. Some OpenAI-compatible
proxies disagree and default to text/event-stream, which crashes the
`response.json()` parser below with:

  Unexpected token 'd', "data: {"id"... is not valid JSON

After a few of these in a row, the resilient wrapper's circuit breaker
trips and all subsequent compression calls fail with `circuit_breaker_open`,
silently disabling LLM-backed compression / summarisation / reflection.

Reproduced upstream in decolua/9router#1260: 9Router's `handleChatCore`
returns SSE unless `stream: false` is explicit. PR
https://github.com/decolua/9router/pull/1272 fixes the proxy side, but
sending the field explicitly here is defensive — other OpenAI-compatible
endpoints (older self-hosted proxies, vLLM compat shims, …) hit the same
spec gap.

No behavior change for spec-compliant endpoints (openai.com, Azure
OpenAI, well-behaved proxies): they already default to non-streaming
when `stream` is absent, so making it explicit is a no-op there.

Co-authored-by: Ptah-CT <221234802+Ptah-CT@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:34:02 +01:00
Faraz Ahmed 3cb7f90894 fix: read tool_response instead of tool_output in PostToolUse hook (#561)
* fix: read tool_response instead of tool_output in PostToolUse hook

Claude Code's PostToolUse payload sends the field as `tool_response`,
not `tool_output`. The hook was reading `data.tool_output` which is
always undefined, so `cleanOutput` was undefined, the observe request
contained no `tool_output` value, and mem::compress consistently failed
its XML schema validation (requires narrative >= 10 chars + facts >= 1).

Fix: read `data.tool_response` with `data.tool_output` as a fallback
so older integrations that emit the legacy field name keep working.

Fixes #539

* style: remove explanatory comment per repo guidelines
2026-05-20 16:33:42 +01:00
Rohit Ghumare 93d1bdd81a fix(deps): pin iii-sdk to 0.11.2 (closes #555) (#567)
* fix(deps): pin iii-sdk to 0.11.2 to avoid routing regression in 0.11.6

iii-sdk@0.11.6 changes nested behavior so that all /agentmemory/* routes
return 404 against the iii-engine, even though both packages still
satisfy the previous "^0.11.2" semver range. npm picked up the new
version on `npm install -g @agentmemory/agentmemory` after 0.9.21
shipped, silently breaking installs.

Two pin sites:

1. package.json — caret -> exact "0.11.2" so npm cannot drift forward
   on minor releases until the upstream regression is sorted.

2. src/cli.ts — `agentmemory setup` previously ran
   `pnpm up iii-sdk@latest` / `npm install iii-sdk@latest`, which would
   re-pull 0.11.6+ even after a freshly-pinned install. Both call sites
   now pin to 0.11.2 with a label referencing this issue.

Tests (1081) + build pass against iii-sdk@0.11.2.

Closes #555.

* fix(cli): drop issue ref from iii-sdk pin label

Source labels should describe what the code does, not point at issues
that rot as the codebase evolves. Issue context lives in the PR body.
2026-05-20 16:23:32 +01:00
Rohit Ghumare e371be9d98 fix(codex): --with-hooks workaround for openai/codex#16430 (closes #509) (#564)
Codex Desktop currently does not dispatch plugin-local hooks.json even
though both CodexHooks and PluginHooks feature flags are stable +
default-enabled in codex-rs/features/src/lib.rs (openai/codex#16430).
MCP tools still work; lifecycle observations are silently missing.

Adds `agentmemory connect codex --with-hooks` which mirrors the bundled
hooks.codex.json into the user-scope ~/.codex/hooks.json:

- Resolves ${CLAUDE_PLUGIN_ROOT} to the absolute bundled plugin/ path
  (user-scope hooks don't get plugin-root injection)
- Idempotent merge: previous agentmemory entries are stripped on
  reinstall via the resolved scripts/ path prefix; unrelated user
  hooks are preserved untouched
- Preserves matcher fields from the bundled manifest so PreToolUse
  routing still works
- findPluginRoot walks up from import.meta.url to locate the plugin/
  dir; works for both dist/cli.mjs (bundled) and src/ (dev) layouts
- Dry-run path previews both TOML and hooks.json changes

Closes #509.
2026-05-20 15:26:08 +01:00
Rohit Ghumare 7fb72f4010 feat(eval): pluggable benchmark harness with in-house coding-agent corpus (#562)
* feat(eval): pluggable benchmark harness with in-house coding-agent corpus

Adds eval/ tree (outside files field so npm tarball stays thin) with Adapter
interface, three reference adapters (grep / vector / agentmemory-hybrid),
two benchmarks (LongMemEval _s public, coding-agent-life-v1 in-house 15
sessions), scoring (P@K, R@K, hit, top-gold-rank), NDJSON output,
sandbox script.

coding-agent-life-v1 published scorecard at
docs/benchmarks/2026-05-20-coding-agent-life-v1.md:
agentmemory-hybrid R@5=0.967 P@5=0.578 (100% hit) vs grep R@5=0.967 P@5=0.267.
2.2x better precision on identical input, sandbox-reproducible.

Adapter contract: init(sessions, config) -> State; query(q, state, k) -> RankedDoc[]

npm scripts:
  npm run eval:coding-life   (no download, no API key for grep)
  npm run eval:longmemeval   (needs OPENAI key + 278MB download)

eval/scripts/sandbox.sh boots clean agentmemory + iii-engine on ports
3411/3412 with isolated data dir; tears down on exit.

README headline updated. 1072/1072 tests pass + 5 new eval tests.

* fix(eval): address review findings on benchmark harness

- agentmemory adapter: prefer row.sessionId before observationToSession lookup
- vector adapter: validate embedBatch response (length, indexes, non-empty rows)
- coding-life: positive-int guard on --k; wrap query loop in try/finally so teardown runs
- longmemeval: positive-int guards on --k/--limit/--stratify; per-question try/finally
- load: throw on haystack_session_ids vs haystack_sessions length mismatch
- score: P@K denominator is k (requested cutoff) not topK.length
- sandbox.sh: guard rm -rf with non-empty + /tmp/ prefix check
- README: drop unsafe rm "$(which iii)"; instruct ~/.local/bin + PATH instead; add language tag to repo-layout fenced block
- sessions.json: fix "two-phase" -> "three-phase" wording mismatch
2026-05-20 14:11:52 +01:00
Rohit Ghumare e9dc710e56 ci: cross-platform matrix + paths-ignore + concurrency (#556)
* ci: cross-platform matrix + paths-ignore + concurrency

1. **OS matrix** — Linux + Windows + macOS, both Node 20 + 22. 6 cells,
   ~3min each, ~18min wall time. Direct test against the class of
   bug #487 caught: hooks crashing on Windows usernames with spaces.
   Pre-merge Linux-only CI meant that bug landed in main + a release.
   fail-fast: false so a flake on one cell doesn't mask whether the
   same failure reproduces elsewhere.

2. **paths-ignore** — skip CI runs on README / CHANGELOG / docs /
   website / assets / .md / .mdx pushes. ~half the runner minutes
   back on doc-only churn. Source / config / workflow changes
   always run.

3. **concurrency + cancel-in-progress** — PR force-pushes cancel
   in-flight runs instead of piling them up. Push to main protected
   (concurrency group still scoped to ref, no cancel for main pushes).

Plus minor hardening: persist-credentials: false on the checkout
step so the GITHUB_TOKEN doesn't land in .git/config.

What was NOT lifted (rationale per plan):
- Per-package reusable workflows (Rust/Python/Homebrew — non-TS).
- License-header check (no per-file Apache banners in agentmemory).
- CLA bot (defer until external PR volume justifies friction).
- tsc --noEmit lint job (codebase has ~10 pre-existing type errors
  tsdown skips; gating CI on those would block every PR until
  fixed; tracked as separate cleanup).
- Smoke test (`agentmemory demo + livez`) — defer to its own PR
  with its own validation cycle.
- Codecov badge — defer until baseline is set.

* ci(windows): force bash shell so build script's POSIX idioms work

Windows runners default to cmd.exe for npm run scripts; the build
script uses POSIX patterns the build script's exit codes
(`cp ... 2>/dev/null || true`, `mkdir -p`) that cmd doesn't
parse. ubuntu + macos already use bash by default so this is
Windows-only behaviour change.

Alternative: rewrite the build script in Node. Bigger lift, not
minimal.

* ci(windows): point npm script-shell at git-bash before build

`shell: bash` on the step only sets the shell for the step's own
runner; `npm run` still spawns its inner script via npm's
`script-shell` config, which defaults to cmd.exe on Windows.

Configure npm to use Git-Bash (preinstalled on GitHub-hosted
Windows runners) so `npm run build` and `npm run test` execute
the build script the same way ubuntu + macos do.

Step is gated on `runner.os == 'Windows'` so it's a no-op on the
other matrix cells.

* ci: drop windows-latest from matrix (obsidian-export hardcoded POSIX paths)

Windows runners fail on test/obsidian-export.test.ts because the
test + src hardcode `/tmp/...` POSIX paths that don't resolve on
the D:\ drive Windows uses. Fixing it cleanly requires reworking
src/functions/obsidian-export.ts to use os.tmpdir() + path.join,
which is a separate scope.

Drop windows from the matrix for now. Ship ubuntu + macos coverage
(real darwin/linux divergence catch) and file a follow-up to make
obsidian-export cross-platform so Windows can be added back.

* test(fs-watcher): bump waits to 1500ms + describe retry for macos fsevents flake
2026-05-20 11:10:46 +01:00
Rohit Ghumare 1838f4d74c chore(release): v0.9.21 (#551)
Quality + integration wave. Bundles 11 PRs since v0.9.20:

Contributor feature:
- #237 OpenCode plugin with 22 auto-capture hooks (@cl0ckt0wer)

Bug fixes (9):
- #516 memory_recall endpoint + format/token_budget (@serhiizghama, closes #507/#440)
- #461 env-file AGENTMEMORY_DROP_STALE_INDEX flag honored (@honor2030, closes #456)
- #487 Windows hook path quoting (@honor2030, closes #477)
- #517 viewer IME composition guard (@jonathanzhan1975)
- #472 chunk large sessions for LLM context window (@efenex)
- #473 surface lessons in smart-search + diagnose tally (@efenex)
- #486 declare all Hermes plugin hooks (@honor2030)
- #500 rebuildIndex non-blocking on boot (@efenex)
- #504 batched embed in rebuildIndex (25h -> 3h) (@efenex)
- #491 cli skip onboarding without tty (@honor2030)

Upstream-installer revert:
- #546 drop --next workaround now that iii-hq/iii#1660 shipped

1067/1067 tests pass across 95 files.
v0.9.21
2026-05-19 19:37:19 +01:00
efenex cb2e0131dc fix(visibility): surface lessons in smart-search + tally per-store in diagnose (#473)
* fix(visibility): surface lessons in smart-search + tally per-store in diagnose

Two related UX gaps in the memory layer's reflection surfaces. A consumer
that calls `memory_lesson_save` and gets `success:true` reasonably expects
to find the lesson via `memory_smart_search` ("did my save land?") and
to see it counted in `memory_diagnose` ("what's in the store?"). Neither
was true: lessons live in their own KV store (`KV.lessons`), and both
diagnostic surfaces only looked at `KV.observations` / `KV.memories`.
A 4,350-lesson store could read as "memories: 0" on diagnose and return
zero hits on smart_search — the trust-shock that prompted this fix.

A) mem::smart-search: also return lessons in the compact response.
   - New optional `project` and `includeLessons` (default true) params.
   - Delegates lesson scoring to the existing mem::lesson-recall via
     sdk.trigger, so confidence + recency weighting stays consistent
     with mem::lesson-recall (no duplicate scoring logic).
   - Lessons come back in a separate `lessons` field on the response,
     not merged into `results`. Existing consumers reading `results`
     are unaffected; new consumers can read `result.lessons` too.
   - Content truncated to 240 chars in compact mode (full content
     remains available via mem::lesson-recall directly).
   - Lesson-recall failures are soft: log + return empty lessons,
     observation results still flow through.

B) mem::diagnose: add per-store tally categories for lessons,
   summaries, semantic, procedural, crystals, insights. Mirrors the
   existing `memories` pattern: count + light consistency check
   (confidence range for scored memories; non-empty title/narrative/
   steps for the rest). Each new category is in ALL_CATEGORIES so
   `--categories lessons` filtering works as expected.

The empty-system pass count goes from 8 to 14 (8 original + 6 new
stores). Test updated accordingly.

- src/types.ts: add CompactLessonResult
- src/functions/smart-search.ts: lesson recall + merge (single-call
  path unchanged, expand mode unchanged)
- src/functions/diagnostics.ts: six new category blocks before mesh
- test/smart-search.test.ts: 6 new cases (lesson inclusion, content
  preview truncation, includeLessons=false opt-out, project filter
  passthrough, soft-fail on recall error / non-success response)
- test/diagnostics.test.ts: 7 new pass/warn cases for each new
  category + filter check; empty-system pass count bumped 8→14

43/43 tests pass.

* fix(diagnostics): defensive guards on new validators (CodeRabbit #473 review)

CodeRabbit flagged two patterns in the per-store validators added in
the parent commit:

1. .trim() on .title / .narrative was unconditional — a corrupted row
   with title=null or title=42 would throw, abort the whole diagnose
   run, and silently skip every later category. Add typeof guards.

2. confidence range checks were `< 0 || > 1` which silently passes
   NaN and Infinity (NaN < 0 is false, NaN > 1 is false → "healthy").
   Add Number.isFinite(...) prefix so corrupted scored rows surface
   as warnings instead.

Applied across all 6 new validators: lesson confidence, summary title,
semantic confidence, crystal narrative, insight confidence.

Tests added in test/diagnostics.test.ts under "defensive row-shape
handling": NaN confidence on a lesson, null summary title (verifies
diagnose still completes and later categories still execute),
undefined crystal narrative, Infinity / NaN on insight + semantic.

34/34 tests pass.
2026-05-19 19:29:46 +01:00
efenex e68d4ebe9b fix(summarize): chunk large sessions to fit LLM context window (#472)
* fix(summarize): chunk large sessions to fit LLM context window

JSONL-imported sessions can have far more observations than the
500-cap MAX_OBS_PER_SESSION that constrains native sessions.
mem::summarize previously built one prompt containing every
observation and shipped it as a single LLM call, which exceeded the
provider's context window for sessions >~7,000 observations and
returned an unhelpful 400 from upstream — silently leaving large
bulk-imported sessions out of the semantic tier.

Approach: map-reduce inside mem::summarize.
- Sessions ≤ SUMMARIZE_CHUNK_SIZE (default 400) take the legacy
  single-call path with no overhead
- Larger sessions are split into chunks, each summarized with the
  existing per-session prompt in parallel batches of
  SUMMARIZE_CHUNK_CONCURRENCY (default 6), and partial summaries
  merged via a new REDUCE_SYSTEM prompt
- Per-chunk retry-once on transient parse / provider errors
- Persistently-failing chunks are skipped (not propagated) so a
  flaky chunk doesn't waste 30+ already-completed LLM calls on
  the same session
- Bail with too_many_chunks_skipped only if >50% of chunks fail

Companion operator tool: scripts/backfill-imported-sessions.sh
walks jsonl-imported sessions and POSTs mem::summarize per session,
with project / agent / obs-count filters, cost estimation, and
per-failure payload dumping for debugging provider rejections.

Validated locally against a real corpus:
- 5,392-obs session (14 chunks, c=6): 39s
- 10,704-obs session (27 chunks, c=6): 34s
- 105,966-obs session (265 chunks, c=50): handler completes
  server-side and persists
- 52-session bulk backfill → 25 new semantic facts + 6 new reflect
  insights produced by consolidate-pipeline

Known limit: iii-engine has a hardcoded 180s function-invocation
timeout. Sessions large enough that chunked summarize wallclock
exceeds that will return a timeout/500 to the HTTP client even
though the handler completes and persists server-side. High-RPM
providers (Novita / DeepInfra / DeepSeek typically allow 100+
concurrent) can raise SUMMARIZE_CHUNK_CONCURRENCY to push the
cliff well past any realistic session size. True fix is an
async-job pattern; left as follow-up.

- src/prompts/summary.ts: add REDUCE_SYSTEM + buildReducePrompt
- src/functions/summarize.ts: chunking, retry, skip, parallelism
- test/summarize.test.ts: 9 cases covering single-call path,
  chunking, env-override, retry-then-success, persistent skip,
  too-many-skipped bail, provider error after retry, concurrency
- .env.example: document SUMMARIZE_CHUNK_SIZE / _CONCURRENCY
- .gitignore: agentmemory-debug/, data-*/  (operator artefacts)
- scripts/backfill-imported-sessions.sh: bulk-import backfill tool

9/9 new tests pass; existing tests untouched.

* fix(summarize): address CodeRabbit review on #472

Four nits flagged by the automated reviewer, all worth fixing:

- scripts/backfill: add curl --connect-timeout + --max-time profiles
  (META_CURL_OPTS vs WORK_CURL_OPTS). Metadata reads fail fast and
  retry on transient blips; LLM-backed work calls get a wide 30-min
  cap and no retry (retrying a half-finished LLM job double-spends).
- scripts/backfill: sanitize sessionId before joining with DEBUG_DIR
  in dump_failure() (otherwise a session id containing `/` or `..`
  could escape the debug dir). UUIDs in practice, but the server
  doesn't enforce that.
- scripts/backfill: switch the observations query to
  `--get --data-urlencode "sessionId=$id"` so special characters
  can't corrupt the query string.
- scripts/backfill: guard `jq` on summarize + consolidate responses
  with `jq -e . </dev/null 2>&1` first. iii's HTTP layer occasionally
  returns non-JSON (HTML 5xx, empty body on timeout). Without the
  guard, `set -e` aborts the whole backfill loop on a single bad
  response — now it logs `invalid_json_response` and moves on.
- test/summarize.test.ts: fix `vi.mock("./audit.js", ...)` path to
  `"../src/functions/audit.js"`. The old path resolved to
  `test/audit.js` (nonexistent), so the mock was a silent no-op.
  Tests passed anyway because `safeAudit` writes to a mocked KV.

9/9 tests still pass; backfill dry-run still resolves the corpus
cleanly.
2026-05-19 19:29:06 +01:00
efenex 6c2a689daa fix(rebuild): batch embed calls in rebuildIndex (25h → 3h on large corpora) (#504)
* fix(rebuild): batch embed calls in rebuildIndex (25h → 3h on large corpora)

rebuildIndex called `await vectorIndexAddGuarded(...)` per memory and
per observation. Each call is one HTTP round-trip to the embedding
provider for a single input. On a 500k-observation imported corpus
against an embedding endpoint with even modest latency, that's
serial 100-200ms per call = 14-28 hours of wallclock. The new
non-blocking rebuild path (#500) made this no longer block boot, but
the rebuild itself still takes the same wallclock.

Add `vectorIndexAddBatchGuarded()` next to the existing per-item
helper, accepting an array of items and calling `provider.embedBatch()`
once. For batchable endpoints (vLLM, Triton, OpenAI's `/v1/embeddings`
all accept an `input` array), latency for N items is roughly the
latency of a single embed because network + GPU setup amortize.

Refactor `rebuildIndex` to accumulate items into a buffer and flush
every REBUILD_EMBED_BATCH_SIZE (default 32). BM25 add stays
per-item-synchronous; only the vector path is batched.

Validated against a vLLM Qwen3-Embedding-8B endpoint:
  - single embed: 175ms
  - batch-of-32:  737ms (= 23ms/item amortized, ~7.6× speedup)
  - projected backfill time for 500k obs: 25h → 3h

Per-item failure shape is preserved:
  - whole-batch network/provider error → all skipped, single warn line
    (vs N warns previously when the same error hit every item)
  - per-item dimension mismatch → that item skipped, others continue
  - rebuildIndex return value unchanged (count of attempted items)

Override knob:
  - REBUILD_EMBED_BATCH_SIZE (default 32) — set lower for endpoints
    with small per-request input limits, higher for endpoints that
    prefer larger batches. Set to 1 to fall back to the per-item path.

39/39 existing tests in search-index/vector-index/remember-bm25-index
pass unchanged.

Related: #500 (non-blocking rebuildIndex), #503 (separate embedding
base URL).

* fix(rebuild): per-item vi.add try/catch to preserve soft-fail

Restores the pre-batch soft-fail behavior — a single failing
vi.add() no longer aborts the entire rebuild batch. Failures
are logged and counted toward fail, just like dimension
mismatches above.
2026-05-19 19:27:33 +01:00
efenex c6a1fec2f1 fix(boot): make rebuildIndex non-blocking so viewer + later boot steps run (#500)
mem::observe's boot flow had this sequence in main():

  1. registerSearchFunction / registerContextFunction / ...
     (sync — completes immediately)
  2. restore persisted vector index from disk
  3. await rebuildIndex(kv)        ← blocks here
  4. bootLog "Ready" / "REST API" / "MCP surface"
  5. startViewerServer(...)
  6. setInterval auto-forget / lesson decay / consolidation

rebuildIndex iterates every observation across every session and AWAITS
an embedding-provider call per record. On a large corpus + a rate-limited
embedding endpoint (e.g. 100 RPM), step 3 takes hours to days.
Everything that runs AFTER it — including startViewerServer — is
silently delayed for the same duration.

Symptoms in the wild:
- http://localhost:3113/ unreachable (no listening socket on the viewer
  port) even on a freshly-started server
- `agentmemory doctor` reports "viewer-unreachable"
- log floods with `vector-index add: embed failed — skipping {429: ...}`
  from the still-running rebuild burning rate-limit budget
- no error message — the worker stays alive serving HTTP because
  sdk.registerFunction had already completed synchronously in step 1

Fix: detach rebuildIndex with `void` + .then/.catch instead of awaiting.
The index lazily fills in over time, search degrades gracefully (BM25
keeps working immediately, vector results fill in as the embed queue
drains), and the viewer comes up in seconds.

Repro on the operator side:
1. import a sizeable jsonl corpus (`mem::replay::import-jsonl`)
2. clear the persisted vector index so rebuildIndex runs on next boot
3. restart agentmemory with EMBEDDING_PROVIDER pointed at a rate-limited
   endpoint (any OpenAI-compat with low RPM)
4. observe: REST API responds on :3111, but :3113 is never bound, and
   the doctor's "viewer-unreachable" check fires until the rebuild
   finishes (hours-to-days for a 300+ session corpus)

The 5-second non-fix workaround was a hard kill + restart; that just
re-entered the same hang.

No tests added — main() isn't unit-tested today and wiring up a fake
slow rebuildIndex + asserting the post-rebuild boot lines run early
would need the full worker mock harness. The change is one line and
the failure mode is dramatic; visual review + integration smoke covers
the regression risk.
2026-05-19 19:20:18 +01:00
이민재 c1c2c3a944 fix(hermes): declare all plugin hooks (#486)
* fix(hermes): declare all plugin hooks

* test(hermes): compare manifest hooks to provider

---------

Co-authored-by: honor2030 <19909783+honor2030@users.noreply.github.com>
2026-05-19 19:19:24 +01:00
이민재 db9f000dca fix(cli): skip onboarding prompts without a tty (#491)
Co-authored-by: honor2030 <19909783+honor2030@users.noreply.github.com>
2026-05-19 19:18:48 +01:00
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 6ed47c155a fix(funding): tighten FUNDING.yml to canonical single-line form (#547)
Sponsor button still missing from the repo page despite #545 merging.
The committed FUNDING.yml started with 4 lines of `#` comments before
the canonical `github: [rohitg00]` directive. GitHub's FUNDING parser
documents only the canonical key-value form; leading comments
shouldn't break it but some users have reported indexer lag when the
file starts with non-data lines. Strip to the bare single-line form
to match the documented schema and remove any ambiguity.

Sponsor profile is enabled (github.com/sponsors/rohitg00 returns 200
+ 'Sponsor @rohitg00' button), so the only remaining gap is GitHub's
side-bar indexing. Tightening the file forces a re-parse.
2026-05-19 18:45:04 +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
Rohit Ghumare 2a027d8952 revert: drop --next workaround for iii-console installer (upstream #1660 shipped) (#546)
v0.9.19 (#460 / commit bb259ac) routed the first-run iii-console
install through `bash -s -- --next` to dodge the upstream tag-prefix
bug at iii-hq/iii#1652. Upstream PR iii-hq/iii#1660 fixed the bug on
2026-05-19 — installer's jq filter now accepts both `iii/v...` and
bare `v...` tags, and `-v X.Y.Z` falls back gracefully.

`install.iii.dev/console/main/install.sh` is a thin proxy serving
`raw.githubusercontent.com/iii-hq/iii/main/console/install.sh` with
a 5-minute CDN cache — verified byte-for-byte that the live URL
already serves the post-#1660 fix. No iii release tag needed.

Switch agentmemory back to the canonical bare invocation:

  curl -fsSL https://install.iii.dev/console/main/install.sh | sh

Drops the workaround comment block (10 lines) explaining the prior
detour. v0.9.19/v0.9.20 users on the `--next` path will still
resolve a valid release (next-release lookup also handles
`iii/v...-next.*` correctly post-#1660), so this isn't a forced
upgrade.

1038/1038 tests pass.
2026-05-19 18:23:28 +01:00
Serhii Zghama c2f231fe8b fix(mcp): memory_recall hits the right endpoint and forwards format/token_budget (#507) (#516)
* fix(mcp): route memory_recall to /agentmemory/search and forward format/token_budget

memory_recall and memory_smart_search were sharing the smart-search
endpoint, which always returns compact mode and silently drops the
format and token_budget parameters that the tool schema advertises.
Split the cases so memory_recall hits /agentmemory/search (which
honors format) while memory_smart_search keeps its own endpoint.
Default format to "full" for memory_recall so the documented behavior
matches the wire call.

Signed-off-by: serhiizghama <zmrser@gmail.com>

* test(mcp): cover memory_recall endpoint, format forwarding, and defaults

Two new proxy tests for issue #507: one asserts memory_recall calls
POST /agentmemory/search with the format and token_budget fields,
and never falls through to smart-search; the other pins the default
format to "full" when the caller omits it.

Signed-off-by: serhiizghama <zmrser@gmail.com>

---------

Signed-off-by: serhiizghama <zmrser@gmail.com>
2026-05-19 13:01:21 +01:00
이민재 48bf700f62 fix(hooks): quote plugin script paths (#487)
Co-authored-by: honor2030 <19909783+honor2030@users.noreply.github.com>
2026-05-19 12:59:22 +01:00
이민재 a74c2880d5 fix(config): honor env file drop-stale-index flag (#461)
Co-authored-by: 이민재 <19909783+honor2030@users.noreply.github.com>
2026-05-19 12:57:47 +01:00
Jz 3a24c326b8 fix(viewer): prevent IME composition interruption in search inputs (#517)
The viewer's five search inputs (graph, memories, lessons, actions,
crystals) destroy and recreate their input DOM via innerHTML on every
keystroke, which interrupts active IME composition sessions and makes
non-Latin input (Chinese, Japanese, Korean) unusable.

Additionally, the viewer's CSP includes script-src-attr 'none', which
silently blocks the inline oninput=/onchange= handlers on the lessons,
actions, and crystals panels. Those three search/filter controls have
been non-functional under the strict CSP.

This patch:

1. Adds a bindImeSafeSearch helper that guards on both an explicit
   compositionstart/compositionend flag and event.isComposing.
   compositionend triggers an immediate commit and sets a
   justCommitted one-shot flag to suppress the redundant trailing
   input event that browsers dispatch after compositionend.

2. Adds captureSearchFocus/restoreSearchFocus helpers to preserve
   focus and cursor position across innerHTML rebuilds, so multi-word
   IME input doesn't require clicking back into the search box after
   each commit.

3. Migrates all five search inputs to addEventListener via the new
   helpers, removing the CSP-blocked inline handlers on lessons,
   actions, and crystals. The actions panel's status filter <select>
   is also migrated for the same reason.

4. Unifies debounce delay to 200ms across all five panels.

Verified via:
- 8/8 jsdom + synthetic CompositionEvent regression cases (ASCII
  debounce, IME composing suppresses input, compositionend immediate
  commit + justCommitted suppression, post-IME ASCII resumes, fast
  typing coalesces, composition cancel returns to idle).
- 8/8 manual browser cases on Windows + Chrome (Chinese pinyin commit,
  multi-word IME without re-focusing, English regression, all five
  panels, zero CSP violations in DevTools, cursor position retained).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 11:49:07 +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 f1e7b2c577 fix(website): move Product Hunt badge into FeaturedIn (correct post_id 1144164)
Earlier PH badge URL pointed at post_id 773058 which returned 404 —
the WebFetch I pulled the ID from grabbed a wrong number. Correct
post_id is 1144164 per the official embed snippet from the PH
"Embed" tab. Badge now resolves with HTTP 200 + Content-Type
image/svg+xml against api.producthunt.com.

Position fix: pulled the badge out of the Testimonials section
(wrong place — buried below the fold) and added Product Hunt as a
fourth entry in the existing FeaturedIn "AS FEATURED IN" bar at the
top of the page where it sits naturally alongside AlphaSignal,
Agentic AI Foundation, and Trendshift.

- FeaturedIn ITEMS: Product Hunt entry added with the canonical
  featured.svg embed URL (theme=neutral to match the existing
  badges, no t= cache-buster since CDN cache is fine for a
  marketing surface)
- FeaturedIn grid: 3-col → 4-col at desktop, 2-col at 980px,
  1-col at 540px so the four cells stay balanced
- next.config: api.producthunt.com added to remotePatterns so the
  Next image loader accepts the SVG badge
- Testimonials: removed the broken badge row + the now-unused CSS
  (.badgeRow / .badgeLink / .badge); section keeps the
  use-cases + endorsements grid as before
2026-05-18 21:42:06 +01:00
Rohit Ghumare af1ba5b5e9 fix(website): update Peter Neyra quote to Cursor-backfill use case
PH discussion comment #5379518 is the specific, meatier framing of
how Peter is using agentmemory: backfilled a full month of Cursor
agent transcripts and saw the system pick up on pivots that he had
moved away from. Concrete tool name + concrete time horizon reads
much harder than the generic "tracking product pivots" framing
that ended up in the earlier card.

Also pointed the use-case card's href at the specific comment ID so
hover-target lands on the right reply, not the thread top.
2026-05-18 20:58:00 +01:00
Rohit Ghumare 255d8030e9 feat(website): split Testimonials into use-cases + endorsements + PH badge
The launch-thread quotes broke into two natural shapes — concrete
use cases (Peter Neyra tracking product pivots, Pranav Prakash on
two weeks of production use) deserve more weight than the shorter
positioning endorsements. Top of section now embeds the official
Product Hunt featured.svg badge so the live upvote count + day
rank stays current without any client refresh.

- Featured "HOW THEY USE IT" row: 2 large cards with a gold use-case
  tag chip + larger quote type
- "WHAT THEY SAY" row: 4 compact cards in a 4-col grid
- PH badge embedded at api.producthunt.com/widgets/embed-image/v1/
  featured.svg?post_id=773058&theme=dark — live, no client JS
- Responsive: use-cases 2→1 col at 980px, endorsements 4→2→1
2026-05-18 20:52:39 +01:00
Rohit Ghumare 57ceaf45c7 feat(website): add Testimonials section with PH launch-thread quotes
Six verbatim quotes pulled from the Product Hunt launch discussion,
each linked back to the source comment for verification. Quotes
cover the spectrum: concrete use cases (Peter Neyra on product-pivot
tracking, Pranav Prakash on two weeks of production use), framing
endorsements (Alper Tayfur, Mia Taylor on agentmemory's intent
versus storage-only tools), and onboarding feel (Zoe Alexandra).

Section header: "BUILDERS USING AGENTMEMORY" with the title
"IN THE WILD." in the gold accent. 3-column grid above 980px,
2-column at tablet, single-column on mobile. Cards are clickable
and route to the original PH comment thread.

Slotted between Compare and Agents — social proof after the
comparison table, before the agent-list grid. Matches the
existing FeaturedIn aesthetic (charcoal border + iron fill +
gold hover) so the section feels native, not bolted on.

website/lib/generated-meta.json also picked up the v0.9.20 bump +
the two new MCP tools shipped via #498 (memory_commit_lookup,
memory_commits) — clean by-product of `npm run build`.

- website/components/Testimonials.tsx: 6 verbatim quotes + attribution
- website/components/Testimonials.module.css: 3/2/1 grid + card chrome
- website/app/page.tsx: wired between Compare and Agents
- website/lib/generated-meta.json: regenerated against v0.9.20

Site builds clean.
2026-05-18 20:34:04 +01:00
Rohit Ghumare 08d781d431 chore(release): v0.9.20 — hotfix Codex Stop revert (#501)
v0.9.19 shipped #495 which chained session-end.mjs after stop.mjs on
the Codex Stop hook. Field-testing surfaced the underlying issue:
Codex fires Stop multiple times within a single conversation (once
per assistant turn), so chaining session-end marked sessions as
completed while later observations were still arriving.

#501 reverts the chain. Stop returns to summarize-only behavior. The
SessionEnd-shaped solution (a dedicated terminate event the agent
sends only once on real session end) tracks at #493.

Files bumped (9):
- package.json, packages/mcp/package.json
- plugin/.claude-plugin/plugin.json, plugin/.codex-plugin/plugin.json
- src/version.ts, src/types.ts
- src/functions/export-import.ts
- test/export-import.test.ts
- CHANGELOG.md

1034/1034 tests pass.
v0.9.20
2026-05-18 19:20:48 +01:00
Nanami a953cadb62 Revert "fix: end Codex sessions on Stop hook (#495)" (#501)
This reverts commit abeec1d0b7.

Signed-off-by: Nanami <Rex57@users.noreply.github.com>
Co-authored-by: Nanami <Rex57@users.noreply.github.com>
2026-05-18 19:17:26 +01:00
Rohit Ghumare 29e6ebdcc0 docs(changelog): credit contributors on v0.9.19 entries 2026-05-18 14:49:19 +01:00
Rohit Ghumare bb259acedc chore(release): v0.9.19 — route around upstream iii-console installer bug
`install.iii.dev/console/main/install.sh` filters releases with
`startswith("v")` while `iii-hq/iii` tags as `iii/v0.12.0` —
slash-prefixed. Every stable release is filtered out and the script
bails with "no stable iii release found", breaking first-run console
install for every fresh agentmemory user.

Switch our prompt to pass `--next` through the installer. The next-
release lookup uses a regex on `-next.` without the buggy
`startswith` constraint, so it succeeds against the same tag set and
installs the most recent prerelease. Inline comment documents the
upstream tag-prefix bug + the revert condition for when the
installer is patched.

Files bumped (9):
- package.json
- packages/mcp/package.json
- plugin/.claude-plugin/plugin.json
- plugin/.codex-plugin/plugin.json
- src/version.ts
- src/types.ts
- src/functions/export-import.ts
- test/export-import.test.ts
- CHANGELOG.md

1007/1007 tests pass.
v0.9.19
2026-05-18 13:57:23 +01:00
Rohit Ghumare caa9f5223e fix(plugin): inherit AGENTMEMORY_URL/SECRET via env passthrough (#375) (#460)
The bundled plugin .mcp.json + the `agentmemory connect <agent>` wiring
both hardcoded the MCP server to localhost:3111 with no env-var seam.
Users running agentmemory remotely (k8s cluster-internal DNS, reverse
proxy with API key auth) hit a permanent /doctor warning because they
had to add a second entry with the right AGENTMEMORY_URL + SECRET, and
both entries fought for the "agentmemory" name.

Both files now use ${AGENTMEMORY_URL} / ${AGENTMEMORY_SECRET}
expansion. Host (Claude Code, Cursor, etc.) substitutes the shell
value at MCP-server launch; when vars are unset the host passes empty
string, which the standalone shim treats as missing and falls back to
http://localhost:3111. One wired entry now covers both local and
remote without duplicates.

- plugin/.mcp.json: env block added with ${VAR} expansion
- src/cli/connect/util.ts: AGENTMEMORY_MCP_BLOCK env updated; comment
  documents the empty-string fallback contract
- test/cli-connect.test.ts: regression test asserting the env block
  shape after install() — protects against future regressions where
  the literal hardcoded URL leaks back in

1008/1008 tests pass.
2026-05-18 11:22:05 +01:00
Nanami abeec1d0b7 fix: end Codex sessions on Stop hook (#495)
Signed-off-by: Nanami <Rex57@users.noreply.github.com>
Co-authored-by: Nanami <Rex57@users.noreply.github.com>
2026-05-18 11:20:40 +01:00
Rohit Ghumare 06248fd199 refactor(providers): extract OpenAI transport into shared module (#371) (#462)
* refactor(providers): extract OpenAI transport into shared module (#371)

OpenAIProvider (LLM) and OpenAIEmbeddingProvider (embedding) both
speak the OpenAI wire shape over POST /v1/chat/completions and POST
/v1/embeddings — and both need to swap to the Azure shape when the
base URL lands at *.openai.azure.com (api-key header instead of
Authorization: Bearer, api-version query param, no /v1/ prefix
because the deployment is already in the URL).

Until now only the LLM provider knew about Azure. The embedding
provider hardcoded /v1/embeddings + Bearer auth, so any user pointing
OPENAI_BASE_URL at an Azure resource saw their LLM calls route
correctly while embedding calls 404'd / 401'd.

New src/providers/_openai-shared.ts owns:

  detectAzure(baseUrl)
  buildChatUrl(baseUrl, isAzure, apiVersion)
  buildEmbeddingUrl(baseUrl, isAzure, apiVersion)
  buildAuthHeaders(apiKey, isAzure)
  normalizeBaseUrl(raw)

Both classes import these helpers. The embedding provider gains
Azure auto-detection via the same OPENAI_BASE_URL hostname check the
LLM provider already uses, plus the same OPENAI_API_VERSION knob
(default 2024-08-01-preview). Closes the gap users hit when mirroring
an Azure deployment on both surfaces.

Net delta: ~40 LOC of duplicated transport collapsed into one shared
file; both provider files are smaller and focused on their surface's
unique concerns (LLM owns max_tokens / reasoning_effort / timeout
precedence; embedding owns the MODEL_DIMENSIONS table).

Approach diverges from the issue body (single class implementing both
interfaces): the existing factory contracts at
src/providers/index.ts dispatch LLM and embedding independently
(detectProvider vs detectEmbeddingProvider), and merging into one
class would force constructors to coordinate detection across two
config surfaces. Shared transport keeps factories clean while
delivering the DRY win and the Azure-on-embeddings parity gain.

- src/providers/_openai-shared.ts: new shared transport module
- src/providers/openai.ts: import shared helpers; drop local
  detectAzure + private buildUrl/buildHeaders
- src/providers/embedding/openai.ts: import shared helpers; Azure
  auto-detection via OPENAI_BASE_URL host check, OPENAI_API_VERSION
  query param when Azure-shaped
- test/openai-shared.test.ts: 17 cases covering all four helpers
  plus end-to-end embedding Azure shape (asserts URL path + api-key
  header substitution via mocked fetch)

1023/1023 tests pass.

* fix(providers): use URL API for Azure path composition (review)

Inline review on #462 surfaced an edge-case bug in the azureUrl
helper: it tested baseUrl for an existing query string to pick the
?/& separator, then string-concatenated the route path BEFORE that
separator. If a baseUrl arrived with pre-existing query params
(corporate proxy carrying a tenant token, diagnostics endpoint,
etc.), the route path got interpolated into the query string,
producing a malformed URL like:

  baseUrl: https://proxy.example.com/openai/deployments/d?tenant=acme
  result:  https://proxy.example.com/openai/deployments/d?tenant=acme/chat/completions&api-version=2024-08-01-preview
                                                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ in the query, not the path

Switch to the URL API: parse baseUrl, normalise the existing
pathname (strip trailing slashes), join the route path with a
single boundary slash, set api-version via url.searchParams.set
(handles encoding without a manual encodeURIComponent), then
toString. Existing query params are preserved.

Test header normalisation pass: the OpenAIEmbeddingProvider
Azure-detection tests captured RequestInit.headers as
Record<string,string>. buildAuthHeaders happens to return a plain
record today, but a future contributor migrating to
HeadersInit/Headers would have these tests silently pass-but-stale
because Record-style access would no longer match. Wrap the capture
in new Headers(init?.headers) so the assertions go through .get()
and tolerate any of the three valid HeadersInit shapes.

Skipped — replacing DEFAULT_AZURE_API_VERSION with /openai/v1: the
v1 Azure pattern is a recent addition and many deployments still
require the api-version query param. Switching the default would
break those deployments. Separate opt-in flag (e.g.
OPENAI_AZURE_API_STYLE=v1) is the right shape but out of scope for
this minimal-change review pass.

- src/providers/_openai-shared.ts: azureUrl rewritten on URL API
- test/openai-shared.test.ts: two new cases (query-param
  preservation, trailing-slash normalisation); embedding Azure
  tests switched to Headers API + .get() / .toBeNull()

1025/1025 tests pass.

* feat(providers): support Azure OpenAI v1 URL pattern (review)

Azure shipped a v1 GA endpoint pattern that mirrors the OpenAI wire
shape one-for-one:

  POST https://<resource>.openai.azure.com/openai/v1/chat/completions

No api-version query param, no /deployments/<dep> path segment —
deployment name is passed in the request body as `model`. Existing
OpenAI clients work as drop-in. The legacy URL pattern
(/openai/deployments/<dep>/chat/completions?api-version=<date>) is
still supported by Azure indefinitely for back-compat.

Verification of the inline review's "remove DEFAULT_AZURE_API_VERSION
and migrate all Azure calls to /openai/v1" instruction: removing it
flat would break legacy users mid-flight (their existing
OPENAI_BASE_URL=https://r.openai.azure.com/openai/deployments/d
configs would still route through the legacy path and 400 without
api-version). Hybrid auto-detect off the URL shape is the right
shape:

  - baseUrl pathname contains `/openai/deployments/` -> legacy style,
    api-version still appended (DEFAULT_AZURE_API_VERSION fallback)
  - otherwise (bare resource host, /openai prefix, /openai/v1 prefix)
    -> v1 style, no api-version, route through /openai/v1/<path>

Users migrate by stripping the /openai/deployments/<dep> suffix from
their OPENAI_BASE_URL — no env-var changes required.

azureStyleOf(baseUrl) is the new detection helper. v1AzureUrl
preserves any pre-existing query params on the base URL (corporate
proxy diagnostics tokens) but never appends api-version. The
/openai/v1 prefix is normalised so configurations like
OPENAI_BASE_URL=https://r.openai.azure.com/openai or
.../openai/v1 don't double-prefix.

- src/providers/_openai-shared.ts: azureStyleOf + v1AzureUrl;
  legacyAzureUrl (renamed from azureUrl) is unchanged; module header
  comment documents both styles and the migration path
- test/openai-shared.test.ts: 3 new cases — v1 routing on bare host,
  /openai or /openai/v1 prefix normalisation, v1 on the embedding
  surface as well; legacy assertion updated to say "Azure legacy" so
  the contrast with v1 is explicit

1028/1028 tests pass.
2026-05-18 11:20:12 +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 4db303b6b9 perf(graph): swap BFS for Dijkstra over edge-weighted graph (#328) (#463)
* perf(graph): swap BFS for Dijkstra over edge-weighted graph (#328)

Memory graph edges carry weights from 0.1 to 1.0 (encoding relation
strength). BFS visits nodes in edge-count order regardless of weight,
so a one-hop weak edge ranked the same as a two-hop chain of strong
edges to the same node — which is the wrong semantics for the
weighted graph the retrieval surface actually queries.

Replace bfsTraversal with dijkstraTraversal:

  cost(edge) = 1 / max(edge.weight, 0.01)

Higher-weight edges cost less, so Dijkstra prefers strong-edge paths.
The clamp floor at 0.01 protects against malformed edges with
weight=0 dividing by zero (the documented weight floor is 0.1, but
imports / older snapshots may carry zeros).

Same swap tightens the perf profile:

  - Adjacency built once in O(V+E) (was: filter allEdges per visited
    node, O(V·E) overall).
  - Min-heap dequeue is O(log V) per pop (was: Array.shift() which is
    O(n) — the dominant cost on graphs above ~200 nodes per the
    contributor's benchmark linked in #328).

Min-heap is a 50-LOC inline class so the file doesn't take a new
dependency for the perf-critical inner loop.

Semantics preserved:

  - maxDepth still bounds edge-count, not accumulated cost. So a deep
    chain of strong edges still terminates at maxDepth hops.
  - One path per reachable node within depth (matches what the
    existing scoring loop consumes).
  - score = avgWeight × (1/pathLength) formula unchanged.

- src/functions/graph-retrieval.ts: dijkstraTraversal + MinHeap
- test/graph-retrieval.test.ts: 4 new cases — weight-optimal path
  selection (the core Dijkstra promise), disconnected-node safety,
  near-zero-weight clamp, maxDepth bound preserved

1011/1011 tests pass.

* fix(graph): exclude startNode from Dijkstra returned paths (review)

Inline review on #463 surfaced a pre-existing bug carried forward
through the BFS-to-Dijkstra refactor: the returned path list
included the startNode's own length-1 path, so the generic
path-scoring loop in searchByEntities ran first — pathLength=1 with
no edges → avgWeight fell to 0.5 (empty-edgeWeights fallback) →
score=0.5 → startNode obs marked visited.

The dedicated score=1.0 fallback loop for startNode obs was then
skipped by the visitedObs guard, making it dead code. So startNode
observations were silently scoring 0.5 instead of the intended 1.0.

Drop the startNode entry from pathTo before returning. The caller's
fallback loop now fires as designed.

- src/functions/graph-retrieval.ts: pathTo.delete(startNode.id)
  immediately before Array.from(pathTo.values()); inline comment
  explains why the fallback would otherwise be dead.
- test/graph-retrieval.test.ts: regression asserts obs_root carries
  score=1.0 and pathLength=0 when its node is the matched start.

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

1007/1007 tests pass.

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

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

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

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

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

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

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

Unify on the global name while keeping back-compat:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: redact quoted sensitive preview keys

---------

Co-authored-by: 이민재 <19909783+honor2030@users.noreply.github.com>
2026-05-17 11:19:36 +01:00