30 Commits

Author SHA1 Message Date
Rohit Ghumare be89b222b0 feat: devin support (cli adapter, plugin, cloud mcp) (#1214)
* feat: devin support replacing windsurf

* feat: devin cli adapter, plugin manifest, and hook payload compat

* fix: stale tool counts in translations and cwd validation
2026-08-16 14:44:10 +01:00
Rohit Ghumare 37ea1b99ad feat: cursor marketplace plugin with hooks, mcp, and skills (#1213)
* feat: cursor marketplace plugin with native hooks and mcp config

* fix: cursor payload compat and transcript prompt backfill in hooks

* fix: plugin-root hook paths, backfill ordering, session-id fallbacks

* docs: cursor plugin rows in readme, translations, and changelog

* fix: cursor native-plugin card, broken agent logos

* chore: sync openclaw and hermes plugin manifest versions

* docs: openclaw hook permission and hermes tool count

* chore: clawhub compat metadata for openclaw plugin

* docs: tested openclaw and hermes install rows in readme
2026-08-16 13:30:40 +01:00
Rohit Ghumare 696cf7abb8 feat: recall quality, provenance, keyless graph, and connector parity (#1205)
* fix: prompt dedup, double summarize, docker-mode stop, hermetic tests

- observe: hash the hook payload when tool_input is absent so prompt_submit,
  notification, and lifecycle events dedup on content instead of collapsing
  onto one shared key that silently dropped every prompt after the first in
  a TTL window (#1173)
- stop hook: drop the direct /agentmemory/summarize POST; /session/end
  already fans out event::session::stopped which runs mem::summarize, so
  every Stop dispatched two full summarizes (#1203)
- cli: refuse to adopt or signal Docker/VM port holders (com.docker.backend,
  vpnkit, colima, ...) as the native engine unless --force; scope Docker-mode
  teardown to agentmemory's own compose services via rm -s -f instead of an
  unscoped down; reap the native worker before Docker teardown instead of
  deleting worker.pid with the process still running (#1151)
- tests: isolate HOME/USERPROFILE for the whole vitest run so suites stop
  reading the developer's real ~/.agentmemory/.env (#1178)

* fix(viewer): live stream port discovery, fresh tab data, honest states

- resolve the stream WebSocket target from /agentmemory/livez (new
  streamsPort field) instead of viewerPort-1 arithmetic, which pointed at
  the wrong server whenever the viewer bound a fallback port and silently
  degraded live updates to 10s polling — verified reaching 'live' on the
  fallback-port case
- refetch tab data on every tab entry; the loaded-once cache meant a
  memory saved by the agent never appeared until a hard browser reload
  (loading placeholders now render only on first load, so background
  refreshes don't flash)
- memories: rows expand on click/Enter to the full stored record —
  content, id, project, created, supersedes, files — plus a collapsible
  raw JSON view
- graph: a 503 with the structured disabled body renders 'Knowledge
  graph is off' with the enableHow text and docs link instead of a
  'query failed / Retry' error that sends users to server logs
- sessions: cards get role=button, tabindex, Enter/Space activation, and
  the detail panel scrolls into view on select; session ids truncate
  head…tail so the distinguishing suffix stays visible
- style search inputs and toolbar buttons on lessons/actions/crystals/
  replay (previously bare native controls); horizontal scroll containment
  for narrow viewports
- demo: only print the semantic-recall success notice when the search
  actually hit; on 0 hits explain the missing embedding key instead

* fix: thread agentId/project through save paths, per-session OpenCode scope

- REST /agentmemory/remember accepts and forwards agentId to mem::remember;
  it previously dropped the field so per-request multi-agent scoping was
  impossible over REST (#1159)
- memoryToObservation() carries the memory's agentId into the search-index
  shape; dropping it made every memory invisible to agent-scoped search
  (#1160)
- MCP memory_save path: the tool schema now exposes agentId, the in-worker
  MCP server forwards it, and the standalone stdio package parses and
  forwards both agentId and project — the stdio pipeline previously dropped
  project even though its schema advertised it (#1197)
- opencode plugin: project/cwd attribution is per-session (resolved from
  the session's own directory at session.created, pruned on session end)
  instead of module-level state that recorded every session in a
  multi-directory OpenCode process under whichever repo loaded the plugin
  first (#1188)

Live-verified: memory saved with agentId=agent-alpha is returned by
smart-search for agent-alpha and hidden from agent-beta.

* feat: hybrid recall everywhere, indexed lessons, provenance, recall hygiene

- mem::search ranks through the full BM25+vector+graph fusion when the
  vector index is populated (injected post-boot via setHybridRanker);
  the primary recall surface was keyword-only while only smart-search
  got hybrid ranking
- fusion weights normalize per item over the streams that actually
  ranked it, with a small explicit cross-stream agreement bonus; the
  old every-enabled-stream denominator permanently penalized
  single-stream hits (the graph stream is empty on default installs).
  Result order is now deterministic (score, best rank, id)
- lessons get a dedicated in-memory BM25 index built lazily from one KV
  list and maintained incrementally on save/delete/decay; recall
  previously listed and substring-scanned the whole corpus per query.
  Confidence x recency composite scoring is unchanged
- mem::remember finds supersession candidates through the search index
  (top-50) instead of walking every memory per save, with a full-scan
  fallback while the index is cold; near-miss similarity (0.4-0.7)
  is reported back as an advisory similarTo hint
- superseded memory versions leave the BM25 and vector indexes; the
  version chain stays in KV for history, but recall no longer returns
  an outdated fact as if current
- every observation and memory now carries an immutable origin block
  (channel: user|agent|tool|import|shared, detail, capturedAt) stamped
  at capture, save, and import, and inherited through both compression
  paths — the base for trust-aware retrieval and ingest screening
- regression tests: supersede index removal, similarTo hint,
  index-backed candidate discovery, lesson index recall/lazy
  rebuild/delete

* feat(viewer): two-pane sessions, navigable dashboard, motion and copy polish

- sessions: list + sticky detail panel side by side above 1100px (the
  detail previously rendered below the whole list, off-screen on any
  real corpus); selected/hover/active states with reserved left border
  so selection doesn't shift layout
- dashboard stat cards for sessions/memories/lessons/crystals/graph
  navigate to their tabs (click or Enter), with hover affordance
- observation subtitles that are raw serialized tool input now display
  the meaningful field (file path, command, pattern, url) instead of a
  JSON blob
- expanded memory rows show the new origin provenance (channel + detail)
- motion: 160ms view entrance, live-badge pulse, both gated behind
  prefers-reduced-motion; tabular numerals in tables
- mobile: header stops wrapping the dateline into the badge row
- lessons/crystals empty-state copy aligned with the header definitions
  (each concept was described two conflicting ways)

* refactor: cleanup pass over the branch diff

- shared test mocks: the three new test files use test/helpers/mocks
  (extended with update, store access, and an opt-in loose trigger)
  instead of three diverging inline copies
- lessons: record cache beside the index takes recall to zero KV
  round-trips (was up to 50 gets per call); the observation adapter
  moved next to memoryToObservation so both record kinds thread new
  fields in one place; dead reset export removed
- mem::search hybrid path carries the observations the ranker already
  loaded instead of refetching every result (halves KV I/O on the
  primary recall path); remember's candidate lookup skips ids that
  cannot resolve as memories and fails open to a full scan
- fusion: derived tiebreak field no longer rides along past the sort;
  comment trimmed to the non-obvious history
- cli: engine identity is a positive check (only the iii binary may be
  adopted or signaled; unknown port holders are refused, not just known
  VM names); worker reap extracted to one helper; demo notice picks its
  branch from a hoisted count
- api: livez and health share one instanceInfo source (health now
  reports streamsPort too) computed once at boot instead of rebuilding
  the merged env per request
- provenance: one importOrigin factory encodes the keep-or-mark rule at
  all three import sites
- opencode plugin: project resolution memoized per directory (was a
  blocking git subprocess per session event); session.created uses the
  entry it just built
- observe: origin channel derived from a named hook set, no nested
  ternary
- viewer: toolbar buttons merged into the .btn rules, one 720px media
  block, generic keyboard activation for role-carrying cards,
  scroll-into-view only on the stacked layout, 5s freshness gate on tab
  refetch (replay stays fetch-once, reason documented), subtitle
  humanizer covers the capture-side key variants

* feat(viewer): clarity pass and ambient refresh

- health notes/alerts translate their machine slugs into sentences
  (memory_heap_tight_93%_rss111mb reads as heap usage with context)
- lessons rows expand to full detail: rule, why-learned context, tags,
  learned/last-confirmed times, source sessions, raw record; column
  headers carry title hints for confidence and uses
- actions tab gets the same intro card as the other tabs (status flow
  and frontier explained on the populated view, not just when empty)
- timeline defaults to the session with the most observations instead
  of the newest, which was often a sparse just-started session
- consolidation status and top-concepts zero states explain what fills
  them and which flags gate it
- ambient background: the static dot grid becomes a slowly drifting
  ordered-dither field (quarter-res canvas, ~12fps, static frame under
  prefers-reduced-motion, theme-aware)
- dark theme: layered near-black surfaces, hairline borders, softened
  accent — replaces the flat gray borders

* feat(website): reskin on the near-black token system

- neutral token foundation in globals.css: canvas/canvas-soft/card
  surfaces, hairline borders, ink/body/mute text scale, one warm accent
  used sparingly, 8px card radius + pill buttons, focus-visible rings
- Inter display at weight 400 with tight tracking; mono uppercase
  eyebrows and captions; sentence-case body (case normalization only,
  no copy changes)
- hero: two-tone lowercase wordmark, install command as a soft input
  card, ambient drifting dot field capped at 0.12 alpha with a static
  frame under prefers-reduced-motion
- sections rebuilt on the card recipe: quiet background-shift hovers,
  hairline data table for the comparison, segmented tabs with polarity
  flip, per-vendor accent colors stripped from agent cards
- fixed two latent token misuses that resolved to nothing
- build green: 5/5 static pages, TypeScript clean

* fix(viewer): make the graph tab legible without edges

- nodes anchor to per-type cluster centers (captioned on the canvas)
  whenever relations are sparse; a pure force layout with no edges was
  an unlabeled scatter. Edge springs take over as real relations arrive
- labels always render on graphs of 30 or fewer visible nodes instead
  of only past a zoom threshold
- sidebar explains the entities-without-relations state and what
  produces edges; static legend removed (the type filter already
  carries color and shape)

* fix(viewer): graph readability on sparse data

- hover focus-fade only engages when the graph has edges; with none it
  faded every other node and suppressed all labels
- cluster anchor pull reduced and initial scatter widened so type
  groups spread instead of collapsing into blobs
- minimum node radius raised for degree-zero nodes; cluster captions
  offset above their groups

* fix(viewer): graph fits the view; site copy grounded in the repo

viewer graph:
- container height leaves room for the footer instead of running under it
- one-shot auto-fit zooms and pans to the node bounds once the layout
  settles, so first paint is framed instead of adrift
- cluster captions and small-graph labels hide below readable zoom

website copy (full pass, technical register):
- every unverifiable number removed: benchmark percentages, latency
  claims, press strip, testimonials, invented terminal output; the
  comparison table usage dropped rather than shipping stale competitor
  figures
- remaining stats are build-derived (54 MCP tools, 130 REST endpoints,
  12 hooks, 1619 tests) or live from the GitHub API (stars)
- feature copy corrected against src behavior: consolidation, graph
  extraction, and LLM compression activate with a provider key;
  provider list completed; install step numbering fixed
- release-branch capabilities surfaced: agent-scoped save and recall,
  write-time provenance channels, hybrid ranking on the primary recall
  path, indexed lessons, near-duplicate save hints, superseded-version
  recall hygiene, JSONL import deriving crystals and lessons
- em-dashes and slop phrasing removed throughout

* fix: restore featured strip, label collision avoidance, optional no-think

- website: FeaturedIn strip returns to the hero (its claims are the
  project's own credentials); rest of the grounded-copy pass unchanged
- viewer graph: canvas cluster captions removed and labels place
  greedily into free space (selected/hovered always win), so zoomed-out
  views degrade to fewer labels instead of overlapping pills
- graph extraction: AGENTMEMORY_LLM_NOTHINK=1 opt-in asks local
  reasoning models to skip their hidden thinking pass (several times
  faster, slight quality tradeoff); documented in .env.example, default
  behavior unchanged

* feat(website): testimonials return, OpenCode joins the featured connectors

- Testimonials section restored after LiveTerminal (launch-thread
  quotes are the project's own record)
- OpenCode promoted from the marquee to the featured grid: it ships a
  native capture plugin with per-session project attribution; fills the
  empty eighth slot
- full connector roster verified against src/cli/connect (18 dedicated
  adapters all present: featured grid + marquee)

* fix(viewer): official icon as the favicon (was a text placeholder)

* test(viewer): favicon assertion checks the served SVG, not a hex value

* test(viewer): favicon checks assert served SVG shape, not old artwork

* docs: readme grounded in source, changelog entry, env example consistency

* revert(website): restore measured benchmark claims and comparison table

The retrieval recall and token reduction figures are the project's own
measurements and its adoption story; earlier scrubbing was over-strict.
Backing them with a published run of the eval harness stays on the
roadmap.

* fix(website): drop the orphaned pause control on the hero field

The old animated constellation earned a pause button; the subtle dither
field does not, and prefers-reduced-motion already renders it static.

* fix(website): official OpenCode brand mark on the featured card

* chore: bump provider default models to current generations

OpenAI gpt-4o-mini to gpt-5.6-luna, Anthropic claude-sonnet-4-20250514 to claude-sonnet-5, Gemini gemini-2.5-flash to gemini-3.7-flash, MiniMax M2.7 to M3, OpenRouter default to anthropic/claude-sonnet-5. Premium cost warning matches the Sol tier; cheap-model hints lead with deepseek/deepseek-v4-flash-0731. README local picks move to qwen3 / gpt-oss / deepseek-r1 with a NOTHINK callout; cost table refreshed with verified OpenRouter list prices. Embedding defaults unchanged.

* feat: keyless heuristic graph extraction with LLM enrichment optional

Entities and co-occurrence do not need a language model: files and concepts on compressed observations already name the nodes, and appearing in the same observation is an edge. mem::graph-extract now always runs this deterministic pass, so the graph populates for keyless installs; the LLM pass layers typed relations on top only when GRAPH_EXTRACTION_ENABLED is set and a real provider exists. Session end fires extraction unconditionally.

* feat: DeepSeek Harness connector via home-level cordis patch layer

agentmemory connect dsh appends an @deepseek-ai/dsh-mcp-client row to DSH_HOME/cordis.patch.yml (default ~/.dsh), the machine-local patch layer every Harness profile loads, so the MCP tools register as mcp__agentmemory__* before the first turn. Idempotent, --force replaces the row, dry-run supported. Config shape verified against the mcp-client README and publish docs in deepseek-ai/deepseek-harness. Website agents grid and README connector table updated.

* feat: dsh --with-hooks auto-capture via Harness Claude Code bridge

DeepSeek Harness ships a first-party @deepseek-ai/dsh-hooks-claude-code plugin that runs Claude Code shaped command hooks on the harness's own interception points. connect dsh --with-hooks writes the bundled hook manifest (absolute script paths, reusing the codex-hooks merge engine) to DSH_HOME/agentmemory.hooks.json and appends a second patch row pointing the bridge at it. Auto-capture on SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop; PreCompact is outside the bridge subset and skipped. Adapter recategorized native. MCP-only installs never touch the hooks row; --force replaces both.

* feat(pi): automated connect install into pi's auto-discovery dir

connect pi was a stub printing manual copy steps because integrations/ never shipped in the npm package. The extension source now ships (integrations/pi/ in files), and the adapter copies index.ts + security.ts into ~/.pi/agent/extensions/agentmemory/, which pi auto-discovers with no settings.json edit; /reload picks it up live. Idempotent by content compare, stale copies refresh with a backup, dry-run supported. integrations/pi is also a private local pi package (pi-package keyword, pi.extensions manifest limited to index.ts so security.ts is not loaded as its own extension, peer deps on the pi core packages) so pi install ./integrations/pi works from a checkout; never published to npm. Type import moved to @earendil-works/pi-coding-agent (upstream rename).

* fix(codex): warn that hooks need one-time TUI trust approval

Codex executes only hooks with a recorded trusted_hash in config.toml, and the Hooks-need-review approval prompt appears only in the interactive TUI. A codex exec-only workflow therefore never runs freshly installed hooks and gets no signal why. connect codex --with-hooks now warns to launch codex once and choose Trust all, and to re-approve after upgrades since the refreshed absolute paths change the hash. Verified live on codex 0.147.0: before trust, exec dispatched nothing for agentmemory hooks; after TUI approval, SessionStart and UserPromptSubmit fired and the observation landed in the daemon.

* feat(pi): capture parity for the pi extension

Session registration on session_start (ordered after the health check so it fires on the first session of a fresh process), prompt capture with client-side dedup and user-channel provenance, per-tool observations from tool_result with AGENTMEMORY_TOOL_OBSERVE=0 opt-out, turn slices raised to 8000, memory_save scoped to the current project, session end plus one consolidate run on real quit only (no client summarize: session/end already fans out the summary), health accepts status ok, and refreshStatus binds the status setter before awaiting so a session replacement mid-check cannot throw a stale-context error. Live-verified on pi 0.84.2 with a local model: prompt and turn observations landed and the session closed as completed on quit.

* chore: regenerate skill reference docs

npm run skills:gen after the adapter, env, and tool changes: 20 adapters including dsh, refreshed env defaults, tool listing.

* refactor: trim oversized comments to constraint one-liners

Connector and extension comments compressed to the constraints the code cannot show; narrative headers, source citations, and restated behavior removed.

* docs(changelog): fold unreleased into the 0.9.29 release section

This branch ships as 0.9.29 (npm latest is 0.9.28; the previous 0.9.29 section was prepped but never published). One section, dated 2026-08-15, upgrade notes preserved, all 44 bullets intact.

* docs(readme): interactive-first install, dedupe, refresh stale counts

Install leads with npx and the first-run wizard (agent multi-select, provider pick, global-install offer) instead of six manual commands; Windows, EACCES, npx-cache, and iii-pin notes collapse into details blocks. Quick Start drops the duplicated install prose for an everyday-commands list. Nav drops the redundant iii Console link. Gist badge updated to the live 1.6k stars / 230 forks; test count pill and alt text updated to 1,648.

* fix: apply review round — ranking, indexes, lifecycle, connectors

Hybrid scoring normalizes once per query by the best attainable weighted score over streams that produced results, so configured stream weights survive single-stream hits; expansion merge gets the same deterministic tie-break. Graph functions register unconditionally (keyless installs previously fired mem::graph-extract at an unregistered function every session end) and the trigger goes through fireVoid; heuristic edges accumulate observation provenance for repeated pairs instead of dropping it. Supersession candidate search waits for the memory index walk (new isMemoryIndexReady signal) instead of trusting idx.size, and mem::search falls back to keyword search when the hybrid ranker throws. Lesson recall over-fetches under project/confidence filters, refreshes the index entry when reinforcement changes indexed text, and resetLessonIndex clears the cache after import and replay write lessons directly. Observe preserves primitive payloads in the dedup key so distinct prompts never collapse (regression test added). pi extension dedups prompts per session and passes project on both smart-search calls. dsh reads a corrupt hooks manifest as absent via readJsonSafe; pi install returns skipped instead of throwing when the bundled source is missing; both use the shared writeTextAtomic. Docker-mode stop clears each pidfile/state only after its shutdown succeeded and matches compose services at any indentation. Viewer livez fetch gets a 5s timeout. opencode session.deleted prunes through pruneSessionMaps (was leaking sessionProjects). MiniMax MAX_TOKENS doc says the real 4096 default. README MCP catalog: base-tools table completed to the registry's 14, the 8-tool core mode and 7-tool standalone fallback distinguished, two missing resources listed. Tests restore env vars without writing the string undefined, similarTo assertions are unconditional, vitest test home is unique per run, website meta regenerated, deprecated word-break replaced.

* docs: competitors refreshed — TencentDB Agent Memory column, entrants

TencentDB Agent Memory (TencentCloud OSS, May 2026, 22K stars) gets a full column: team memory hub captured through an LLM proxy, four asset types, PersonaMem 76% self-reported, Docker Core+Hub+Proxy stack. Stale star counts refreshed against the live API (mem0 58K to 63K, Letta 24K, Khoj 36K, supermemory 29K). A newer-entrants table covers Zep/Graphiti, Cognee, LangMem, Cloudflare Agent Memory, and Memobase, with matching choose-if sections in benchmark/COMPARISON.md. Section badge subtitle updated.

* fix: lesson index build races, rebuild ready flag, pi file backups

* docs: drop competitor links from README

* Update README.md
2026-08-15 20:37:12 +01:00
Rohit Ghumare 9c82d2aa7d chore(release): v0.9.29 with project-scope parity across capture surfaces (#1141)
* chore(release): v0.9.29 with project-scope parity across surfaces

Version trio + plugin manifests + supportedVersions + ExportData union
bumped to 0.9.29; CHANGELOG entry covering everything since v0.9.28 with
upgrade notes for the four visible behavior changes.

Fixes the endpoint-count drift on main (130 registered routes vs docs
saying 129 after #1132 landed in parallel with #1136).

Project-scope parity: OpenCode plugin, Hermes plugin, Pi extension, and
JSONL replay now resolve project the same way the hooks do (env
override, git toplevel basename, cwd basename) instead of sending raw
filesystem paths, closing #903 and #1135 and pre-empting the same bug
in pi. The filesystem watcher accepts AGENTMEMORY_PROJECT_NAME with the
old AGENTMEMORY_PROJECT kept as a deprecated alias, replay handles
Windows-recorded paths, and OpenCode file enrichment matches the
agent's lowercase tool names (the capitalized set never matched).

Tests: opencode fallback expectations updated to basenames per the
canonicalization, git-toplevel resolution covered with a fixture repo,
new project-scope-parity suite for replay and fs-watcher.

* fix(release): review findings, git-toplevel parity, doc counts

- skills generator dedupes routes on method plus path, so the REST
  reference lists all 130 registered routes instead of hiding the second
  method on ten dual-method paths (header said 119)
- fs-watcher trims AGENTMEMORY_PROJECT_NAME and the deprecated alias,
  treating whitespace as unset, and derives the git toplevel basename
  when watching a subdirectory
- replay resolves the git toplevel basename when the recorded cwd still
  exists locally (memoized per cwd), keeping the basename fallback for
  historical or cross-platform paths; no env override here since a bulk
  import spans many projects
- parity tests for replay git-root resolution, watcher git-root and
  trim behavior
- stat-tests badge updated from 1428+ to 1550+ passing

* fix(cli): refuse second-instance boot over a live daemon

Closes the class behind issue 1140: agentmemory consolidate (or any
unrecognized word) fell through the command table into the full server
boot, registering a duplicate worker on the running engine; on iii
0.11.2 the second instance's shutdown tears down the daemon's HTTP
trigger routing until a full engine restart. Unknown subcommands now
error with the supported list, and main() probes livez on the resolved
port and refuses to boot over a live daemon, so multi-instance setups
on other ports are unaffected. Verified behaviorally against the built
CLI: both paths refuse with exit 1.

Also from review: the watcher stamps each event with its own root's
project via a per-root map (an explicit config.project still overrides
for every root), and replay only accepts a non-empty string cwd from
parsed JSONL so malformed entries cannot reach the filesystem probe.

* test(watcher): two-repository flush events scope to their own project

* chore(release): bump packages/mcp, guard it, refresh CONTRIBUTING

packages/mcp was still 0.9.28 after the release bump because nothing
guarded it; a consistency test now pins it to package.json. CONTRIBUTING
release list corrected to the files a bump actually touches (no tracked
lockfile, the two extra plugin manifests, the export test derives from
VERSION now), and the subsystems table gains src/cli, integrations/pi,
and the generated-manifest note.

* fix(export): refuse over-frame export instead of dropping the worker

Closes the availability bug in issue 1142: GET /agentmemory/export
assembles the full store and returns it through sdk.trigger, so a store
whose serialized export passes the engine's 16 MiB WebSocket frame
(tungstenite max_frame_size, not raisable under the 0.11.2 pin) dies on
the worker->engine hop, drops the worker, and 404s every endpoint for
~1s. The session collections page on maxSessions/offset but ~18 others
do not, so a large store hits this at any parameter combination.

A shared frame-guard measures the serialized size before returning:
mem::export returns a small oversized error instead of the giant
object, and api::mesh-export returns 413 (same dead-end as #890). Either
way the over-frame payload never crosses the boundary, so the daemon
stays up and the failure is one clean request with a hint to narrow the
range. Full pagination of the non-session collections is a follow-up.

Layer 1 of the fix; verified with a synthetic oversized export returning
the error object (tiny) rather than the payload.

* ci: collapse to a single npm install to fix Node 24/26 CI

The two-step install (npm install --package-lock-only then npm ci) failed
only on the Node 24/26 matrix rows: their stricter npm rejects rolldown's
optional platform bindings (@rolldown/binding-android-arm64) that a
--package-lock-only pass does not fully enumerate. Lockfiles are gitignored,
so npm ci re-validation buys no reproducibility here. A single lenient
npm install resolves and installs in one pass.

* fix(mesh): scope exported memories by project like actions

api::mesh-export filtered actions by ?project but returned every project's
memories. On a mesh instance federating one project to a peer, the peer
pulled other projects' memories (cross-project leak), and those extras could
push the payload past the 16 MiB transport frame into a 413 even when the
requested project's own slice fit. Memories carry the same optional project
field as actions, so filter both before the frame-size guard runs.

Adds a regression test asserting a project-scoped export excludes other
projects' memories and that an oversized memory in another project no longer
413s the scoped request.

* chore(release): credit the Antigravity native hooks adapter in 0.9.29 notes

* chore(release): sweep stale 0.9.28 refs for 0.9.29

Deploy Dockerfiles/compose/render pins, AGENTS.md stats header, opencode
plugin manifest, website meta snapshot, test-count claims (1,428 -> 1,596)
in README/AGENTS/stat SVGs, and the missing 0.9.29 CHANGELOG compare link.

* chore(release): sync stat-tests badge to 1596+ and commit bridge exec bit

* refactor: trim frame-guard comments and drop issue refs from code
2026-08-09 13:22:25 +01:00
Bertho Joris d60652a705 feat(cli): native hooks adapter for Antigravity CLI (agy) (#1146)
* feat(cli): native hooks adapter for Antigravity CLI (agy)

Antigravity ships two products with unrelated configuration: the IDE,
already wired by `connect antigravity`, and the `agy` CLI, which reads
its customizations out of ~/.gemini/ and until now was not wired at all.
This adds `connect antigravity-cli` for the latter — MCP via
~/.gemini/config/mcp_config.json, plus optional native auto-capture hooks
behind --with-hooks.

Unlike Droid (#1130), the Codex merge engine could not be reused. The
Antigravity hooks contract differs in three ways:

  * hooks.json is a map of *named* hook bundles at the root, not the
    `{ hooks: { <Event>: [...] } }` envelope, so antigravity-hooks.ts
    implements a merge that owns top-level keys instead of per-event
    entries. User-authored bundles are preserved; a re-install replaces
    only the bundle whose commands point under the bundled plugin dir.
  * only five events exist (PreToolUse, PostToolUse, PreInvocation,
    PostInvocation, Stop) — no SessionStart/SessionEnd/UserPromptSubmit,
    so the session lifecycle is synthesized from the first PreInvocation
    and from Stop. PostInvocation is left unwired to avoid double-capture.
  * the stdin payload is camelCase and nested (`toolCall.args` with
    PascalCase keys, `conversationId`, `workspacePaths`), and stdout must
    be a JSON object — `pre-tool-use.mjs` writes raw prose when context
    injection is on.

plugin/scripts/antigravity-bridge.mjs bridges all three: it normalizes the
payload onto the shape the bundled hooks already accept, maps Cascade tool
names (view_file, replace_file_content, …) onto the read/edit/write/grep
vocabulary the capture heuristics use, pipes to the right script, discards
child stdout and always answers `{}` so Antigravity's own permission
decisions are never overridden.

Event names, tool names and arg keys were verified against the shipped
agy binary rather than docs alone (docs disagree on the global hooks
path); the customization dir is ~/.gemini/config/, matching where agy
already keeps mcp_config.json and plugins/.

Signed-off-by: Bertho Joris <bertho_joris@yahoo.co.id>

* fix(cli): keep $-bearing plugin paths literal when resolving hook commands

resolveBundle() expanded ${CLAUDE_PLUGIN_ROOT} via
String.prototype.replace with a string argument, so a plugin root
containing `$$`, `$&`, "$`" or `$'` was read as a replacement pattern
and rewritten:

  C:/plug$&in  ->  C:/plug${CLAUDE_PLUGIN_ROOT}in/scripts/...
  C:/plug$$in  ->  C:/plug$in/scripts/...

`$1` and `$<name>` are unaffected — the regex has no capture groups.

Switching to a replacer function keeps the path verbatim. The failure
mode this closes is silent: the hook installs with a broken command and
auto-capture simply never fires.

Regression test builds the manifest against a temp plugin root named
`plug$&$$in` and asserts the resolved command contains it literally.

Reported by CodeRabbit on #1146.

Signed-off-by: Bertho Joris <bertho_joris@yahoo.co.id>

* fix(antigravity): emit an explicit allow decision from the PreToolUse hook

Antigravity documents `decision` as a required field of PreToolUse hook
output, and agy treats a response that omits it as a denial: the bare `{}`
the bridge used to write made the agent refuse every matched tool call
(reported against agy 1.0.5 in cmux#5358) instead of passively capturing
it. `responseFor` now answers PreToolUse with `{"decision":"allow"}` and
leaves every other event on `{}`, so no event that carries no permission
decision starts overriding the user's own settings.

The response is written from the `finally` block, so a failed capture or an
unparseable payload still produces the contract rather than empty stdout,
which PreToolUse would read the same way as `{}`.

Tests cover both the pure contract and the built bundled script running
end to end with no server listening. Also extends the ARG_KEY_MAP test to
every mapped key and pins that an explicit canonical key wins over a
PascalCase alias.

* fix(antigravity): match agy's real hooks.json schema, verified against 1.0.15

Three defects found by probing a live agy 1.0.15 with an instrumented hook,
each of which stopped the adapter from capturing anything at all.

Lifecycle events take a flat handler list, not the tool-event wrapper. agy
parses `PreToolUse`/`PostToolUse` as `[{matcher, hooks: [...]}]` but
`PreInvocation`/`PostInvocation`/`Stop` as a bare `[{type, command}]`, since
there is no tool name to match on. Wrapping a lifecycle event makes agy read
the wrapper itself as a handler and reject the *whole file* with
`invalid hook "agentmemory": command hook must specify 'command'` — so the
mis-shaped Stop entry disabled every hook in the bundle, and would have
disabled hooks other tools had written to the same file.

`command` is not run through a shell and quotes are not stripped, so the
quoted path resolved to a module name that literally began with a double
quote: `Cannot find module 'C:\Users\…\.gemini\config\"C:\…\bridge.mjs"'`.
Commands are now bare. That also means a path containing spaces cannot be
expressed at all — quoted and unquoted both fail — so the installer refuses
with an explanation instead of writing hooks that can only fail at tool time.

The merge engine reads both shapes when deciding which bundles agentmemory
owns, so a re-install over the old wrapped layout still replaces it rather
than leaving a second copy behind.

Tests pin both event shapes, the absence of quotes, the space check, and
normalization of a payload captured verbatim from the live run — which also
confirms `conversationId`, PascalCase `toolCall.args`, and that agy sends no
`cwd` key at all.

* refactor(antigravity): cut comment volume to match the sibling adapters

The bundled script carried 24 comment lines where every other script in
plugin/scripts has three. The bundler strips `//` comments but preserves
JSDoc blocks, so the fix is to document the bridge's exported helpers with
line comments: the explanations stay in source and the generated artifact
comes out as clean as its siblings.

The connect adapter and merge engine restated the same facts in a file
header and again in a per-function block. Kept one statement of each,
dropped the repetition, and left the verified agy behaviour in place since
that is the part not derivable from the code.

---------

Signed-off-by: Bertho Joris <bertho_joris@yahoo.co.id>
2026-08-03 18:35:41 +01:00
Rohit Ghumare 6cc9b9f0fe fix: env hydration, indexing, consolidation lifecycle, connector activation, hardening (#1136)
* fix: env hydration, indexing, consolidation, connectors, hardening

- config: hydrate ~/.agentmemory/.env into process.env at boot so all modules see it
- search: shared indexRecords() so export-import and replay populate BM25 and vector (#1072)
- snapshot: wire the periodic timer (#1006), clamp non-positive intervals, add a reentrancy guard
- schema: CJK-aware jaccard dedup plus exact-match fallback for short memories
- embeddings: shared resolveDimensions() so openrouter stops hardcoding 1536 (#1002)
- viewer: buffer request bodies before decoding to fix multibyte corruption (#930)
- providers: retry 429/503 with Retry-After under a total-elapsed budget cap
- consolidation: fire on session stop (#1087), gate keyless installs, debounce the per-turn stop hook, drop the client-side double-fire
- evict: bound stale-session recovery to one consolidation pass
- api/patterns: bound session fan-out (#1100)
- connect: write a memory-usage guideline into each hook-less agent's native rules file (12 agents, doc-verified paths, --no-guidelines opt-out)
- graph: import graphify's graph.json via mem::graph::import-graphify + POST /agentmemory/graph/import-graphify; shared persistGraphDelta with endpoint remap so merged nodes never leave dangling or duplicate edges
- fs-watcher: stat roots before fs.watch so missing roots fail deterministically on Node 24+
- test: regression tests for every fix

* fix: address review findings on import, debounce, and connect paths

- guidelines: refuse to touch files with a lone or reversed marker pair
- export-import/replay: indexing after committed writes is best-effort,
  logged instead of failing the import; flatten the nested runChunked so
  replace-mode deletes stay bounded to one chunk
- graph: persist the snapshot when merge-only batches mutate cached
  topNodes/topEdges entries
- graph-import: async fs, typeof validation on path/cwd; REST handler
  whitelists the payload and 400s non-string values
- fetch: cancel discarded response bodies before retrying
- events: serialize the consolidation cooldown check so concurrent stops
  cannot both pass the read-check-write window
- evict: gate recovered-session consolidation on isConsolidationEnabled
  and mirror the stop path's force flag
- search: rebuild indexes per session chunk to bound peak memory
- test: regression coverage for each (malformed markers, concurrent
  stops, snapshot persistence, AMBIGUOUS/default mappings, env isolation)
2026-08-02 11:16:30 +01:00
Rohit Ghumare 6761a99ba1 fix: guard hooks against null payload (#1074)
#1047: JSON.parse("null") returns null without throwing, so every hook's parse guard passed it through and the first data.xxx access threw a TypeError. Bare main() turned that into an unhandled rejection -> exit 1 -> host reported 'hook failed' on every affected tool call. All 13 hook entrypoints now guard non-object payloads before dereferencing and wrap main() in .catch() to fail closed (silent exit 0).

#1057: mem::context and api::context filtered candidate sessions by project only, leaking cross-agent observations/summaries under AGENTMEMORY_AGENT_SCOPE=isolated. Now applies the same agent-scope filter as mem::search (#817); api::context, api::session::start, and event::session::started forward agentId.

Also: bump 0.9.28 across manifests/deploy/export-import set; refresh stale README/AGENTS stats (files/LOC/functions/KV; AGENTS tests 950+ -> 1,428+) and regenerate the website meta snapshot to 0.9.28; CHANGELOG 0.9.28 section; remove the rate-limited star-history chart from README and all 11 translations.
2026-07-19 11:40:26 +01:00
Ross Story a0da02b6b3 Add GitHub Copilot CLI support (#534)
* feat: add Copilot CLI plugin asset slice

- plugin/.plugin/plugin.json: Copilot manifest with name/version/skills/mcpServers/hooks refs
- plugin/.mcp.copilot.json: MCP server config with type:local, npx, env passthrough, tools:[*]
- plugin/hooks/hooks.copilot.json: Copilot hooks (version:1) with 11 supported events and PreToolUse matcher
- test/copilot-plugin.test.ts: 11 tests covering manifest, MCP config, and hooks validation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add Copilot CLI connect support

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add GitHub Copilot CLI support

Adds Copilot CLI support through a root plugin manifest, Copilot-specific MCP and hook configuration, and a connect adapter for MCP-only setup.

Includes Windows-safe Copilot MCP command generation, COPILOT_HOME handling, Copilot hook payload normalization, generated hook scripts, and targeted tests for plugin shape, hook execution, and connect behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Harden Copilot hook handling

Addresses upstream AI review suggestions by aligning the Copilot preToolUse matcher with the hook allowlist, narrowing hook payload fields at runtime, normalizing subagent fallbacks, and tightening hook config validation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add Copilot to first-run onboarding

Includes GitHub Copilot CLI in the first-run agent picker and adds a regression test so the Copilot setup path remains discoverable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Default onboarding to Copilot inside Copilot CLI

Detect Copilot CLI environment markers during first-run setup so pressing Enter wires the current agent instead of the historical Claude Code default.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Support framed stdio MCP transport

Accept Content-Length framed JSON-RPC messages in addition to the existing newline-delimited transport so Copilot CLI can initialize the standalone MCP server.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Narrow Copilot pre-tool session ids

Ensures pre-tool-use only forwards string session IDs and falls back to unknown for invalid Copilot payload values, with regression coverage for the generated plugin script.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Ross Story <rostory@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Rohit Ghumare <ghumare64@gmail.com>
2026-05-28 09:54:54 +01:00
Rohit Ghumare d626b4ea60 perf(hooks): fire-and-forget telemetry hooks (#573) (#688)
* perf(hooks): fire-and-forget telemetry hooks (closes #573)

Telemetry hooks (notification, post-tool-failure, post-tool-use,
prompt-submit, stop, session-end, subagent-start, subagent-stop,
task-completed) previously `await fetch(..., AbortSignal.timeout(N))`
inside a try/catch. The await kept the hook process alive until the
response arrived — up to N ms per request — which blocks Claude Code's
next-prompt boundary on every assistant turn.

Switch to fire-and-forget:

  fetch(url, { signal: AbortSignal.timeout(N) }).catch(() => {});
  setTimeout(() => process.exit(0), 500).unref();

The unawaited fetch dispatches the request; the unref'd setTimeout
force-exits the process after the request has been flushed to the
local daemon's socket buffer (~500ms is enough). Without the
setTimeout Node keeps the event loop alive waiting for any in-flight
fetch to settle, which means the hook still blocks Claude Code's
next-prompt boundary for up to the AbortSignal duration.

Context-injecting hooks (pre-tool-use, pre-compact, session-start)
still use `await fetch` because Claude Code reads their stdout for
context injection — left untouched.

AGENTS.md updated with the two-pattern guidance.

* chore(hooks): drop verbose comments on fire-and-forget hooks

* fix(hooks): bump stop+session-end exit delay to 1500ms

Multi-request hooks (stop fires 2, session-end up to 4) need more
than 500ms to initiate all fetches when AGENTMEMORY_URL points to a
remote daemon — DNS + TCP + TLS handshakes can eat the budget before
the second/third fetch is even dispatched. Bump to 1500ms on those
two hooks only; single-request hooks keep 500ms.

AGENTS.md updated with the multi-request exception.
2026-05-27 20:51:23 +01:00
Rohit Ghumare 0468407249 fix(hooks): send repo basename as project, not full path (#474) (#687)
* fix(hooks): send repo basename as project, not full path (closes #474)

Hooks were sending `data.cwd` (an absolute filesystem path) as the
`project` field on every observe/session/start call. Native sessions,
replay-import, and manual memory_lesson_save calls all use the repo
basename. The mismatch caused auto-injected context to filter out the
bulk of relevant lessons because the path never matches the stored
project name.

Add shared `resolveProject(cwd)` helper:
  1. AGENTMEMORY_PROJECT_NAME env (per-repo escape hatch)
  2. basename of `git rev-parse --show-toplevel` (handles subdirs)
  3. basename of cwd (final fallback when not in a git repo)

Applied to 9 hooks: notification, post-tool-use, post-tool-failure,
prompt-submit, session-start, subagent-start, subagent-stop,
task-completed, pre-compact.

Build: split hook entries into per-entry tsdown configs so each hook
bundles into a fully self-contained .mjs. Previous shared config
hoisted helpers into hashed chunks that changed on every rebuild.

* chore(hooks): drop issue-number ref from resolveProject comment

* chore: trim verbose comments on _project.ts + tsdown.config
2026-05-27 20:32:54 +01:00
Rohit Ghumare 3551241416 fix(hooks): stop also closes session for Codex (closes #493) (#579)
Codex does not fire a separate SessionEnd event, so its Stop hook is
the only signal we get when a Codex session terminates. The current
stop hook only POSTs /agentmemory/summarize, leaving the session row
stuck on status:"active" in the viewer for Codex users (#493).

Stop now ALSO POSTs /agentmemory/session/end, best-effort with a
short 5s timeout. For Claude Code this is a harmless idempotent second
call (session-end.mjs runs on the dedicated SessionEnd hook and sets
the same endedAt + status fields). For Codex it's the only path that
closes the lifecycle.

Tests (1081) + build pass. plugin/scripts/stop.mjs regenerated by tsdown.
2026-05-22 19:23:08 +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 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.
2026-05-18 19:20:48 +01:00
Rohit Ghumare 1ff5849d9c fix: cap session-start/subagent-start hook latency (#221) (#271)
Two hook scripts blocked Claude Code's startup waiting on REST responses
they didn't actually need:

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

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

Fix:

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

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

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

Closes #221.
2026-05-10 19:08:34 +01:00
Rohit Ghumare 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 51bcb09104 address CodeRabbit review on #187 + fix CI
Findings verified against current code on this branch; all four valid.

1. config.ts loadFallbackConfig (L281) — user could set
   FALLBACK_PROVIDERS=agent-sdk and bypass the AGENTMEMORY_ALLOW_AGENT_SDK
   gate added to detectProvider. Filter it out at the fallback layer too,
   with the same warning pointing at the opt-in flag.

2. summarize.ts (L87-92) — the empty_provider_response branch returned
   without recording failure metrics or a diagnostic log, unlike the
   parse/validation paths. Record the same metricsStore failure event and
   log provider name, prompt size, system size, and observation count so
   empty responses are visible in telemetry.

3. providers/agent-sdk.ts (L14-45) — setting
   process.env.AGENTMEMORY_SDK_CHILD = '1' without restoring it caused
   every subsequent .query() in the same parent process to hit the
   short-circuit guard and return '' (classified as a SDK child it is
   not). Capture prev, set in try, restore in finally (delete if prev
   was undefined). Child processes spawned during the for-await loop
   still inherit the marker because env is inherited at spawn time; we
   only restore after the loop completes.

4. plugin/scripts/sdk-guard-DI1NUOS9.mjs — tsdown extracted the shared
   guard helper into a hashed chunk. Hash rotates on every rebuild and
   churns the diff. Stopped using the shared module from hooks entirely
   and inlined the 6-line guard function into each hook .ts file
   instead. sdk-guard.ts stays in the tree because the unit tests cover
   it directly. Deleted the tracked hashed .mjs and confirmed no new
   chunk is emitted.

Also applied the CI two-step install (npm install --package-lock-only
then npm ci) on this branch, matching #184. Without it, npm ci fails
because lockfiles are gitignored.

Tests: 74 files / 819 tests pass.
2026-04-22 11:44:05 +01:00
Rohit Ghumare 5e63846b29 fix(hooks): break Stop-hook infinite recursion via agent-sdk fallback
Reported: a user with no provider API key and AGENTMEMORY_AUTO_COMPRESS=false
(which they believed protected them) hit unbounded recursion — Stop hook
POSTs /agentmemory/summarize, handler calls provider.summarize(), agent-sdk
provider spawns @anthropic-ai/claude-agent-sdk query(), which creates a full
CC-style child session that reads ~/.claude/settings.json, registers the
same plugin hooks, and fires its own Stop -> another child -> loop. ~579
ghost 'entrypoint: sdk-ts' sessions accumulated in a few minutes, draining
Claude Pro tokens.

#149 only added a stderr warning. AGENTMEMORY_AUTO_COMPRESS gated /compress
but never /summarize, so users who followed the warning's implied guidance
still got hit. Fix the loop at every layer:

1. config.ts detectProvider
   - Treat empty-string provider keys (ANTHROPIC_API_KEY=) as unset; they
     previously passed the truthiness check identically to a real key.
   - Stop defaulting to agent-sdk. When no key is set, return a 'noop'
     provider config and warn. Agent-sdk fallback now requires an explicit
     AGENTMEMORY_ALLOW_AGENT_SDK=true opt-in with a loud second warning.

2. providers/noop.ts (new) + providers/index.ts
   - NoopProvider implements MemoryProvider and returns empty strings for
     compress and summarize so callers can detect .name === 'noop' and
     short-circuit without spawning anything.
   - Add ProviderType 'noop' and wire it through createBaseProvider.

3. providers/agent-sdk.ts
   - Before spawning query(), check process.env.AGENTMEMORY_SDK_CHILD === '1'
     and return '' instead of recursing. Set the env var to '1' before the
     spawn so any child process (including the Agent SDK session's hooks)
     inherits it.

4. hooks/sdk-guard.ts (new) + all 12 hook scripts
   - Shared isSdkChildContext(payload) checks both AGENTMEMORY_SDK_CHILD=1
     and payload.entrypoint === 'sdk-ts' (CC writes this into the stdin
     jsonl for SDK-spawned sessions). Every hook script now bails early
     when that returns true, so even if one guard layer fails the others
     break the loop.

5. functions/summarize.ts
   - Short-circuit with {success:false, error:'no_provider'} when
     provider.name === 'noop' — never reach .summarize().
   - Treat an empty provider response as empty_provider_response instead
     of trying to parse it.

Tests: 74 files / 819 tests pass (+7 new in stop-hook-recursion-guard.test.ts).
Defense in depth means any ONE of the five layers breaks the loop.
2026-04-22 10:57:57 +01:00
Rohit Ghumare 2edc02feb5 fix: resolve conflicts with main, address qodo-ai security findings
Resolves conflicts in:
- src/state/schema.ts (merged imageRefs + accessLog)
- src/functions/auto-forget.ts (kept audit, reordered ref decrement)
- src/functions/compress.ts (collapsed iii-sdk imports, s/ctx.logger/logger/)
- src/functions/evict.ts (kept audit + deleteAccessLog, deferred ref
  decrement until after delete succeeds)
- src/functions/retention.ts (kept source-aware delete routing, fetch mem
  from resolved scope, defer ref decrement)
- src/triggers/api.ts (kept main's registerFunction/trigger shape)
- src/types.ts (kept superset of AuditEntry.operation union)

qodo-ai action-required findings:
- mem::forget now decrements image refs for every deleted memory and
  observation (including the session-wipe branch), which stops sensitive
  screenshots leaking to ~/.agentmemory/images/ after user-initiated
  forget.
- Eviction / auto-forget now decrement refs only after the KV delete
  actually succeeds. The old order (decrement-then-delete) could
  desync ref counts when the delete threw.

Follow-on fixes:
- buildSyntheticCompression now carries modality and imageData through
  from raw to compressed, so the default zero-LLM path doesn't drop
  image metadata the viewer and governance paths rely on.
- Test harness updated for the current registerFunction (name, cb)
  signature and iii-sdk partial-mock pattern — 11/11 multimodal
  tests pass, full suite 788/788.

Closes #64.
2026-04-21 10:37:24 +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
tanmaishi e692cf0095 feat: implement multimodal image memory 2026-04-11 11:38:30 +05:30
Rohit Ghumare e8f410b537 feat: v0.7.0 — lessons, tool visibility, auto-consolidation, obsidian export, npx bootstrap (#82)
* feat: v0.7.0 — lessons, tool visibility, auto-consolidation, obsidian export, npx bootstrap

Five DX improvements based on competitive research against Mem0, Engram, CodeMem:

1. `npx agentmemory` zero-config startup
   - New CLI bootstrap (src/cli.ts) auto-detects and starts iii-engine
   - Tries `iii` binary first, falls back to `docker compose up -d`
   - Bundles iii-config.yaml and docker-compose.yml in dist/

2. Simplified MCP tool surface (7 core tools by default)
   - AGENTMEMORY_TOOLS=all unlocks all 49 tools
   - Default: save, recall, consolidate, forget, sessions, diagnose, lesson_save
   - Call handler remains unfiltered — any tool callable by name

3. Auto-consolidation on session end
   - CONSOLIDATION_ENABLED defaults to true (was false)
   - Session-end hook: session/end → crystallize → consolidate → bridge sync
   - Consolidation pipeline always registered (timer gated by config)

4. First-class lesson memory type with confidence decay
   - Lesson interface: confidence (0-1), reinforcements, decayRate, source
   - 5 functions: lesson-save, lesson-recall, lesson-list, lesson-strengthen, lesson-decay-sweep
   - Dedup via SHA-256 fingerprint — duplicate saves strengthen existing
   - Crystal lessons auto-flow into lesson system at confidence 0.6
   - Daily decay sweep with parallel KV writes

5. Obsidian-compatible Markdown export
   - Export to ~/.agentmemory/vault/ with YAML frontmatter + wikilinks
   - MOC.md (Map of Content) index file
   - Parallel KV reads, auto-export via OBSIDIAN_AUTO_EXPORT=true

Stats: 49 MCP tools, 99 REST endpoints, 573 tests passing

* docs: add AGENTS.md, fix tool/endpoint counts across README, plugin, cli

- Create AGENTS.md with strict consistency rules for MCP tools, REST
  endpoints, versions, KV scopes, and audit operations
- Fix MCP tool count: 38 → 41 across README.md (4 occurrences)
- Fix REST endpoint count: 93/95 → 99 across README.md and index.ts
- Fix plugin.json: version 0.6.1 → 0.7.0, tool count 5 → 41
- Fix cli.ts help text: 48+ → 41 MCP tools
- Fix README api.ts path reference → triggers/api.ts

* fix: address code review findings — consolidation guard, decay bug, async fs, export-import lessons

Inline fixes:
- Revert CONSOLIDATION_ENABLED to opt-in (=== "true"), matching original behavior
- Add early-exit guard in consolidation-pipeline handler when disabled
- Gate session-end crystallize+consolidation calls on CONSOLIDATION_ENABLED
- Fix lesson decay over-decay bug: add lastDecayedAt to Lesson, compute
  incremental delta instead of reapplying full age every sweep run
- Add LESSON_DECAY_ENABLED flag (default true) to gate the sweep timer

Outside diff fixes:
- Add lessons to export-import (export + import + replace cleanup)
- Log warning instead of swallowing obsidian auto-export errors

Nitpick fixes:
- Switch obsidian-export to async fs/promises (mkdir, writeFile)
- Add per-item try/catch in obsidian-export, return errors array
- Replace governance_delete with smart_search in ESSENTIAL_TOOLS (non-destructive default)
- Fix CLI whichBinary for Windows (uses "where" on win32)
- Use dynamic port in CLI error message instead of hardcoded 3111
- Return 201 for newly created lessons, 200 for strengthened
- Wrap lesson audit calls in try/catch so audit failure doesn't surface
- Fix build script to not swallow tsdown failure
- Remove exact tool count from test, use >=41 + uniqueness + required names
- Add test/consistency.test.ts: validates version, tool count, README consistency
- Add isConsolidationEnabled mock to consolidation-pipeline test

579 tests passing (573 original + 6 new consistency checks)

* fix: guard remaining audit calls, correct endpoint count, add CI + npm publish

Review fixes:
- Wrap lesson_recall and lesson_strengthen audit calls in try/catch
- Fix REST endpoint count: 99 → 100 (verified via grep) across index.ts,
  README.md, and AGENTS.md
- Use regex in consistency test for README assertions (flexible phrasing)
- Add consolidation gate tests: disabled returns early, force=true bypasses

CI/CD:
- Add .github/workflows/ci.yml — test on Node 18/20/22
- Add .github/workflows/publish.yml — auto-publish to npm on GitHub release
  (uses NPM_TOKEN secret + provenance)

Nitpick:
- Single-char term filter (t.length > 1) kept intentionally — prevents noise
  from single-letter matches; documented in AGENTS.md if needed

581 tests passing

* fix(ci): add --legacy-peer-deps for zod v3/v4 peer conflict

@anthropic-ai/claude-agent-sdk@0.2.56 requires zod@^4.0.0 as a peer
dependency but the project uses zod@^3.23.0. The lock file resolves
this locally but npm ci is strict about peer deps in CI.

* fix(ci): drop Node 18 from matrix — tsdown requires Node 20+

tsdown/rolldown uses node:util.styleText which is only available in
Node 20.12+. Updated engines field to >=20.0.0 to match.

* fix(ci): add inlineOnly: false to tsdown config, target node20

tsdown errors on CI with "Consider adding inlineOnly option" when
dependencies are bundled. Setting inlineOnly: false suppresses this.
Also updated target from node18 to node20 to match engines field.
2026-04-04 17:57:01 +01:00
Rohit Ghumare 857f71e3c6 feat: v0.6.0 advanced retrieval with real-world benchmarks (#76)
* feat: add 9 orchestration modules for v0.5.0

Add actions, frontier, leases, routines, signals, checkpoints,
flow-compress, mesh, and branch-aware modules with full MCP tools,
REST endpoints, and 170 new tests. Includes SSRF protection,
race-condition-safe keyed mutex locking, SHA-256 fingerprinting,
and fixes for 29 CodeRabbit review findings.

- 9 new source files (src/functions/*)
- 8 new test files (170 tests, total 386)
- 10 new MCP tools, 23 new REST endpoints
- 8 new KV scopes, new types for orchestration
- Version bump to 0.5.0, README and viewer updated

* feat: add sentinels, sketches, crystallize, diagnostics, facets modules

5 new modules inspired by beads patterns but with original naming and
iii-engine real-time streaming (no polling):

- sentinels: event-driven condition watchers (webhook, timer, threshold,
  pattern, approval) that auto-unblock gated actions via SSE
- sketches: ephemeral action graphs with auto-expiry, promote or discard
- crystallize: LLM-powered compaction of completed action chains into
  compact crystal digests with key outcomes and lessons
- diagnostics: self-diagnosis across 8 categories (actions, leases,
  sentinels, sketches, signals, sessions, memories, mesh) with auto-heal
- facets: multi-dimensional tagging (dimension:value) with AND/OR queries

- 5 new source files, 5 new test files (132 tests, total 518)
- 9 new MCP tools (total 37), 21 new REST endpoints (total 93)
- 4 new KV scopes (total 33), new types for all modules
- README stats and function table updated

* fix: address code review findings across v0.5.0 modules

- actions: validate edges before persisting, set blocked status for requires deps
- leases: use mem:action lock key, reject blocked actions, check expiry on release
- checkpoints: validate linkedActionIds exist, check requires edges in unblock
- mesh: add SSRF validation on peer registration
- routines: remove invalid "failed" action status check
- export-import: add v0.5.0 scope export/import (actions, sentinels, sketches, etc)
- mcp/server: validate CSV inputs are strings before splitting
- schema: replace runtime require with static import
- README: fix stale tool/endpoint counts (28→37, 72→93)

* fix: second round code review — mesh locks, routines DAG, MCP input validation, README counts

- mesh.ts: add withKeyedLock on action writes in receive path, add IPv6 private ranges to SSRF check
- routines.ts: validate DAG (duplicate orders, unknown deps), set dep actions to blocked, refresh stepStatus in routine-status
- checkpoints.ts: runtime type enum validation, set linked pending actions to blocked
- leases.ts: validate ttlMs is finite positive number
- export-import.ts: add skip strategy checks for all v0.5.0 import blocks
- mcp/server.ts: typeof guards on tags, config JSON.parse, actionIds, linkedActionIds, categories
- README.md: Tools 18→37, Functions 33→50, stats line updated
- test: update checkpoint test for new blocked-on-create behavior

* fix: third round review — lease renew/release safety, mesh locking, export replace cleanup, MCP input guards

- leases.ts: renew extends from max(now, existing expiry) instead of now; release verifies action ownership before mutation
- mesh.ts: withKeyedLock on memory writes in mesh-receive; applySyncData validates id/updatedAt and locks both memory and action writes
- routines.ts: stepStatus maps "blocked" to "pending" explicitly; progress includes blocked/cancelled counts; routine-freeze wrapped in withKeyedLock
- export-import.ts: remove unused RoutineRun import; replace strategy clears all orchestration namespaces; skip strategy for graphNodes/graphEdges/semantic/procedural
- mcp/server.ts: sentinel_trigger JSON.parse with typeof+try/catch; facet_query typeof guards on matchAll/matchAny; remove redundant requires cast; .filter(Boolean) on concepts/files/tags/requires CSV splits
- README.md: clarify API table is a representative subset

* fix: fourth round review — redirect SSRF, blocked-on-create, missing action handling, boolean normalization

- mesh.ts: add redirect:"error" to both outbound fetch calls to prevent SSRF via redirect
- routines.ts: create actions with status "blocked" directly when hasDeps (eliminates two-pass race); handle missing actions in routine-status as cancelled; progress.total uses run.actionIds.length
- mcp/server.ts: sentinel config accepts object values directly; normalize unreadOnly/dryRun for both JSON booleans and string values
- README.md: consistent bundle size (365KB) across both occurrences

* feat: v0.6.0 advanced retrieval — triple-stream search, stemming, real benchmarks

Search improvements:
- Porter stemmer for word normalization (authentication ↔ authenticating)
- 40+ coding-domain synonym groups (db ↔ database, k8s ↔ kubernetes)
- Binary-search prefix matching replaces O(n) full scan
- Session diversification (max 3 results per session)
- Co-occurrence graph edges between all concept pairs

New retrieval modules:
- Sliding window inference pipeline (context enrichment at ingestion)
- Adaptive query expansion (LLM-generated reformulations)
- Triple-stream search (BM25 + Vector + Graph with RRF fusion)
- Append-only temporal knowledge graph (versioned edges, point-in-time queries)
- Graph-augmented retrieval (entity search + neighborhood expansion)
- Ebbinghaus retention scoring (decay + tiered hot/warm/cold/evictable)

Real-world benchmarks (240 observations, 20 labeled queries):
- Quality eval: 64.1% recall@10 with Xenova embeddings (vs 55.8% grep)
- Scale eval: 92-100% token savings vs built-in memory at 240-50K observations
- Cross-session: 12/12 queries found vs 10/12 for 200-line MEMORY.md cap
- Token measurement uses actual search results (fixed fake constant bug)
- Removed old microbenchmarks (bench.ts, run-bench.ts, COMPARISON.md)
2026-03-18 08:47:35 +00:00
Rohit Ghumare 709905696b fix: package.json paths, standalone shebang, integration tests
- Fix main/bin/start to use .mjs extension (tsdown outputs ESM)
- Remove duplicate shebang banner from standalone build config
- Update integration test: health status "healthy" (v0.4.0), viewer HTML check
- Add POST body to pre-compact hook for claude-bridge sync
2026-03-01 23:53:46 +05:30
Rohit Ghumare 134cf96124 feat: agentmemory v0.4.0 — the memory layer for all AI coding agents (#9)
* feat: agentmemory v0.4.0 — the memory layer for all AI coding agents

7 new features: Claude Code memory bridge, standalone cross-agent MCP server,
knowledge graph with entity extraction, 4-tier memory consolidation pipeline,
team/shared memory, memory governance with audit trail, git-versioned snapshots.

33 functions, 18 MCP tools, 6 MCP resources, 3 MCP prompts, 49 REST endpoints,
21 KV scopes, 216 tests. All features opt-in via env vars.

* fix: address 28 CodeRabbit findings across v0.4.0 codebase

Critical: Fix bin path mismatch (dist/mcp-standalone.mjs -> dist/standalone.mjs)
Bugs: Fix procedural decay, parseFloat||0.5 for zero values, snapshot restore
missing graphNodes/observations, git catch too broad, N+1 graph query, BFS
duplicate edges, array mutation in audit sort, date validation
Improvements: Lazy readline in transport, JSON-RPC validation, persist error
handling, ID collision prevention, defensive null coalescing for concepts/files,
Windows backslash support in config, direction required for bridge sync tool,
try-catch for audit/governance MCP, team profile fallback teamId, POST body for
bridge sync hook, observations validation for graph-extract endpoint, await in test
2026-03-01 18:03:36 +00:00
Rohit Ghumare 8b21ae2bf6 feat: agentmemory v0.4.0 — MCP resources, prompts, enrichment, confidence (#7)
* feat: agentmemory v0.4.0 — MCP resources, prompts, enrichment, confidence

Add 4 MCP resources (status, project profile, recent sessions, latest memories),
3 MCP prompts (recall_context, session_handoff, detect_patterns), confidence-scored
memory relations, and a unified enrich endpoint that aggregates file context,
relevant observations, and past bug memories for PreToolUse hook injection.

- Add confidence field to MemoryRelation with auto-scoring from co-occurrence,
  recency, and relation type
- Add mem::enrich function aggregating 3 sources with resilient .catch() fallbacks
- Add POST /agentmemory/enrich REST endpoint
- Switch PreToolUse hook from file-context to enrich endpoint with search term extraction
- Add minConfidence filter and confidence-desc sorting to mem::get-related
- 27 new tests (171 total), all passing

* fix: address review findings — input validation, XML escaping, confidence scoring

- enrich.ts: Sort bug memories by recency before slicing top 3; add escapeXml()
  for narratives and bug content injected into XML tags
- relations.ts: Use filter+max instead of find() for matchingRelation to pick
  highest confidence when multiple edges exist; normalize minConfidence with
  Number.isFinite + clamp to [0,1]
- mcp/server.ts: Guard decodeURIComponent with try/catch returning 400 on
  malformed percent-encoding; use typeof checks for prompt args instead of
  truthy checks; normalize minConfidence/maxHops at MCP layer
- api.ts: Validate files[] and terms[] element types are strings before
  calling mem::enrich
- Tests: Add malformed URI and non-string prompt arg test cases (173 total)
2026-02-28 10:28:32 +00:00
Rohit Ghumare 0e9d5ac76f fix: address 12 code review findings across validation, safety, and correctness
- observe.ts: extract fields from sanitizedRaw instead of payload.data
- mcp/server.ts: add input validation per tool and try-catch error boundary
- api.ts: validate request bodies for /remember, /forget, /migrate
- post-tool-failure.ts: truncate tool_input and error to 4000 chars
- pre-tool-use.ts: skip "pattern" key for Grep (regex, not file path)
- recall/SKILL.md: escape $ARGUMENTS before injecting into curl JSON
- consolidate.ts: use ?? instead of || for minObservations, add 30s timeout
- file-index.ts: filter sessions by project, cache observations per session
- patterns.ts: remove 80-char truncation on error keys
- remember.ts: validate content non-empty, skip empty observationIds
- index.ts: add missing /generate-rules log line
- subagent-stop.ts: normalize timeout to 2000ms
2026-02-27 11:33:48 +05:30
Rohit Ghumare 45795b6033 feat: v0.2.0 -- full memory upgrade with 12 hooks, MCP tools, skills, and intelligence
New hooks (7):
- PreToolUse: inject file history before edits (Edit/Write/Read/Glob/Grep)
- PostToolUseFailure: capture error patterns for learning
- PreCompact: preserve memory context through compaction
- SubagentStart/SubagentStop: track multi-agent workflows
- Notification: capture permission prompts (tool preferences)
- TaskCompleted: track team task completions

New functions (4):
- mem::file-context: file-centric memory index for PreToolUse
- mem::consolidate: merge observations into long-term memories via LLM
- mem::patterns + mem::generate-rules: detect co-change patterns and recurring errors
- mem::remember + mem::forget: explicit save/delete for long-term memory

MCP server (2 endpoints):
- GET /agentmemory/mcp/tools: list 5 MCP tools (recall, save, file_history, patterns, sessions)
- POST /agentmemory/mcp/call: dispatch tool calls to iii functions

Skills (4):
- /recall [query]: search past observations
- /remember [insight]: save to long-term memory
- /session-history: show past session timeline
- /forget [target]: delete specific memory data

New API endpoints (7):
- POST /agentmemory/file-context
- POST /agentmemory/remember
- POST /agentmemory/forget
- POST /agentmemory/consolidate
- POST /agentmemory/patterns
- POST /agentmemory/generate-rules
- GET/POST /agentmemory/mcp/*

Updated: types (7 new HookTypes, 3 new ObservationTypes), version 0.2.0
2026-02-27 11:33:48 +05:30
Rohit Ghumare 4c334b0a0e fix: system audit -- 10 bugs found and resolved
1. events.ts: Event triggers were calling api:: functions which
   require ApiRequest shape and auth headers. Rewrote to call
   core functions (kv.set, sdk.trigger) directly, bypassing auth.

2. All 5 hooks: Missing AGENTMEMORY_SECRET auth header. If secret
   was set, every hook would get 401 from the API. Now all hooks
   read AGENTMEMORY_SECRET and send Bearer token.

3. observe.ts: stripPrivateData on JSON string could break JSON
   structure when replacement text differs in length. Added
   try/catch fallback to string coercion.

4. post-tool-use.ts: truncate() for objects did
   JSON.parse(str.slice(0, max-1) + '}') which produces invalid
   JSON in nearly all cases. Changed to return truncated string.

5. compress.ts: LLM-returned importance was not clamped to 1-10
   range. Added Math.max(1, Math.min(10, ...)) with NaN fallback.

6. compress.ts: LLM-returned observation type was not validated
   against ObservationType union. Invalid types now fall back to
   "other".

7. context.ts: Token estimate for observation blocks only counted
   inner content, not the "## Session..." header. Fixed to estimate
   the full block text.

8. viewer: WebSocket port now configurable via ?wsPort= query param
   for non-default III_STREAMS_PORT configurations.

9. plugin/scripts: Rebuilt with auth header support matching the
   updated hook source files.
2026-02-27 11:33:48 +05:30
Rohit Ghumare 6df02d3e20 add plugin marketplace install support
- Add .claude-plugin/marketplace.json for /plugin marketplace add
- Build hook scripts into plugin/scripts/ (self-contained)
- Fix hooks.json to use ${CLAUDE_PLUGIN_ROOT} paths
- Update README with plugin install as primary quick start
2026-02-27 11:33:48 +05:30