* perf(hooks): fire-and-forget telemetry hooks (closes#573)
Telemetry hooks (notification, post-tool-failure, post-tool-use,
prompt-submit, stop, session-end, subagent-start, subagent-stop,
task-completed) previously `await fetch(..., AbortSignal.timeout(N))`
inside a try/catch. The await kept the hook process alive until the
response arrived — up to N ms per request — which blocks Claude Code's
next-prompt boundary on every assistant turn.
Switch to fire-and-forget:
fetch(url, { signal: AbortSignal.timeout(N) }).catch(() => {});
setTimeout(() => process.exit(0), 500).unref();
The unawaited fetch dispatches the request; the unref'd setTimeout
force-exits the process after the request has been flushed to the
local daemon's socket buffer (~500ms is enough). Without the
setTimeout Node keeps the event loop alive waiting for any in-flight
fetch to settle, which means the hook still blocks Claude Code's
next-prompt boundary for up to the AbortSignal duration.
Context-injecting hooks (pre-tool-use, pre-compact, session-start)
still use `await fetch` because Claude Code reads their stdout for
context injection — left untouched.
AGENTS.md updated with the two-pattern guidance.
* chore(hooks): drop verbose comments on fire-and-forget hooks
* fix(hooks): bump stop+session-end exit delay to 1500ms
Multi-request hooks (stop fires 2, session-end up to 4) need more
than 500ms to initiate all fetches when AGENTMEMORY_URL points to a
remote daemon — DNS + TCP + TLS handshakes can eat the budget before
the second/third fetch is even dispatched. Bump to 1500ms on those
two hooks only; single-request hooks keep 500ms.
AGENTS.md updated with the multi-request exception.
* fix(hooks): send repo basename as project, not full path (closes#474)
Hooks were sending `data.cwd` (an absolute filesystem path) as the
`project` field on every observe/session/start call. Native sessions,
replay-import, and manual memory_lesson_save calls all use the repo
basename. The mismatch caused auto-injected context to filter out the
bulk of relevant lessons because the path never matches the stored
project name.
Add shared `resolveProject(cwd)` helper:
1. AGENTMEMORY_PROJECT_NAME env (per-repo escape hatch)
2. basename of `git rev-parse --show-toplevel` (handles subdirs)
3. basename of cwd (final fallback when not in a git repo)
Applied to 9 hooks: notification, post-tool-use, post-tool-failure,
prompt-submit, session-start, subagent-start, subagent-stop,
task-completed, pre-compact.
Build: split hook entries into per-entry tsdown configs so each hook
bundles into a fully self-contained .mjs. Previous shared config
hoisted helpers into hashed chunks that changed on every rebuild.
* chore(hooks): drop issue-number ref from resolveProject comment
* chore: trim verbose comments on _project.ts + tsdown.config
Findings verified against current code on this branch; all four valid.
1. config.ts loadFallbackConfig (L281) — user could set
FALLBACK_PROVIDERS=agent-sdk and bypass the AGENTMEMORY_ALLOW_AGENT_SDK
gate added to detectProvider. Filter it out at the fallback layer too,
with the same warning pointing at the opt-in flag.
2. summarize.ts (L87-92) — the empty_provider_response branch returned
without recording failure metrics or a diagnostic log, unlike the
parse/validation paths. Record the same metricsStore failure event and
log provider name, prompt size, system size, and observation count so
empty responses are visible in telemetry.
3. providers/agent-sdk.ts (L14-45) — setting
process.env.AGENTMEMORY_SDK_CHILD = '1' without restoring it caused
every subsequent .query() in the same parent process to hit the
short-circuit guard and return '' (classified as a SDK child it is
not). Capture prev, set in try, restore in finally (delete if prev
was undefined). Child processes spawned during the for-await loop
still inherit the marker because env is inherited at spawn time; we
only restore after the loop completes.
4. plugin/scripts/sdk-guard-DI1NUOS9.mjs — tsdown extracted the shared
guard helper into a hashed chunk. Hash rotates on every rebuild and
churns the diff. Stopped using the shared module from hooks entirely
and inlined the 6-line guard function into each hook .ts file
instead. sdk-guard.ts stays in the tree because the unit tests cover
it directly. Deleted the tracked hashed .mjs and confirmed no new
chunk is emitted.
Also applied the CI two-step install (npm install --package-lock-only
then npm ci) on this branch, matching #184. Without it, npm ci fails
because lockfiles are gitignored.
Tests: 74 files / 819 tests pass.
Reported: a user with no provider API key and AGENTMEMORY_AUTO_COMPRESS=false
(which they believed protected them) hit unbounded recursion — Stop hook
POSTs /agentmemory/summarize, handler calls provider.summarize(), agent-sdk
provider spawns @anthropic-ai/claude-agent-sdk query(), which creates a full
CC-style child session that reads ~/.claude/settings.json, registers the
same plugin hooks, and fires its own Stop -> another child -> loop. ~579
ghost 'entrypoint: sdk-ts' sessions accumulated in a few minutes, draining
Claude Pro tokens.
#149 only added a stderr warning. AGENTMEMORY_AUTO_COMPRESS gated /compress
but never /summarize, so users who followed the warning's implied guidance
still got hit. Fix the loop at every layer:
1. config.ts detectProvider
- Treat empty-string provider keys (ANTHROPIC_API_KEY=) as unset; they
previously passed the truthiness check identically to a real key.
- Stop defaulting to agent-sdk. When no key is set, return a 'noop'
provider config and warn. Agent-sdk fallback now requires an explicit
AGENTMEMORY_ALLOW_AGENT_SDK=true opt-in with a loud second warning.
2. providers/noop.ts (new) + providers/index.ts
- NoopProvider implements MemoryProvider and returns empty strings for
compress and summarize so callers can detect .name === 'noop' and
short-circuit without spawning anything.
- Add ProviderType 'noop' and wire it through createBaseProvider.
3. providers/agent-sdk.ts
- Before spawning query(), check process.env.AGENTMEMORY_SDK_CHILD === '1'
and return '' instead of recursing. Set the env var to '1' before the
spawn so any child process (including the Agent SDK session's hooks)
inherits it.
4. hooks/sdk-guard.ts (new) + all 12 hook scripts
- Shared isSdkChildContext(payload) checks both AGENTMEMORY_SDK_CHILD=1
and payload.entrypoint === 'sdk-ts' (CC writes this into the stdin
jsonl for SDK-spawned sessions). Every hook script now bails early
when that returns true, so even if one guard layer fails the others
break the loop.
5. functions/summarize.ts
- Short-circuit with {success:false, error:'no_provider'} when
provider.name === 'noop' — never reach .summarize().
- Treat an empty provider response as empty_provider_response instead
of trying to parse it.
Tests: 74 files / 819 tests pass (+7 new in stop-hook-recursion-guard.test.ts).
Defense in depth means any ONE of the five layers breaks the loop.
New hooks (7):
- PreToolUse: inject file history before edits (Edit/Write/Read/Glob/Grep)
- PostToolUseFailure: capture error patterns for learning
- PreCompact: preserve memory context through compaction
- SubagentStart/SubagentStop: track multi-agent workflows
- Notification: capture permission prompts (tool preferences)
- TaskCompleted: track team task completions
New functions (4):
- mem::file-context: file-centric memory index for PreToolUse
- mem::consolidate: merge observations into long-term memories via LLM
- mem::patterns + mem::generate-rules: detect co-change patterns and recurring errors
- mem::remember + mem::forget: explicit save/delete for long-term memory
MCP server (2 endpoints):
- GET /agentmemory/mcp/tools: list 5 MCP tools (recall, save, file_history, patterns, sessions)
- POST /agentmemory/mcp/call: dispatch tool calls to iii functions
Skills (4):
- /recall [query]: search past observations
- /remember [insight]: save to long-term memory
- /session-history: show past session timeline
- /forget [target]: delete specific memory data
New API endpoints (7):
- POST /agentmemory/file-context
- POST /agentmemory/remember
- POST /agentmemory/forget
- POST /agentmemory/consolidate
- POST /agentmemory/patterns
- POST /agentmemory/generate-rules
- GET/POST /agentmemory/mcp/*
Updated: types (7 new HookTypes, 3 new ObservationTypes), version 0.2.0