main
482 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
2d38dafede | chore: 0.9.29 release prep (#1216) v0.9.29 | ||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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. |
||
|
|
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 |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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) |
||
|
|
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. |
||
|
|
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> |
||
|
|
a8e7d19a81 |
fix(viewer): surface health status from non-2xx /agentmemory/health responses (#1046)
* 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> |
||
|
|
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 |
||
|
|
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 |
||
|
|
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> |
||
|
|
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 |
||
|
|
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. |
||
|
|
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). |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
a76224f098 | Update README.md | ||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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 |
||
|
|
25158519d5 | chore(release): collapse v0.9.28 into v0.9.27 (not tagged yet) (#851) v0.9.27 | ||
|
|
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 |
||
|
|
a323fb0b96 | chore(release): v0.9.27 (#827) | ||
|
|
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 |
||
|
|
334e5ad81a | fix(cli): auto-derive streams + engine ports from REST anchor (#750) (#815) | ||
|
|
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) |
||
|
|
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 |
||
|
|
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
|
||
|
|
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 |
||
|
|
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) |
||
|
|
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. |
||
|
|
3e806c9167 |
Merge pull request #789 from rohitg00/fix/753-graph-pagination
fix(viewer): graph tab blank on large graphs (graph/query pagination) |
||
|
|
a3fb7aeab5 |
Merge pull request #764 from Rokurolize/fix/762-shard-index-persistence
fix(index-persistence): shard persisted search snapshots |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |