From 380705db90e7d0ae11ee6356646e6020152b66de Mon Sep 17 00:00:00 2001 From: Hunter Casillas Date: Mon, 18 May 2026 19:59:15 -0500 Subject: [PATCH] fix(hooks): use SessionEnd not Stop for Claude Code transcript extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Claude Code adapter installs the `stop.py` extraction hook on the `Stop` event, which fires per assistant turn (once per response). The intent — per the README and stop.py docstring — is session-end extraction. The Gemini adapter at adapters/gemini.py:27 already uses `SessionEnd` correctly; the Claude adapter was missed. Per Claude Code docs (code.claude.com/docs/en/hooks): - `Stop` fires once per turn ("when Claude finishes responding") - `SessionEnd` fires once per session ("when a session terminates") Net effect of the bug: every assistant turn triggers an ingest spawn. The 20/hr budget + 2-process spawn cap prevent runaway, but produces ~1 ingest spawn every ~10s on chatty sessions — most then no-op via the size-dedup gate but still cost the python.exe spawn cycle. Fix is the trivial event-name swap on the Claude adapter only. Codex (adapters/codex.py:30) and Kimi (adapters/kimi.py:30) adapters intentionally untouched — they correctly use 'Stop' per their own CLI specs. Files changed: - truememory/ingest/cli.py:794 docstring narrative - truememory/ingest/cli.py:812 installer event key (the bug) - truememory/ingest/cli.py:1086 expected-events health check - truememory/ingest/CLAUDE_TEMPLATE.md user-facing narrative - docs/architecture.md - install.sh comment Co-Authored-By: claude-opus-4-7 --- docs/architecture.md | 2 +- install.sh | 2 +- truememory/ingest/CLAUDE_TEMPLATE.md | 2 +- truememory/ingest/cli.py | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index afa482f..f088f1f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -40,7 +40,7 @@ 3. **Snapshot** (pre-compact): The PreCompact hook (Claude Code, Kimi) saves a lightweight snapshot of the conversation before context compression. -4. **Extract** (session end): The Stop hook launches a background ingestion process that parses the transcript, runs the encoding gate on each fact, and stores high-quality memories. +4. **Extract** (session end): The SessionEnd hook launches a background ingestion process that parses the transcript, runs the encoding gate on each fact, and stores high-quality memories. (On Claude Code, the `SessionEnd` event fires only when the session actually terminates — `Stop` fires per-turn and is therefore the wrong trigger for transcript-end extraction.) ## Package Structure diff --git a/install.sh b/install.sh index 8a995f2..fcbc93c 100755 --- a/install.sh +++ b/install.sh @@ -12,7 +12,7 @@ # 4. Runs `truememory-mcp --setup` (code from PyPI) to auto-configure # Claude Code and/or Claude Desktop. Set TRUEMEMORY_SKIP_SETUP=1 to skip. # 5. Runs `truememory-ingest install` to wire up lifecycle hooks -# (SessionStart, Stop, UserPromptSubmit, PreCompact) and merge +# (SessionStart, SessionEnd, UserPromptSubmit, PreCompact) and merge # CLAUDE.md instructions so Claude uses TrueMemory proactively. # # Environment overrides: diff --git a/truememory/ingest/CLAUDE_TEMPLATE.md b/truememory/ingest/CLAUDE_TEMPLATE.md index 32407bf..4bdba7d 100644 --- a/truememory/ingest/CLAUDE_TEMPLATE.md +++ b/truememory/ingest/CLAUDE_TEMPLATE.md @@ -33,7 +33,7 @@ MEMORY.md may contain personal facts that were cached from earlier sessions. **D ## Background Processing - Memories are also extracted automatically from conversations via background processing. -- The Stop hook captures the full transcript and runs deep extraction after sessions end. +- The SessionEnd hook captures the full transcript and runs deep extraction when the session terminates. - You do NOT need to store everything manually — focus on in-conversation corrections and explicit preferences. - The background extractor handles: personal facts, preferences, decisions, temporal facts, and technical context. diff --git a/truememory/ingest/cli.py b/truememory/ingest/cli.py index 56d21fb..f886701 100644 --- a/truememory/ingest/cli.py +++ b/truememory/ingest/cli.py @@ -791,7 +791,7 @@ def _run_install(args): Hooks installed: - SessionStart: injects relevant memories as additionalContext - UserPromptSubmit: buffers user messages (kept for future use) - - Stop: triggers background fact extraction after each session + - SessionEnd: triggers background fact extraction when the session terminates - PreCompact: saves context snapshot before Claude compresses conversation Note: the event is named ``PreCompact`` in Claude Code's settings.json @@ -809,7 +809,7 @@ def _run_install(args): hook_files = { "SessionStart": hooks_dir / "session_start.py", "UserPromptSubmit": hooks_dir / "user_prompt_submit.py", - "Stop": hooks_dir / "stop.py", + "SessionEnd": hooks_dir / "stop.py", "PreCompact": hooks_dir / "compact.py", } missing = [name for name, path in hook_files.items() if not path.exists()] @@ -1083,7 +1083,7 @@ def _run_status(args): try: settings = json.loads(settings_path.read_text(encoding="utf-8")) hooks = settings.get("hooks", {}) - expected = ["SessionStart", "UserPromptSubmit", "Stop", "PreCompact"] + expected = ["SessionStart", "UserPromptSubmit", "SessionEnd", "PreCompact"] installed = [] missing = [] for event in expected: