* feat: add Copilot CLI plugin asset slice
- plugin/.plugin/plugin.json: Copilot manifest with name/version/skills/mcpServers/hooks refs
- plugin/.mcp.copilot.json: MCP server config with type:local, npx, env passthrough, tools:[*]
- plugin/hooks/hooks.copilot.json: Copilot hooks (version:1) with 11 supported events and PreToolUse matcher
- test/copilot-plugin.test.ts: 11 tests covering manifest, MCP config, and hooks validation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add Copilot CLI connect support
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add GitHub Copilot CLI support
Adds Copilot CLI support through a root plugin manifest, Copilot-specific MCP and hook configuration, and a connect adapter for MCP-only setup.
Includes Windows-safe Copilot MCP command generation, COPILOT_HOME handling, Copilot hook payload normalization, generated hook scripts, and targeted tests for plugin shape, hook execution, and connect behavior.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Harden Copilot hook handling
Addresses upstream AI review suggestions by aligning the Copilot preToolUse matcher with the hook allowlist, narrowing hook payload fields at runtime, normalizing subagent fallbacks, and tightening hook config validation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add Copilot to first-run onboarding
Includes GitHub Copilot CLI in the first-run agent picker and adds a regression test so the Copilot setup path remains discoverable.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Default onboarding to Copilot inside Copilot CLI
Detect Copilot CLI environment markers during first-run setup so pressing Enter wires the current agent instead of the historical Claude Code default.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Support framed stdio MCP transport
Accept Content-Length framed JSON-RPC messages in addition to the existing newline-delimited transport so Copilot CLI can initialize the standalone MCP server.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Narrow Copilot pre-tool session ids
Ensures pre-tool-use only forwards string session IDs and falls back to unknown for invalid Copilot payload values, with regression coverage for the generated plugin script.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Ross Story <rostory@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Rohit Ghumare <ghumare64@gmail.com>
* 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.
Codex does not fire a separate SessionEnd event, so its Stop hook is
the only signal we get when a Codex session terminates. The current
stop hook only POSTs /agentmemory/summarize, leaving the session row
stuck on status:"active" in the viewer for Codex users (#493).
Stop now ALSO POSTs /agentmemory/session/end, best-effort with a
short 5s timeout. For Claude Code this is a harmless idempotent second
call (session-end.mjs runs on the dedicated SessionEnd hook and sets
the same endedAt + status fields). For Codex it's the only path that
closes the lifecycle.
Tests (1081) + build pass. plugin/scripts/stop.mjs regenerated by tsdown.
Bug-fix patch focused on search recall correctness and plugin
compatibility. Pins iii-engine to v0.11.2 because v0.11.6 introduces
a new sandbox-everything-via-`iii worker add` model that agentmemory
hasn't been refactored for yet — pin lifts once that refactor lands.
Adds a hard guard against silent vector-index corruption, fixes BM25
indexing for memories saved via memory_save, and lands four Hermes
plugin fixes.
Per AGENTS.md release checklist:
- package.json version 0.9.4 -> 0.9.5
- src/version.ts VERSION constant
- src/types.ts ExportData version union
- src/functions/export-import.ts supportedVersions Set
- test/export-import.test.ts assertion
- plugin/.claude-plugin/plugin.json version
- CHANGELOG.md detailed entries with contributor shoutouts
Headlines (full detail in CHANGELOG):
Fixed:
- BM25 search now indexes memories saved via memory_save (#258, #257)
Thanks @Nizar-BenHamida for the precise repro.
- Embedding providers no longer silently corrupt the vector index when
an API returns wrong-dimension vectors (#248, #247, #256)
Thanks @AmmarSaleh50 for issue + fix + tests.
- Hermes handle_tool_call returns JSON strings, not raw dicts (#255, #254)
Thanks @KyoMio for the Anthropic-protocol repro.
- Hermes status reflects real service state on systemd installs (#253, #250)
Thanks @OptionalCoin for tracing it to env-source divergence.
- Hermes hooks accept passthrough kwargs (#252, #249)
Thanks @OptionalCoin again for the log analysis.
- agentmemory demo now seeds observations correctly (#251, #229)
Thanks @seishonagon for root-cause analysis.
- LLM compression / summarization timeouts increased (#213)
Thanks @xuli500177.
- Pi / OpenClaw / Hermes integration plugin fixes (#230)
Thanks @deepmroot.
Changed:
- iii-engine pinned to v0.11.2 across every install path (#260).
v0.11.6 introduces a new `iii worker add` sandbox model that
agentmemory still pre-dates; pin lifts when we refactor agentmemory
to register as a sandboxed worker. Override with
AGENTMEMORY_III_VERSION=<version> for users who've migrated manually.
- README documents iii worker add extension surface (#242).
- README iii Console install/launch commands corrected (#243).
Validated: 852/852 tests pass, npm run build clean.
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.
1. events.ts: Event triggers were calling api:: functions which
require ApiRequest shape and auth headers. Rewrote to call
core functions (kv.set, sdk.trigger) directly, bypassing auth.
2. All 5 hooks: Missing AGENTMEMORY_SECRET auth header. If secret
was set, every hook would get 401 from the API. Now all hooks
read AGENTMEMORY_SECRET and send Bearer token.
3. observe.ts: stripPrivateData on JSON string could break JSON
structure when replacement text differs in length. Added
try/catch fallback to string coercion.
4. post-tool-use.ts: truncate() for objects did
JSON.parse(str.slice(0, max-1) + '}') which produces invalid
JSON in nearly all cases. Changed to return truncated string.
5. compress.ts: LLM-returned importance was not clamped to 1-10
range. Added Math.max(1, Math.min(10, ...)) with NaN fallback.
6. compress.ts: LLM-returned observation type was not validated
against ObservationType union. Invalid types now fall back to
"other".
7. context.ts: Token estimate for observation blocks only counted
inner content, not the "## Session..." header. Fixed to estimate
the full block text.
8. viewer: WebSocket port now configurable via ?wsPort= query param
for non-default III_STREAMS_PORT configurations.
9. plugin/scripts: Rebuilt with auth header support matching the
updated hook source files.