From 25705f9a773ce8665ee36904ce6b2ee4ed8200b2 Mon Sep 17 00:00:00 2001 From: StayBlue Date: Wed, 25 Mar 2026 17:38:00 -0700 Subject: [PATCH] fix: use raw stdout for SessionStart context injection JSON additionalContext via hookSpecificOutput is ignored for the compact SessionStart source. Raw text printed to stdout is injected into Claude's context for all sources per the docs. Also adds the compact matcher to hooks.json so SessionStart only fires after compaction. Co-Authored-By: Claude Opus 4.6 (1M context) --- hooks/hooks.json | 1 + src/hook.ts | 54 +++++++++++++++++++++++++++--------------------- src/morph.ts | 6 +++--- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/hooks/hooks.json b/hooks/hooks.json index 4c912b7..eda5fcd 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -12,6 +12,7 @@ ], "SessionStart": [ { + "matcher": "compact", "hooks": [ { "type": "command", diff --git a/src/hook.ts b/src/hook.ts index 8bf7133..906c459 100644 --- a/src/hook.ts +++ b/src/hook.ts @@ -52,7 +52,9 @@ export async function hookPreCompact(): Promise { if (!input.transcript_path) throw new Error("no transcript_path in hook input"); - log(`PreCompact triggered: trigger=${input.trigger} session=${input.session_id}`); + log( + `PreCompact triggered: trigger=${input.trigger} session=${input.session_id}`, + ); if (!(await Bun.file(input.transcript_path).exists())) { throw new Error(`transcript not found: ${input.transcript_path}`); @@ -73,19 +75,29 @@ export async function hookPreCompact(): Promise { try { const messages = await parseTranscript(input.transcript_path); const inputChars = messages.reduce((n, m) => n + m.content.length, 0); - log(`PreCompact: parsed ${messages.length} messages (${inputChars} chars), calling Morph API...`); + log( + `PreCompact: parsed ${messages.length} messages (${inputChars} chars), calling Morph API...`, + ); const start = performance.now(); const summary = await compact(messages); const durationMs = Math.round(performance.now() - start); - const ratio = inputChars > 0 ? ((summary.length / inputChars) * 100).toFixed(1) : "N/A"; + const ratio = + inputChars > 0 ? ((summary.length / inputChars) * 100).toFixed(1) : "N/A"; - log(`PreCompact: compaction complete in ${durationMs}ms — ${inputChars} → ${summary.length} chars (${ratio}% ratio)`); + log( + `PreCompact: compaction complete in ${durationMs}ms — ${inputChars} → ${summary.length} chars (${ratio}% ratio)`, + ); const state: StateData = { summary, warn: input.trigger === "manual" && !input.custom_instructions, - stats: { messageCount: messages.length, inputChars, outputChars: summary.length, durationMs }, + stats: { + messageCount: messages.length, + inputChars, + outputChars: summary.length, + durationMs, + }, }; await Bun.write(sf, JSON.stringify(state)); @@ -100,17 +112,6 @@ export async function hookPreCompact(): Promise { } } -function emitContext(data: string): void { - console.log( - JSON.stringify({ - hookSpecificOutput: { - hookEventName: "SessionStart", - additionalContext: data, - }, - }), - ); -} - export async function hookSessionStart(): Promise { const input = await readStdin(); if (!input.session_id) throw new Error("no session_id in hook input"); @@ -125,13 +126,17 @@ export async function hookSessionStart(): Promise { } const state = (await file.json()) as StateData; - log(`SessionStart: state=${JSON.stringify({ error: state.error, warn: state.warn, summaryLen: state.summary?.length ?? 0, stats: state.stats })}`); + log( + `SessionStart: state=${JSON.stringify({ error: state.error, warn: state.warn, summaryLen: state.summary?.length ?? 0, stats: state.stats })}`, + ); if (state.error) { log(`SessionStart: injecting error — ${state.error}`); - emitContext( - "ERROR: Morph compaction failed: " + state.error + "\n" + - "Inform the user about this error. Context from the previous conversation was NOT preserved.", + process.stdout.write( + "ERROR: Morph compaction failed: " + + state.error + + "\n" + + "Inform the user about this error. Context from the previous conversation was NOT preserved.", ); await unlink(sf).catch(() => {}); return; @@ -154,12 +159,15 @@ export async function hookSessionStart(): Promise { if (state.stats) { const { messageCount, inputChars, outputChars, durationMs } = state.stats; - const ratio = inputChars > 0 ? ((outputChars / inputChars) * 100).toFixed(1) : "N/A"; - log(`SessionStart: injecting summary — ${messageCount} messages, ${inputChars} → ${outputChars} chars (${ratio}%), took ${durationMs}ms`); + const ratio = + inputChars > 0 ? ((outputChars / inputChars) * 100).toFixed(1) : "N/A"; + log( + `SessionStart: injecting summary — ${messageCount} messages, ${inputChars} → ${outputChars} chars (${ratio}%), took ${durationMs}ms`, + ); } else { log(`SessionStart: injecting summary (${data.length} chars)`); } - emitContext(data); + process.stdout.write(data); await unlink(sf).catch(() => {}); } diff --git a/src/morph.ts b/src/morph.ts index fd4ede2..d248391 100644 --- a/src/morph.ts +++ b/src/morph.ts @@ -21,14 +21,14 @@ function loadApiKey(): string { try { const text = readFileSync(ENV_FILE, "utf-8"); for (const line of text.split("\n")) { - const m = line.match(/^MORPH_API_KEY=(.+)$/); - if (m) return m[1].trim(); + const key = line.match(/^MORPH_API_KEY=(.+)$/)?.[1]; + if (key) return key.trim(); } } catch {} throw new Error( "Morph API key not found. Run /morph-compact:install to configure it, " + - "or set MORPH_API_KEY environment variable.", + "or set MORPH_API_KEY environment variable.", ); }