482 Commits

Author SHA1 Message Date
Rohit Ghumare e04ba88819 fix(cli): make fresh installs portable and persistent (#892)
* fix(cli): anchor engine cwd and rewrite bundled config with absolute paths

The bundled iii-config.yaml uses cwd-relative paths and the engine was
spawned without a cwd, so on global and npx installs ./data/state_store.db
and ./data/stream_store landed in whatever directory the user ran the CLI
from, and the iii-exec supervision block (src/**/*.ts watch, node
dist/index.mjs exec) never resolved, meaning the engine never supervised a
worker and nothing respawned it after the in-process worker died. That
surfaced as all data gone reports against a live REST port.

startIiiBin now prepares the launch: when the resolved config is the
bundled one it writes ~/.agentmemory/iii-config.runtime.yaml (regenerated
each boot) with absolute data paths under ~/.agentmemory/data and an
absolute node exec line for the installed worker entry, copies any legacy
./data stores from the invocation directory on first run, and spawns the
engine with cwd anchored at ~/.agentmemory. Repo checkouts keep the cwd
config and repo-root cwd, so dev behavior is unchanged. User overrides
via env or ~/.agentmemory/iii-config.yaml are passed through verbatim.

agentmemory remove gains a plan item for the generated runtime config.

Covered by test/engine-launch.test.ts including a drift guard that
rewrites the repo's real iii-config.yaml and asserts no relative paths
remain.

* fix: make fresh installs portable and persistent

* docs: refresh generated config reference
2026-08-23 15:54:08 +01:00
Rohit Ghumare 2d38dafede chore: 0.9.29 release prep (#1216) v0.9.29 2026-08-16 14:54:32 +01:00
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 a2a2af936e feat: MCP version negotiation, translation sync, skills, mobile fix (#1211)
* fix: negotiate MCP protocolVersion instead of hardcoding 2024-11-05

* docs: sync 11 README translations with v0.9.29 English rework

* feat: memory-discipline and lesson skills for proactive memory use

* fix: bump remaining skill-count claims to 17

* fix: mobile install-box overflow and stale compare counts

* chore: regenerate website meta snapshot

* fix: honest compare benchmarks and scrollable mobile table

* docs: refine lesson skill wording and context block copy

* fix: compare table reads generated meta, focusable scroll region

* fix: render each lesson as a single context line
2026-08-15 23:05:55 +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 2973e4ec4c chore: remove drafts (#1170)
The six drafts covered vulnerabilities fixed in 0.8.2 and were staging
material for filing through the repository Security tab. GitHub reads
advisories only from the Security tab, so the folder carries no function
in the repo.
2026-08-09 13:30:54 +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
Som Samantray 5023cf3ccb fix(memory): honest memory_forget reporting + lesson delete path (#1132)
* fix(memory): guard mem::forget delete/count on record existence

Calling mem::forget with a lesson id (lsn_*) deleted a nonexistent key
from the memories keyspace, counted it, and reported success. Guard the
delete, index cleanup, and counter on the kv.get result, matching the
mem::governance-delete pattern, so nonexistent ids return
{ success: true, deleted: 0 } with no audit row. Closes #1120.

* feat(lessons): add mem::lesson-delete soft-delete function

Register mem::lesson-delete to set deleted: true on a lesson, mirroring
the lesson-strengthen existence guard and audit pattern. Read paths
already filter !l.deleted, and re-saving deleted content creates a fresh
lesson. Adds lesson_delete to the audit operation union.

* feat(mcp): expose memory_lesson_delete tool and REST endpoint

Wire mem::lesson-delete through the MCP tool registry and dispatch
case (memory_lesson_delete) and a POST /agentmemory/lessons/delete REST
route with 400 for a missing lessonId and 404 for a nonexistent lesson.

* chore(consistency): bump tool/endpoint counts to 54/129

Adds memory_lesson_delete to the registry, so update every count surface:
tool-count test, README badge and prose, AGENTS.md stats,
INSTALL_FOR_AGENTS.md, plugin manifests and docs, and the two code
comments this change makes stale. REST endpoint count goes 128 to 129
for the new /agentmemory/lessons/delete route.

* refactor(lessons): simplify 404 mapping and restore decay-delta test

Cast the lesson-delete trigger result once instead of twice inline, and
restore the lastDecayedAt incremental-delta decay test that was dropped
when the lesson-delete describe block was added.

* fix(review): align 404 error shape and regenerate skill references

Review fixes: the lesson-delete REST route now returns the repo-standard
{ error: 'lesson not found' } body on 404 instead of the function-shaped
{ success: false } payload, matching api::memory-by-id. Regenerated the
autogen MCP and REST skill references so memory_lesson_delete and the
lessons/delete route appear in the tables with accurate counts.

* fix(lessons): normalize lessonId at entry points and harden no-op test

Address CodeRabbit review: trim lessonId once at both the MCP dispatch
and REST route before triggering mem::lesson-delete (whitespace-padded
ids previously 404'd or looked up raw), and extend the nonexistent-
memoryId regression test to assert the no-op path performs no kv.delete
and no search-index cleanup.

---------

Co-authored-by: Rohit Ghumare <48523873+rohitg00@users.noreply.github.com>
2026-08-02 19:07:09 +01:00
Abdullah Alaqeel 9be3d2743a feat(opencode): allow AGENTMEMORY_PROJECT_NAME to override project name (#1125)
* feat(opencode): allow AGENTMEMORY_PROJECT_NAME to override project name

The OpenCode capture plugin resolved the project as ctx.worktree ||
ctx.project?.id || process.cwd() (a full path) with no way to override
it. It now consults AGENTMEMORY_PROJECT_NAME first — the same env var the
Claude hooks use in resolveProject() — falling back to the existing
resolution when unset.

This lets users pin a stable project name and aligns the two plugin
ecosystems so OpenCode and Claude sessions can share memory when the env
var is set to the same value.

Signed-off-by: Abdullah Alaqeel <abdullah.t.aqeel@gmail.com>

* test(opencode): replace source-text assertions with runtime project-name tests

Execute the plugin end-to-end, fire a session.created event, and assert
the /session/start payload's project field. Covers whitespace trimming
of AGENTMEMORY_PROJECT_NAME and the documented fallback chain
(ctx.worktree -> ctx.project.id -> process.cwd).

Signed-off-by: Abdullah Alaqeel <abdullah.t.aqeel@gmail.com>

---------

Signed-off-by: Abdullah Alaqeel <abdullah.t.aqeel@gmail.com>
2026-08-02 19:00:05 +01:00
Bertho Joris d219763ccb feat(cli): native hooks adapter for Droid (Factory.ai) (#1130)
* feat(cli): add native hooks adapter for Droid (Factory.ai)

Droid ships a first-party hooks system (~/.factory/hooks.json) using
the same manifest shape as Codex/Claude Code. `agentmemory connect
droid --with-hooks` now merges the bundled hooks.droid.json into it,
covering SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, and
SessionEnd via the existing buildMergedHooks merge/re-install logic.

Also fixes a Windows test-isolation bug in connect-new-agents.test.ts
where os.homedir() reads USERPROFILE (not HOME), which was silently
letting adapter tests write into the real user home directory.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: changelog entry + regenerate agentmemory-agents skill reference for Droid hooks

CHANGELOG [Unreleased] entry for the new --with-hooks path, and
npm run skills:gen to sync the auto-generated agents table (pulled
from src/cli/connect/index.ts protocolNote) so it reflects Droid's
updated hooks capability. tools/rest/config/hooks reference docs
showed as stale too but diffed empty (pre-existing line-ending-only
drift on this checkout) so left untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(cli): surface skipped hook results in dry-run branch, tighten Droid hook tests

- json-mcp-adapter: capture and warn on skipped hook results in the
  dry-run branch (consistent with already-wired and post-install branches)
- droid-connect-hooks test: assert exact five-event set instead of
  presence-only plus selective absence checks
- droid-connect-hooks test: use deep equality (toEqual) for idempotency
  check instead of per-event length comparison

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* fix(cli): use Droid Create tool in PreToolUse matcher

Droid has no Write tool — file creation is Create — so the Claude-era
matcher blocked enrichment on new files. Align the manifest and test.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: drop changelog entry and trim Droid README row

Per review: remove the Unreleased CHANGELOG addition, and keep the
Droid agents-table row short with a brief --with-hooks mention.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-02 11:53:52 +01:00
Tassamu Akhsan 52d229d517 fix(claude-bridge): restore memory/ subdir in MEMORY.md path (#1134)
The #625 fix removed the memory/ subdirectory and pointed the bridge at
~/.claude/projects/<slug>/MEMORY.md. Claude Code 2.x actually stores
project memory at ~/.claude/projects/<slug>/memory/MEMORY.md (the index)
plus per-topic .md files in the same memory/ dir — verified against the
Claude Code 2.1.141 bundle (constants memory/MEMORY.md) and an existing
project memory dir on disk.

With the current path the bridge writes a MEMORY.md that Claude Code
never reads, so CLAUDE_MEMORY_BRIDGE is silently broken on every
platform. Restore the memory/ segment while keeping the leading-dash
slug fix from #625, and update the path tests accordingly.

Co-authored-by: txtsamu <txtsamu@users.noreply.github.com>
2026-08-02 11:34:59 +01:00
Dmitrii Zhukov a7114c7b68 test(hooks): decouple resolveProject tests from checkout directory name (#1138)
* test(hooks): decouple resolveProject tests from checkout directory name

test/hook-project.test.ts asserted that resolveProject() returns the
literal "agentmemory", which only holds when the repository is cloned
into a directory named exactly "agentmemory". Any fork, worktree or
rename made five tests fail on a clean checkout, blocking the "npm test
must pass" gate in CONTRIBUTING.md.

Run the git-toplevel cases against a throwaway repo created under
tmpdir() with a name the test controls, and stub process.cwd() with
vi.spyOn for the cwd-fallback cases. The assertions still exercise the
real behaviour: dropping the git-toplevel branch or the process.cwd()
default from resolveProject() still fails the suite.

Signed-off-by: Dmitrii Zhukov <dmitry0983@gmail.com>

* test(hooks): ceiling the git search in the non-repo fallback case

The fallback case created its fixture under os.tmpdir() and assumed that
is outside any repository. It is not guaranteed: with TMPDIR pointing at
a working directory, git rev-parse walks up from the fixture, finds the
enclosing repo and returns its basename, so the fallback under test never
runs. Verified by pointing TMPDIR at a checkout — the assertion failed
with the repository's name in place of the fixture's.

Set GIT_CEILING_DIRECTORIES to the fixture's parent for the duration of
the assertion and restore it afterwards. The path is resolved first
because git compares ceiling entries after resolving symlinks, and
os.tmpdir() is a symlink on macOS.

Signed-off-by: Dmitrii Zhukov <dmitry0983@gmail.com>

---------

Signed-off-by: Dmitrii Zhukov <dmitry0983@gmail.com>
2026-08-02 11:33:12 +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
Abdullah Alaqeel 8c90741c63 chore(deps): migrate @xenova/transformers to @huggingface/transformers v4 (#1096)
* chore(deps): migrate @xenova/transformers to @huggingface/transformers v4

@xenova/transformers@2.x is deprecated and silently broken on Node 22+
(see #479). The project was renamed to @huggingface/transformers; same
Apache-2.0 license, same code. v4 ships onnxruntime-node/web and sharp
as hard deps, so they're dropped from our optionalDependencies.

Pipeline / RawImage.fromBlob / tolist / text-classification output
shape all unchanged. Three behavior-preserving adjustments needed:

- All 4 pipeline call sites pass { dtype: "q8" }. v4's default on Node
  is fp32 (DEFAULT_DEVICE_DTYPE = "fp32"); v2 defaulted to quantized=true.
  Without explicit dtype, all 4 sites silently regress (~3.5x larger
  download, slower inference). dtype "q8" maps to model_quantized.onnx
  per v4's DEFAULT_DTYPE_SUFFIX_MAPPING; file exists in all 3 Xenova
  models. This was the regression that prompted the test additions below.
- src/providers/embedding/local.ts: split import try/catch from
  pipeline() call so model-load errors (network, missing q8 variant,
  disk) propagate with their actual message, not masked as
  "Install @huggingface/transformers...".
- src/providers/embedding/{local,clip}.ts: type module from
  typeof import("@huggingface/transformers") so PretrainedModelOptions
  flows through; drop hand-rolled aliases and @ts-ignore. Cast at
  assignment sites (pipeline return union isn't structurally assignable
  to our narrow FeatureExtractor / ClipPipeline shapes).

Tests added where coverage was zero (would have caught the dtype
regression):

- test/local-embedding-provider.test.ts (3 tests): unavailable-path
  install hint; pipeline called with dtype:q8 + extractor options +
  mapped Float32Array result; embedBatch shape.
- test/clip-embedding-provider.test.ts (5 tests): unavailable-path;
  text pipeline dtype:q8 + result; embedBatch; embedImage with data:
  URL decode; custom model ID propagation.
- test/reranker.test.ts: positive-path using vi.doMock + resetModules.

Other:
- src/huggingface.d.ts deleted (package ships its own types).
- src/xenova.d.ts removed.
- src/providers/embedding/clip.ts: inline single-use DIMENSIONS constant.
- tsdown.config.ts: trim neverBundle list and comment.
- README.md L1267: BGE-small -> Xenova/all-MiniLM-L6-v2 (was always wrong).
- 16 docs: install commands + prose mentions across main README, 11
  translations, SECURITY.md, 2 benchmark docs, benchmark script.
- Model IDs (Xenova/all-MiniLM-L6-v2, Xenova/clip-vit-base-patch32,
  Xenova/ms-marco-MiniLM-L-6-v2) kept — HF Hub repo names, still valid.

Closes #1095. Fixes #479.

Verified: 1424/1424 tests pass, build clean, tsc clean on migrated files.

* test(embedding): add v4 smoke test, harden import errors, expand CI matrix

Review follow-ups for #1096:
- env-guarded non-mocked smoke test (RUN_HF_SMOKE=1) loading real
  Xenova/all-MiniLM-L6-v2, asserts 384 finite dims; skipped by default
- selective ERR_MODULE_NOT_FOUND handling in local/clip providers so real
  init errors propagate (checks err.code and err.cause.code to handle
  vitest mock-factory wrapping)
- CLIP install hint made embedding-agnostic (loader serves text + image)
- afterEach mock cleanup in doMock-based provider/reranker tests
- CI Node matrix: [20, 22] -> [20, 22, 24, 26] across ubuntu/macos

* refactor(embedding): drop err.cause check, use manual mock for missing-module tests

The .cause branch in the ERR_MODULE_NOT_FOUND check existed only to
accommodate vitest's mock-factory wrapping, not a real Node loader
behavior. Replace it with a manual mock fixture (__mocks__/@huggingface/
transformers.ts) that throws a Node-shaped error at module top-level,
bypassing vitest's factory wrapper so the import rejects with err.code
set directly.

Production code now checks only err.code === 'ERR_MODULE_NOT_FOUND',
matching real Node behavior. Tests verify the same public contract
without coupling production code to the test framework.
2026-07-29 10:21:54 +01:00
Matt Van Horn d8b5267c36 fix(cli): add --data-dir flag + AGENTMEMORY_DATA_DIR so engine state lives outside repos (#314)
CI / test (20, macos-latest) (push) Has been cancelled
CI / test (20, ubuntu-latest) (push) Has been cancelled
CI / test (22, macos-latest) (push) Has been cancelled
CI / test (22, ubuntu-latest) (push) Has been cancelled
* fix(cli): add --data-dir flag + AGENTMEMORY_DATA_DIR so engine state lives outside repos (#303)

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

* feat(cli): adopt legacy ./data stores before platform-default data dir

Before falling back to the new platform default, detect an existing
./data (prior default) store and keep using it so existing users do not
boot into an empty store. Covers both paths with tests.

* docs(skills): regenerate REFERENCE.md to include AGENTMEMORY_DATA_DIR

The autogen env block in the agentmemory-config skill reference was stale
after adding the --data-dir flag; regenerated via npm run skills:gen so
AGENTMEMORY_DATA_DIR is listed (34 -> 35 recognized variables). Fixes the
failing skills-reference drift check.

* docs: fix the local-models anchor in the provider table

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

* fix: narrow legacy data adoption, XDG relocation, and env export

Addresses the three blocking review items.

1. resolveDataDir only adopts a cwd-local data/ directory when it is actually
   ours, keyed on data/state_store.db or data/iii-config.yaml existing. Before,
   any data/ folder was adopted, so running the CLI in an unrelated repo that
   happens to have one (common in ML projects) would start writing our stores
   into it.

2. cli.ts only exports AGENTMEMORY_DATA_DIR when the user actually supplied a
   --data-dir flag or env value. Exporting it for the default too meant
   ${AGENTMEMORY_DATA_DIR:-iii-data} in docker-compose never fell back to the
   named volume, so existing docker users booted against an empty bind-mounted
   platform dir with their memories stranded in the volume.

3. The XDG relocation now requires the XDG path to actually live under the git
   root, rather than firing whenever cwd is inside any repo with XDG_DATA_HOME
   set. Previously XDG_DATA_HOME=/mnt/data run from a normal repo was ignored
   with a warning claiming it was inside a git worktree when it was not.

The two smaller items you flagged as fine-as-follow-ups (IMAGES_DIR not moving
with --data-dir, and renderIiiConfig rewriting file_path by exact string match)
are untouched here.

---------

Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-25 22:52:31 +01:00
Vidit Gujrathi a8e7d19a81 fix(viewer): surface health status from non-2xx /agentmemory/health responses (#1046)
CI / test (20, macos-latest) (push) Has been cancelled
CI / test (20, ubuntu-latest) (push) Has been cancelled
CI / test (22, macos-latest) (push) Has been cancelled
CI / test (22, ubuntu-latest) (push) Has been cancelled
* fix(viewer): surface health status from non-2xx /agentmemory/health responses

The health endpoint intentionally returns HTTP 503 when status is
"critical" (src/triggers/api.ts), with a valid JSON body describing
the degraded state. The viewer's shared api() fetch helper treated
any non-ok response as a hard failure and discarded the body,
returning null - so renderDashboard's `h.status || 'unknown'` fallback
always showed "unknown" for a critical backend instead of "critical".

Parse and return the JSON body on non-ok responses too (falling back
to null only if the body isn't valid JSON), so the dashboard reflects
the real health status regardless of the HTTP status code used to
carry it.

Fixes #1019

Signed-off-by: Vidit Gujrathi <223816573+viditchess64@users.noreply.github.com>

* fix(viewer): log non-2xx JSON parse failures at debug level

Address CodeRabbit review: don't silently swallow the parse error when
a non-2xx response body isn't valid JSON - log it at debug level to
help diagnose unexpected content types.

Signed-off-by: Vidit Gujrathi <223816573+viditchess64@users.noreply.github.com>

* fix(viewer): restore non-2xx -> null contract in api(), special-case health

Per review: changing the shared api() helper to return parsed bodies on
non-2xx responses broke callers that treat null as "request failed"
(e.g. loadGraph's disabled/error state). Restore the null contract as
the default and let only the health call opt in via readErrorBody,
since /agentmemory/health intentionally responds 503 with a valid JSON
body when status is critical (#1019).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Signed-off-by: Vidit Gujrathi <223816573+viditchess64@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 11:58:13 +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.
v0.9.28
2026-07-19 11:40:26 +01:00
Rohit Ghumare 93ae9bc04f CLI onboarding UX, colored output, build + docs cleanup (#984)
* fix(cli): onboarding UX, colored output, build + docs cleanup

- Clearer, actionable first-run guidance with an honest recovery path when startup can't proceed automatically.
- Quieter boot: collapse the no-provider-key notice to a single dim line.
- Colored CLI output across boot, status, demo, connect, and errors (adds picocolors to deps; panels stay aligned; color auto-disables on non-TTY).
- Build warnings: migrate tsdown deps config, silence per-entry plugin-timings, make dynamic imports consistent. Only the node20 deprecation remains.
- Docs: correct install instructions and pin install paths to the supported version across README and 11 translations; bump deploy Dockerfile version to 0.9.27.

* docs: drop --instance from engine-conflict note

Align the README recovery note with the CLI: stop the other engine, then run agentmemory (installs the pinned engine). --instance does not relocate the engine's config-bound port, so it was misleading.

* docs(agents): latest global install, drop --instance, add conflict step

Install runbook now uses npm install -g @agentmemory/agentmemory@latest (agent + future sessions get newest), removes the misleading --instance/--port port suggestions (neither relocates the config-bound engine), and adds an engine-conflict troubleshooting step (stop the other engine, re-run, pinned engine installs into ~/.agentmemory/bin).

* docs(agents): drop multi-instance --instance note from runbook

* fix: fail-closed engine adopt, image rollback, deploy version pins

- cli.ts: only adopt a running engine when its version positively matches the pin; treat unknown/unverifiable as incompatible (fail closed) instead of adopting and risking a reconnect loop. Message handles the unknown-version case.
- observe.ts: on a failed observation write, roll back via decrementImageRef (deletes the file only when no other observation references it) instead of deleteImage, so a failed write can't orphan a deduped image or leave a stale ref.
- deploy: bump AGENTMEMORY_VERSION to 0.9.27 in the Coolify compose override and the Render blueprint to match the Dockerfile default.

* fix: preserve write error on rollback, install/docs tidy

- observe.ts: wrap the image-ref rollback in try/catch and log on failure so the original observation-write error is still thrown (not masked by a rollback error).
- INSTALL_FOR_AGENTS: port-conflict bullet now lists 3112/3113 too (matches prerequisites).
- README: engine-conflict note uses npx -y ...@latest (matches the runbook).
- Drop redundant @latest from npm install -g (global install already resolves latest); keep @latest only on npx, which caches per version.

* docs: drop issue-number refs from CLI output and READMEs

Issue/PR numbers belong in the CHANGELOG, not in CLI output, README, or comments a reader can't act on. Strip our own #NNN refs (keeping the reasoning) from the no-key/boot notices, README prose + env-example comments, comments in the files this PR touches, and all 11 translated READMEs. Kept external upstream links (openai/codex#16430) and CHANGELOG anchors.

* docs: remove npm downloads badge from README and translations
2026-06-28 20:22:38 +01:00
Ritesh Rana f6f9e3cb13 fix(website): portal mobile nav sheet to body and close breakpoint gap (#670)
* fix(website): portal mobile nav sheet to body so it covers the viewport

The mobile nav sheet was rendered inside the <header> element, which uses
backdrop-filter: blur(10px). Per CSS spec, backdrop-filter establishes a
containing block for fixed-positioned descendants, so the sheet's
`position: fixed; inset: 0` was clipped to the header strip instead of
covering the viewport. The dark overlay only painted across the top bar,
leaving the hero fully visible behind the menu and the panel items
squeezed into a strip too short to render.

Render the sheet through createPortal to document.body so it escapes the
nav's containing block. Drop backdrop-filter on the sheet (no longer
needed once the bg is fully opaque) and switch to var(--abyss) so hero
content can't bleed through even if a browser ignores 0.94 opacity.

Closes #669

* fix(website): close mobile nav gaps on top of the portal fix

Show the hamburger at the same 860px breakpoint that hides the section
links; previously the links disappeared at 860px but the hamburger only
appeared at 720px, leaving tablet widths with no navigation at all.

Make the closed sheet inert so keyboard focus cannot tab into hidden
links, return focus to the toggle when the sheet closes, and add
noreferrer to the external links.

---------

Co-authored-by: Rohit Ghumare <ghumare64@gmail.com>
2026-06-11 10:48:24 +01:00
Rohit Ghumare f3dc7f8ece fix: CLI help and docs papercuts (#897)
* fix: CLI help and docs papercuts

- derive the connect agent list in --help from the adapter registry (18 agents, was a stale 9-name list)
- warn on unknown --tools values instead of silently treating them as all
- point claude-code, hermes, and openclaw protocol notes at real doc URLs (the docs/*.md paths never existed)
- correct skill count to 15 in the connect summary and AGENTS.md
- name the real 8 core tools in INSTALL_FOR_AGENTS.md
- extend the tool-count consistency test to guard skill counts and the core tool sentence

* chore: regenerate skill reference docs
2026-06-11 00:00:10 +01:00
Rohit Ghumare 25e7701460 Adapters declare their own category (finish #872 DRY) (#887)
Onboarding still kept a hardcoded NATIVE_AGENTS set to label the picker (native plugin vs MCP server), decoupled from the adapters and prone to the same drift as the agent list (#872) — it had even mislabeled openclaw. Add a category field to ConnectAdapter (default mcp; factory sets mcp; the 7 native plugin/hook adapters set native), derive the onboarding hint from a.category, and assert every adapter declares one so a future adapter without a category fails CI.
2026-06-10 18:33:33 +01:00
Rohit Ghumare a842adecea Add Docs link to website navbar (#886)
Now that /docs proxies to the Mintlify-hosted docs via the Vercel rewrite, surface a DOCS entry in the site navbar (desktop + mobile, since SECTIONS feeds both).
2026-06-10 14:18:39 +01:00
Rohit Ghumare c2d836d623 Add opencode connect adapter, single-source the onboarding picker (#883)
* fix(connect): add opencode adapter and derive onboarding picker from ADAPTERS

The onboarding agent picker and the connect interactive menu maintained two separate hardcoded lists that had drifted: onboarding offered opencode/goose/kilo/aider/claude-desktop/windsurf/roo (which connect had no adapter for, so picking them hit 'no adapter available, skipped'), while connect had antigravity/kiro/warp/continue/zed/droid/qwen that onboarding never offered.

Add a real opencode adapter writing OpenCode's documented mcp schema (top-level mcp key, command as array, type/enabled), and make onboarding's buildAgentOptions derive from connect's ADAPTERS so the two can never diverge again. Connect-only adapters now appear in onboarding; opencode is now wireable; phantom agents with no adapter no longer appear. Fixes #872.

* fix(connect): harden opencode adapter mcp merge and drop unexpanded env block

Guard the mcp spread against a non-object value in an existing opencode.json, and remove the environment block whose shell-style ${VAR:-default} values OpenCode does not expand (writing them literally would clobber a user's real shell AGENTMEMORY_URL). The stdio child inherits the shell env and the shim defaults unset vars. Also drop a redundant inline comment in onboarding.

* docs(skills): regenerate agents reference for opencode adapter

Adding the opencode adapter changed ADAPTERS (17 -> 18); regenerate the auto-derived agents table so npm run skills:check passes in CI.
2026-06-10 14:15:09 +01:00
Rohit Ghumare 1a7b5ca181 Serve docs at agent-memory.dev/docs via Mintlify rewrite (#885)
Per Mintlify's Vercel deploy guide, serve the docs at agent-memory.dev/docs by rewriting /docs and /docs/:match* to the Mintlify-hosted subdomain. Subdomain assumed to be agentmemory.mintlify.dev (matches the dashboard project name); change that one host if the actual Mintlify subdomain differs.
2026-06-10 13:52:33 +01:00
Rohit Ghumare a688e50538 Join session summaries into GET /agentmemory/sessions (#882)
api::sessions returned sessions straight from KV.sessions and never read KV.summaries, so the REST response and the viewer #sessions tab never showed summaries even after a successful /summarize. Join KV.summaries onto the returned (post-filter) sessions, mirroring the existing mem::context join. Fixes #879.
2026-06-10 10:29:56 +01:00
Rohit Ghumare a76224f098 Update README.md 2026-06-09 16:14:55 +01:00
Rohit Ghumare c504b79633 Add oracleagentmemory to the comparison (#861)
Add oracleagentmemory (Oracle AI Database memory engine, Python-only, v26.4.0) to the README and COMPARISON.md comparisons. Its 94.4% LongMemEval is vendor self-reported, scored with GPT-5.5 at xhigh reasoning and requires an Oracle Database; agentmemory's 95.2% uses free local embeddings with no API key. Marked self-reported and unreproduced alongside MemPalace and supermemory.
2026-06-09 16:13:07 +01:00
Rohit Ghumare 749c2806e0 Refresh competitor star counts and add benchmark caveat (#855)
* docs: refresh competitor star counts and add benchmark methodology note

Update mem0 (58K), Letta (23K), Khoj (35K), and claude-mem (81K) star counts in the README comparison table and benchmark/COMPARISON.md, and add a note that agentmemory's R@5 is LongMemEval-S while the mem0/Letta figures are LoCoMo (different dataset, ballpark only). Verified the iii-engine v0.11.2 pin and per-agent hook counts are current, so they are left unchanged.

* docs: replace claude-mem with supermemory in comparison

* docs: add MemPalace column and mark competitor benchmarks as self-reported

Add MemPalace to the comparison (54K-star vector-only OSS) and label its ~96.6% R@5 as a vendor-published claim we have not reproduced. Clarify that only agentmemory's 95.2% is our own measured, reproducible result and every other number is a vendor claim on a different benchmark or harness, so the figures are ballpark not head-to-head.

* docs: expand README comparison table to full competitor set

Add Khoj, supermemory, MemPalace, and Hippo columns to the README comparison table (previously only mem0, Letta, and Built-in), matching benchmark/COMPARISON.md. Mark MemPalace and supermemory retrieval as vendor self-reported and unreproduced; only agentmemory's R@5 is our own measured result.
2026-06-08 00:04:49 +01:00
Rohit Ghumare 45de643cd2 Detailed, self-updating skills covering the whole system (#854)
* feat(skills): detailed tiered skills covering the whole system

Restructure the 8 action skills into the tiered format (SKILL.md under 100 lines, EXAMPLES.md, shared troubleshooting, anti-patterns, cross-refs) and add 7 reference skills covering MCP tools, REST API, config, connect adapters, hooks, architecture, and skill authoring. Reference data tables are generated from source by scripts/skills/generate.ts and guarded against drift by npm run skills:check in CI, so the docs stay current as the repo changes.

* docs(skills): fix recall REST mapping and label code fences

Correct the recap/handoff REST fallback in _shared/TROUBLESHOOTING.md to POST /agentmemory/smart-search (/agentmemory/recall is not a registered route), and add language identifiers to all opening code fences across the skill docs to satisfy markdownlint MD040.
2026-06-07 18:36:59 +01:00
Rohit Ghumare 7d01d057cd Agent-driven install path, Windows clarity, onboarding cost+inject prompts (#853)
* fix(mcp): correct tool-count drift and add count-consistency guard

Correct stale 51-tool references to the registry truth of 53 across the
standalone shim comments and the registry comment, derive the visible
tool count from the registry, update README test-count stats to 1,423+,
add the demo --serve README line, and add a vitest guard that fails CI
if the registry count, README, or CLI help drift out of sync.

* fix(cli): relax engine version gate and add demo --serve one-shot

Convert the hard process.exit on a PATH iii version mismatch into a
non-fatal warning so a mismatched iii on PATH no longer blocks first
run; agentmemory continues with its own pinned engine in
~/.agentmemory/bin and honors AGENTMEMORY_III_VERSION. Add a --serve
flag to the demo subcommand that boots the server, waits for health,
runs the seed and query demo, and tears the server down on exit, all
in one invocation. Derive the --tools help counts from the registry.

* fix(cli): exit cleanly after demo --serve so in-process worker stops

* feat: agent install runbook, windows clarity, onboarding prompts

Add INSTALL_FOR_AGENTS.md, a runbook a coding agent can execute end to
end: global install, start server, demo --serve, connect MCP, install
skills, and a save/recall verification round-trip.

README Install section now leads with the agent one-liner path and a
short Windows callout (WSL2 is the fast path, native setup is manual and
connect is unsupported there).

Onboarding gains a context-injection step that asks whether to enable
AGENTMEMORY_INJECT_CONTEXT with a token-cost note, persists the choice as
a never-nag prefs flag, and writes the env var when enabled. Provider
selection prints a one-line rough cost hint.

* docs: expand agent install runbook with prereqs, REST shapes, troubleshooting
2026-06-07 12:49:01 +01:00
Rohit Ghumare cc28d61fc4 Fix onboarding stat drift, relax engine gate, one-command demo (#852)
* fix(mcp): correct tool-count drift and add count-consistency guard

Correct stale 51-tool references to the registry truth of 53 across the
standalone shim comments and the registry comment, derive the visible
tool count from the registry, update README test-count stats to 1,423+,
add the demo --serve README line, and add a vitest guard that fails CI
if the registry count, README, or CLI help drift out of sync.

* fix(cli): relax engine version gate and add demo --serve one-shot

Convert the hard process.exit on a PATH iii version mismatch into a
non-fatal warning so a mismatched iii on PATH no longer blocks first
run; agentmemory continues with its own pinned engine in
~/.agentmemory/bin and honors AGENTMEMORY_III_VERSION. Add a --serve
flag to the demo subcommand that boots the server, waits for health,
runs the seed and query demo, and tears the server down on exit, all
in one invocation. Derive the --tools help counts from the registry.

* fix(cli): exit cleanly after demo --serve so in-process worker stops
2026-06-07 12:47:46 +01:00
Rohit Ghumare 25158519d5 chore(release): collapse v0.9.28 into v0.9.27 (not tagged yet) (#851) v0.9.27 2026-06-07 09:55:51 +01:00
Rohit Ghumare a6e22fa9c7 fix(release): v0.9.28 hotfix wave (#843, #825, #817, iii pin) (#849)
* fix(release): v0.9.28 hotfix wave (data loss, graph reset, agent leak, iii pin)

* fix(graph,search): cr followups for #843/#825/#817 wave

* docs(readme): warn import-jsonl users about Claude Code 30-day cleanup
2026-06-07 01:47:56 +01:00
Rohit Ghumare a323fb0b96 chore(release): v0.9.27 (#827) 2026-06-05 14:00:21 +01:00
Rohit Ghumare 2a581408a7 fix(graph): top-degree snapshot cache for /graph/query + /graph/stats (#814) (#816)
* fix(graph): top-degree snapshot cache for query + stats

* fix(consistency): bump REST endpoint count 126 -> 127 for snapshot-rebuild

* fix(graph): incremental indexes + snapshot-only hot path + reset endpoint

* fix(graph): sync degree comparator + composite-key reset + batch backfill

* test(graph): edge-key wipe assertion + worker-death rejection path
2026-06-04 16:29:08 +01:00
Rohit Ghumare 334e5ad81a fix(cli): auto-derive streams + engine ports from REST anchor (#750) (#815) 2026-06-03 21:19:23 +01:00
Rohit Ghumare 3e90110961 docs(readme): refresh stats + v0.9.26 callout + corrected P@5 (#807)
* docs(readme): refresh stats + v0.9.26 callout + corrected P@5

* docs(website): regen generated-meta.json (v0.9.26)
2026-06-03 11:17:52 +01:00
Rohit Ghumare e5c9dc68e4 docs(benchmarks): correct coding-agent-life-v1 P@5 numbers (#796) (#805)
* docs(benchmarks): correct coding-agent-life-v1 P@5 numbers

* Update 2026-05-20-coding-agent-life-v1.md
2026-06-03 10:51:50 +01:00
Rohit Ghumare 7446bded53 fix(release): v0.9.26 hotfix for first-run load crash (#797) (#804)
First boot after upgrading to v0.9.25 threw
'TypeError: Cannot read properties of undefined (reading v)' from
IndexPersistence.loadManifestData. Some iii-state adapters return
undefined (not null) for a missing key. The load path checked
manifest.value !== null and forwarded undefined to loadManifestData,
which then read undefined.v.

Self-healing: the next debounced save rebuilt a fresh manifest, so
operation continued after the warning — but the error scared every
fresh upgrader.

Fix: treat null + undefined + non-object values as 'no manifest' and
fall through to the legacy load path. Two regression tests:

  - kv.get returning undefined surfaces as a clean { bm25: null,
    vector: null } load, no throw.
  - kv.get returning a string (wrong shape) does not crash the
    manifest path.

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

125 test files / 1381 tests pass.

Closes #797.
v0.9.26
2026-06-03 10:16:04 +01:00
Rohit Ghumare d442fee78f chore(release): v0.9.25 (#792)
Bump to 0.9.25 across 9 files + CHANGELOG.

Closes #778 #775 #783 (PR #791), #758 #726 (PR #773), #759 (PR #772),
#752 (PR #774), #729 (PR #780), #781 (PR #782), #753 (PR #789), #771
(PR #786), #762 (PR #764).

Files bumped:
  - package.json
  - packages/mcp/package.json
  - plugin/.claude-plugin/plugin.json
  - plugin/.codex-plugin/plugin.json
  - plugin/plugin.json
  - src/version.ts
  - src/types.ts (ExportData.version union)
  - src/functions/export-import.ts (supportedVersions Set)
  - test/export-import.test.ts (assertion)

125 test files / 1379 tests pass. npm audit (root + website): 0 vulns.
v0.9.25
2026-06-03 00:32:56 +01:00
Rohit Ghumare 1ab0a7d294 Merge pull request #791 from rohitg00/fix/778-fallback-model-resolution
fix: close three v0.9.24 regressions before v0.9.25 (#778 #775 #783)
2026-06-03 00:24:15 +01:00
Rohit Ghumare aa386b7884 fix(release): close three v0.9.24 regressions before v0.9.25
Bundles three independent bug fixes that block the v0.9.25 release.

fix(providers): fallback chain resolves its OWN model, not the primary's (#778)

  createFallbackProvider copied config.model from the primary into
  every fallback. With OpenAI primary + Gemini fallback, Gemini was
  called with 'gpt-4o-mini' and 404'd on every call, which tripped
  the circuit breaker and blocked downstream LLM ops entirely. New
  defaultModelFor() resolves each provider's env-driven default
  (OPENAI_MODEL, GEMINI_MODEL, ANTHROPIC_MODEL, MINIMAX_MODEL,
  OPENROUTER_MODEL) so a fallback config now does what it advertises.

fix(replay): import-jsonl re-keys on parsed.sessionId, not existing.id (#775)

  The existing-session branch wrote kv.set(KV.sessions, existing.id, ...)
  but legacy session rows can be missing the 'id' field. existing.id
  was then undefined, JSON.stringify dropped the key from the state::set
  payload, and the engine rejected the call with 'missing field key'.
  The rejection aborted the whole handler — a single legacy row killed
  the entire import. Now re-keys on parsed.sessionId (always populated
  by parseJsonlText's three-level fallback) and backfills missing
  existing.id so future reads stay clean.

fix(summarize): strip markdown fences + retry final parse on failure (#783)

  parseSummaryXml passed the raw LLM payload to the tag regex.
  DeepSeek, GPT, and others wrap XML in  fences or
  prepend a conversational sentence, which caused a silent
  parse_failed and the session was never summarized. Added
  stripXmlWrappers() that peels markdown fences and a surrounding
  pre/postamble before the regex runs. Also extended the chunk-level
  2-attempt retry pattern to the final summarize merge so a flaky
  wrap on the first attempt doesn't drop the summary.

Tests (10 new total)

  test/fallback-model-resolution.test.ts (5): primary/fallback model
  separation across OpenAI/Gemini/Anthropic/Minimax/OpenRouter,
  env override precedence, duplicate-provider skip.
  test/replay-import-key.test.ts (2): id-less existing row no longer
  aborts the import; fresh import path unchanged.
  test/summarize.test.ts (3): markdown-fenced XML parses cleanly,
  final retry recovers on first-attempt parse failure, double-fail
  still surfaces parse_failed.

Full suite: 125 files / 1379 tests pass.

Closes #778, closes #775, closes #783.
2026-06-03 00:18:40 +01:00
Rohit Ghumare 3e806c9167 Merge pull request #789 from rohitg00/fix/753-graph-pagination
fix(viewer): graph tab blank on large graphs (graph/query pagination)
2026-06-03 00:00:11 +01:00
Rohit Ghumare a3fb7aeab5 Merge pull request #764 from Rokurolize/fix/762-shard-index-persistence
fix(index-persistence): shard persisted search snapshots
2026-06-02 23:54:35 +01:00
Rohit Ghumare 436c86c86d fix(viewer): graph tab blank on large graphs (graph/query pagination)
Closes #753.

POST /agentmemory/graph/query with an unbounded body ({}) on a corpus
with 11k+ nodes returned HTTP 500 'Invocation stopped' because the
response payload exceeded the iii state response channel ceiling.
Viewer's api() returns null on non-2xx so the failure surfaced as
'0 nodes / 0 edges' on a blank canvas with no signal — same shape as
#544, never covered for graph/query.

Backend (src/functions/graph.ts)

- mem::graph-query accepts limit + offset on every branch and applies
  a default cap (DEFAULT_GRAPH_QUERY_LIMIT = 500, MAX = 5000) so an
  empty {} body never materializes the whole graph in one invocation.
- Empty-body / nodeType-only branch ranks nodes by incident-edge
  degree before paginating so the truncated page surfaces the densest
  subgraph rather than an arbitrary KV scan order.
- Edges in the page response are restricted to edges with BOTH
  endpoints in the page; cross-page edges are dropped so the viewer
  doesn't render dangling lines.
- Response shape extended: totalNodes, totalEdges, truncated, limit,
  offset. totalNodes / totalEdges reflect the unbounded result for the
  given filter so callers can render 'showing X of Y' without
  re-querying.

API (src/triggers/api.ts)

- api::graph-query whitelists payload fields (startNodeId, nodeType,
  maxDepth, query, limit, offset) per AGENTS.md security rule. No
  more raw req.body spread.

Viewer (src/viewer/index.html)

- Initial /graph/query sends an explicit limit (500) rather than {}.
- Distinguishes apiPost === null (server error) from
  { nodes: [], edges: [] } (truly empty corpus). Failure now renders
  a visible 'Graph query failed' banner with a Retry button instead
  of the 'No graph data yet. Building...' empty state.
- Truncation banner above the search box: 'Showing N of M nodes
  (most-connected first). The full graph is too large to render at
  once.' so operators know the page is bounded.
- state.graph gains queryError, truncated, totalNodes, totalEdges
  fields.

Tests (test/graph.test.ts)

- Unbounded {} body caps to 500 with truncated=true on a 1,200-node
  seed; high-degree nodes land on page 1.
- limit + offset paginate without overlap on a 50-node seed.
- limit clamped above MAX returns at most MAX.
- Cross-page edges excluded from page response; totalEdges still
  counts unbounded.

121 test files / 1330 tests pass.
2026-06-02 23:50:59 +01:00
Rohit Ghumare 9a4bdbe0e8 feat(diag): smart-search followup-rate proxy + reader-failure counter (#786)
* feat(diag): smart-search followup-rate proxy + reader-failure counter

Closes #771.

Disambiguates 'retrieval bug' vs 'reader bug'. Today every retrieval
regression looks identical to a reader-behavior regression because we
have no signal for 'the right memory was in the context but the agent
didn't use it'. This change ships the live-use proxy half end-to-end
plus the placeholder counter the benchmark scorer will increment.

Live-use proxy
--------------

mem::smart-search now detects follow-up searches: when an agent-initiated
call (sessionId present, source !== 'viewer') is followed by another
search from the same session inside AGENTMEMORY_FOLLOWUP_WINDOW_SECONDS
(default 30s) AND the new result set has zero overlap with the prior
one AND the queries differ, we count it as a directional signal that
the first results didn't satisfy the agent.

  - New KV scope mem:recent-searches holds the most recent search per
    session: { sessionId, query, resultIds, at }. sessionId is
    duplicated into the row so the sweep can delete by it (StateKV.list
    returns values only).
  - mem::diagnostic::recent-searches-sweep runs hourly, drops rows
    older than 24h.
  - OTEL counter agentmemory.smart_search.followup_within_window_total
    fires on every detected follow-up. Mirrored to a module-scope
    counter for read-back without an OTEL collector.
  - mem::diagnostic::followup-stats exposes the live counts;
    GET /agentmemory/diagnostics/followup returns them with a
    'directional / may overcount on refinement' caveat.
  - agentmemory status surfaces 'Followup rate: H/T (P%) within Ws'.

Benchmark-mode counter
----------------------

Counter agentmemory.reader_failure_with_evidence_total is registered
but never incremented by core in live use. The benchmark scorer
(separate from this PR) increments it when judge_correct === false AND
the gold-evidence IDs are a subset of the retrieved-context IDs.

Exclusions
----------

  - Viewer-originated searches (X-Agentmemory-Source: viewer or body
    field source: 'viewer') skip the diagnostic entirely. The API
    trigger reads the header and threads it into the smart-search
    payload; the body field wins if both are set.
  - Direct sdk.trigger callers without a sessionId are skipped.
  - Identical re-query is a retry, not a follow-up (skipped).
  - Result-set overlap means the prior results were used (skipped).

Edge cases covered by tests
---------------------------

  - First call records but never flags.
  - Disjoint results within window: flagged.
  - Overlapping results within window: not flagged.
  - Identical query (retry): not flagged.
  - Prior search outside window: not flagged.
  - Viewer source: skipped, scope unchanged so subsequent agent calls
    don't see the viewer search as 'prior'.
  - No sessionId: skipped.
  - Sweep deletes stale (>24h) rows, keeps fresh ones.

Endpoint count bumped 125 -> 126 across README / AGENTS / index banner.

* fix(diag): whitelist smart-search payload, off-critical-path followup detection

Address CodeRabbit review on PR #786.

src/triggers/api.ts: api::smart-search was spreading req.body into the
sdk.trigger payload, which violates the AGENTS.md security rule that
REST endpoints must whitelist fields. Replace the spread with an
explicit field list (query, expandIds, limit, project, includeLessons,
agentId, sessionId, source). Unknown fields from the client are now
dropped before reaching mem::smart-search.

src/functions/smart-search.ts: followup detection used to await
kv.get + kv.set on the critical response path, adding one round-trip
of state I/O per agent-initiated search. Move detection into a
withKeyedLock('recent-searches:<sessionId>', ...) call that runs
fire-and-forget. The lock serializes per session so the next call's
detectFollowup still reads the prior call's row (lock body queues
behind), and other sessions run in parallel. flushPendingFollowups()
exposes a drain hook for tests and graceful-shutdown paths.

Empty-result calls now skip detection entirely. A search that returns
zero hits is a retrieval failure, not a reader-failure signal — the
old code treated the empty-vs-prior comparison as 'disjoint' and would
inflate the rate every time search returned nothing.

src/state/schema.ts unchanged. RecentSearch now exported from
smart-search.ts and consumed by recent-searches-sweep.ts so the row
shape stays consistent across producer and sweeper.

Audit logging on every recent-searches write was suggested but
skipped — high-frequency diagnostic writes don't belong in the audit
log (analogous flows like recordAccessBatch and dedup don't audit
either; it would 10x audit volume without operator-actionable value).

Tests: added an empty-results case (sub-point b) that asserts the
empty-skip guard. Mock sdk.trigger now flushes pending followups
after each smart-search call so test assertions see consistent state.

122 test files / 1336 tests pass.
2026-06-02 20:18:30 +01:00
Rohit Ghumare fc068bee66 fix(agent-sdk): scope recursion guard to AsyncLocalStorage, memoize SDK import (#782)
* fix(agent-sdk): scope recursion guard to AsyncLocalStorage, memoize SDK import

Reported in #781. mem::summarize fails with
'too_many_chunks_skipped: 4/4 chunks failed to parse after retry' on
the agent-sdk provider whenever a session is large enough to split
into >=2 chunks.

Root cause: the recursion guard from #181 mutated process.env globally:

  if (process.env.AGENTMEMORY_SDK_CHILD === '1') return ''
  process.env.AGENTMEMORY_SDK_CHILD = '1'
  // ...await SDK...
  // restore

#472 then introduced chunked summarize that runs chunks concurrently via
Promise.all (SUMMARIZE_CHUNK_CONCURRENCY=6 by default). The first chunk
in a batch flips the env synchronously before its first await; siblings
in the same batch then enter query(), see the flag, and return ''. An
empty string has no <title>, parseSummaryXml returns null, the chunk
is counted as skipped, >50% skip ratio throws too_many_chunks_skipped.

The guard (cross-process) and the chunk concurrency (in-process) were
never reconciled. Each needs the right primitive:

  - In-process recursion guard: AsyncLocalStorage. Scoped to the async
    call tree of the SDK query, so concurrent siblings have separate
    ALS frames and no longer see each other's marker. The check now
    reads sdkChildContext.getStore() instead of process.env.

  - Cross-process recursion guard for hook scripts: still
    process.env.AGENTMEMORY_SDK_CHILD = '1' around the SDK call.
    Subprocesses spawned by @anthropic-ai/claude-agent-sdk inherit
    process.env at spawn time; hook scripts (separate processes) still
    read the env to short-circuit their REST callbacks. The race on
    the env between concurrent in-process siblings is benign because
    every sibling wants the same value '1' during its own SDK call.

Also memoize the dynamic import of @anthropic-ai/claude-agent-sdk on
the provider instance. Production benefit: concurrent callers share
one module resolution. Test benefit: vi.mock factories apply uniformly
across concurrent imports instead of racing between mock and real
module references.

Tests cover:
- 4 concurrent summarize calls each return the real SDK result (no
  empty siblings) — the direct #781 regression
- Mixed concurrent summarize + compress on the same provider
- AGENTMEMORY_SDK_CHILD is set to '1' during the SDK call (child
  process inheritance) and restored to the prior value on exit
- Genuine re-entry inside the same async tree still degrades to '' so
  the #149 / #181 stop-hook -> summarize recursion stays broken

Closes #781.

* fix(agent-sdk): refcount env marker + vi.hoisted mock state

Address CodeRabbit findings on PR #782.

agent-sdk.ts: the per-call prev snapshot races across overlapping
calls. Trace:

  - A enters: prev=undef, env='1'
  - B enters: prev='1' (already set by A), env='1'
  - A finishes first, restores prev=undef -> env=undef WHILE B is still
    mid-flight; any child process B spawns now won't inherit the marker
  - B finishes, restores prev='1' -> env='1' leaks past the last caller

Replace with module-level reference counting:

  let sdkActiveCount = 0
  let sdkOriginalEnv: string | undefined

  if (sdkActiveCount === 0) {
    sdkOriginalEnv = process.env.AGENTMEMORY_SDK_CHILD
    process.env.AGENTMEMORY_SDK_CHILD = '1'
  }
  sdkActiveCount++
  try { ...SDK call... } finally {
    sdkActiveCount--
    if (sdkActiveCount === 0) {
      // restore sdkOriginalEnv (delete if undefined)
    }
  }

Only the first entrant snapshots the original; only the last exit
restores it. Env stays '1' for the full overlap.

test/agent-sdk-provider.test.ts: vi.mock factories are hoisted above
const/let bindings, so the closure can't safely reference non-hoisted
module-scope state. Move queryCalls and mockResult into a vi.hoisted
state object so the mock factory and tests share an explicitly
hoisted reference instead of a closure capture.

Also fixed a test-mock bug while adding the overlap regression: the
mock yielded the unwrapped Promise from a function-shaped mockResult.
The gen now awaits the mockResult inside the generator so async mock
results are unwrapped before yielding.

New regression test: 3 concurrent calls each observe AGENTMEMORY_SDK_CHILD='1'
at enter and exit phases for the full overlap, and env is restored to
undefined only after the last call finishes.

122 test files / 1332 tests pass.
2026-06-02 17:20:49 +01:00
Rohit Ghumare de95403e42 fix(obsidian-export): harden against null fields + never throw to HTTP layer (#780)
Reported in #729. mem::obsidian-export crashed with HTTP 500
'{"error":"[object Object]"}' when any session/memory/lesson/crystal
in the store was missing its id, and zero files were written. The
filename was built via sanitize(X.id) before the per-record try block,
so undefined.replace(...) escaped the handler entirely. The same
unguarded pattern applied to memories, lessons, crystals, and sessions;
relatedIds/concepts/tags/keyOutcomes/etc. assumed non-null arrays;
title/content/narrative assumed non-empty strings; sessions sorted on
new Date(undefined).getTime() = NaN.

This change applies four layers of defense:

1. hasExportId() typeguard filters every per-tier loop, so id-less
   records are skipped instead of crashing. The filter runs before
   sanitize() so the cast to string is safe.
2. safeArray / safeString / safeTimestamp helpers normalize optional
   record fields. Missing arrays render as omitted sections instead of
   crashing on .map / .length / .slice. Missing titles / content /
   narrative fall back to the record id so the export file is still
   useful.
3. An outer try/catch wraps the whole export body and returns
   {success: false, error: <string>} on any unexpected throw. The HTTP
   layer's serializer never sees an Error object now, killing the
   '[object Object]' symptom even if a future refactor reintroduces an
   unguarded path.
4. Sessions sort with safeTimestamp(), so malformed startedAt values
   (undefined / not-a-date) yield 0 instead of NaN and don't poison
   the ordering.

Tests cover each null path:
- id-less records across all four tiers
- malformed startedAt timestamps
- null optional array fields (concepts/files/relatedIds/supersedes)
- missing title / content / narrative (id fallback in heading)
- engine-level throws turn into {success: false, error: <string>}

121 test files / 1331 tests pass.

Closes #729.
2026-06-02 12:26:28 +01:00