mirror of
https://github.com/rohitg00/agentmemory.git
synced 2026-09-14 20:16:33 +08:00
3bad5430ed
* fix: stop burning Claude Pro tokens on every tool call (#143) 0.8.8 fixed the agentmemory-side Claude API burn (where the engine called Claude via the user's ANTHROPIC_API_KEY for per-observation compression). That addressed #138 for users with API keys, but it missed the second and much larger token-burn path: the PreToolUse hook writing context to stdout. Claude Code reads PreToolUse stdout and prepends it to the model's next turn. src/hooks/pre-tool-use.ts was POSTing /agentmemory/enrich on every Edit/Write/Read/Glob/Grep tool call and piping up to 4000 chars of response context into stdout. At ~20 tool calls per user message this silently injected ~20K tokens per message into Claude Code's input window — all charged against the user's Claude Pro allocation because Claude Code was the one sending them to Anthropic. 4 messages drained the cap, which matches @adrianricardo's report. session-start.ts had the same pattern (injected once per session, smaller blast radius). Fix: gate both hooks on AGENTMEMORY_INJECT_CONTEXT, default false. - pre-tool-use.ts: when disabled, exit immediately — no stdin read, no fetch, no stdout write. The hot path (~20x per message) becomes a no-op Node startup. - session-start.ts: when disabled, still POST /agentmemory/session/start so the session gets registered for observation tracking, but never write context to stdout. The session registration is cheap and doesn't touch Claude Code's input window. - src/config.ts: new isContextInjectionEnabled() helper. - src/index.ts: startup banner prints 'Context injection: OFF (default, #143)' on normal startup and a loud WARNING when opt-in is enabled. - test/context-injection.test.ts: 5 subprocess tests that spawn the compiled pre-tool-use.mjs and session-start.mjs hooks, feed real JSON payloads via stdin, and assert stdout is empty in all the off/default paths. Also asserts the disabled path exits under 1s and the opt-in path with an unreachable backend still exits cleanly. - README .env section: new AGENTMEMORY_INJECT_CONTEXT entry. - CHANGELOG [0.8.10] with prominent 'Behavior change' banner. Observations are still captured via PostToolUse regardless of the flag — the memory store and MCP search tools are completely unaffected by this change. The fix only severs the path where agentmemory silently shoves memory context into the user's Claude Code conversation. Bumps to 0.8.10 (main + @agentmemory/mcp shim). Test count: 724 passing (was 719 + 5 new). * chore: rewrite #143 CHANGELOG entry with corrected diagnosis PreToolUse stdout is NOT injected into the model context — per the Claude Code hook docs, only UserPromptSubmit and SessionStart stdout are injected. My initial #143 PR description and CHANGELOG claimed PreToolUse was the smoking gun behind 'Pro allocation burned in 4 messages', which is wrong. What's actually true: - SessionStart stdout injection IS real (~1-2K tokens per session) - PreToolUse stdout goes to debug log only — no tokens - Claude Pro's Claude Code quotas are tight by design (Anthropic has publicly acknowledged this); 4 messages to burn is plausible with or without agentmemory installed The gate on pre-tool-use.ts is still worth keeping as a resource cleanup (skips a 20x-per-message Node+HTTP hot path) and as forward-compat protection in case Claude Code ever changes PreToolUse hook contract. But the CHANGELOG entry has to stop claiming it saves tokens when it doesn't. * fix: mem::retention-evict no longer leaks semantic memories (#124) The eviction loop was unconditionally calling kv.delete(KV.memories, id) for every below-threshold candidate, but retention scores are computed for both episodic (KV.memories) and semantic (KV.semantic) memories. When a candidate came from KV.semantic, the delete silently became a no-op (key wasn't in mem:memories to begin with) and the semantic row stayed alive forever with a sub-threshold score. Semantic memories could not be evicted by this path at all. Fix: - Add a source: "episodic" | "semantic" discriminator to RetentionScore - Tag it at score creation in both loops of mem::retention-score - Branch the delete in mem::retention-evict on candidate.source, routing to KV.memories or KV.semantic accordingly - Pre-0.8.10 retention rows with no source field are treated as episodic for backwards-compat so upgraded stores continue to evict their old rows without re-scoring first - Response now includes evictedEpisodic and evictedSemantic counts so callers can see what was removed from each scope Adds 3 regression tests to test/retention.test.ts: - Scoring tags rows with the correct source - Evicting a mixed set of below-threshold episodic + semantic candidates removes both from their respective scopes - Legacy-shape score rows with no source field still evict to mem:memories (backwards-compat) Full suite: 727 passing (was 724 + 3 new). * review: probe namespaces for legacy retention rows (#124, round 2) CodeRabbit caught a real backwards-compat hole in the #124 fix: pre-0.8.10 stores already contain semantic retention rows with no source field (because the old mem::retention-score scored KV.semantic before the discriminator existed). My first fix defaulted missing source to episodic, which meant those legacy semantic rows still got delete-routed to KV.memories — the exact no-op that stranded them in the first place. Fix: when candidate.source is undefined, probe KV.memories first for the memoryId; if it's there, route to episodic, otherwise route to semantic. Count the resolved source in the response. Adds one new test case: a pre-0.8.10 semantic memory with a legacy-shape retention row (no source field) gets evicted from mem:semantic, not silently no-op'd. Existing 'defaults to episodic' test is kept and retargeted to the genuinely-episodic legacy case. Also fixes a README nit: the AGENTMEMORY_INJECT_CONTEXT comment previously implied SessionStart fires on every tool turn. It's once per session. Now broken out into two bullets explaining what each hook does differently, with the note that only SessionStart actually reaches the model (PreToolUse stdout is debug-log only per Claude Code docs). Full suite: 728 passing (was 727 + 1 new). * review: audit retention evictions + assert persisted source (#124 round 3) CodeRabbit round 3 findings, both real: 1. retention-evict performs structural deletes (memories / semantic / retention scores / access logs) but was not calling recordAudit(). Repo learnings say state-changing functions must be auditable except for read-path bookkeeping. Now emits one batched audit row per non-zero eviction sweep: operation: 'delete' functionId: 'mem::retention-evict' targetIds: every evicted memoryId details: { threshold, evicted, evictedEpisodic, evictedSemantic, reason: 'retention score below threshold' } Zero-eviction sweeps intentionally do NOT write an audit row (no state change, no need to flood the audit log during health checks). 2. The #124 scoring test only checked result.scores (transient response) but not the persisted mem:retention rows. Eviction reads back from stored rows, so a regression in kv.set/serialization would have still passed the old assertion. Now also does kv.get('mem:retention', id) and asserts { source: ... }. Two new tests: - Retention evict with a mixed set of 2 episodic + 1 semantic candidates writes exactly one audit row with all 3 ids in targetIds and the correct evictedEpisodic/Semantic breakdown in details. - Retention evict with zero candidates writes zero audit rows. Full suite: 730 passing (was 728 + 2 new). * review: audit retention-score + parallelize writes (#124 round 4) CodeRabbit round 3 outside-diff findings, both addressed: 1. mem::retention-score was persisting schema-relevant writes to KV.retentionScores (1000+ rows in a mature store) but never called recordAudit(). Per the repo audit-coverage policy, state-changing functions need an audit row. Added a single batched audit event per rescore: operation: 'retention_score' (new audit op — added to the AuditEntry union in types.ts) functionId: 'mem::retention-score' targetIds: [] (intentionally empty — a mature store can have 1000+ ids per sweep; flooding the audit log with every memoryId on every cron tick is worse than recording just the summary counts) details: { total, episodic, semantic, tiers, config } Zero-memory stores intentionally skip the audit call. 2. The per-memory kv.set inside the score loop was O(n) sequential round-trips. Refactored to collect pendingWrites: [id, entry][] while iterating, then flush with Promise.all at the end. On a mature store with 1000+ memories this is ~10x faster (depends on backend pipelining). Test updates: - Added 'mem::retention-score emits audit row per rescore' covering the new audit call, targetIds=[], and details.episodic/semantic. - Existing '#124 audit evict' and 'zero-evict skip audit' tests now filter the audit log by functionId === 'mem::retention-evict' because retention-score also writes one row per sweep now. Full suite: 731 passing (was 730 + 1 new, existing tests retargeted).