You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The plugin's design makes compaction fail closed: the # Compact Instructions override installed into ~/.claude/CLAUDE.md unconditionally suppresses Claude's native compaction summary, so the summary that survives compaction is always the placeholder string. Recovery of the actual context depends entirely on the SessionStart hook injecting the Morph summary. That means any failure of the Morph API call turns into real, unrecoverable context loss for the user — the native summary that would otherwise have existed was suppressed, and there is nothing to inject in its place.
This is easy to hit and I've hit it in the wild (details below). Three related problems, each with a concrete fix — happy to open a PR for all of them.
1. Any Morph API failure loses the conversation context
When compact() throws, hooks/pre-compact.js writes an error-only state (no summary), and hooks/session-start.js then injects:
ERROR: Morph compaction failed: … Context from the previous conversation was NOT preserved.
The user is informed, but the context is gone. PR #12 (retrying transient 429/5xx/network failures) helps, but doesn't cover non-transient failures: revoked/disabled key (401), extended outages, or requests that exceed the timeout on every attempt.
Deterministic repro (no network tricks needed):
echo'{"session_id":"t1","transcript_path":"/path/to/any/transcript.jsonl","trigger":"auto","custom_instructions":""}' \
| MORPH_API_KEY=sk-invalid node hooks/pre-compact.js
# [morph-compact] PreCompact: error — Compact API error 401: {"detail":"Invalid or disabled API key"}echo'{"session_id":"t1","source":"compact"}'| node hooks/session-start.js
# injects "Context from the previous conversation was NOT preserved."
Real-world instance from my machine: a compaction's Morph call hit the 60 s client timeout (The operation timed out. — the AbortSignal.timeout abort from CompactClient), leaving exactly this error state. (Observed while on 0.2.2, but the timeout value and the error/state handling are the same at HEAD.)
Proposed fix (fail soft): in the catch of hooks/pre-compact.js, store a raw tail of the parsed transcript (e.g. last ~16 KB of role-prefixed messages) in the state file as fallback; in hooks/session-start.js, when state.error is set, inject that tail as working context instead of only announcing the loss. Degrades "everything since the last compaction is gone" to "you have the recent tail verbatim". Patch:
// pre-compact.js — hoist `messages` out of the try, then in catch:conststate={summary: "",warn: false,error: e.message,ts: Date.now(),fallback: buildFallbackTail(messages)};
2. State files never expire → stale/false injections on --resume
hooks/session-start.js consumes a state file for its session_id regardless of source and regardless of age. If the restart that should consume the state never happens (compaction cancelled by the user while the hook/API call is pending, CLI killed, machine slept), the file stays in $TMPDIR/compact-hook/ indefinitely. Resuming that session later — days later — injects either a stale summary or, worse, a false "Context … was NOT preserved" alarm into a perfectly healthy session.
Not hypothetical: I found exactly such an orphan on my machine (written during the timeout incident above, still sitting there the next day; a claude --resume of that session would have fired the false alarm).
Proposed fix: write ts: Date.now() into the state in both success and error paths, and have session-start.js discard anything older than a few minutes (the legitimate consumer restarts within seconds of PreCompact):
3. Timeouts: hardcoded 60 s SDK timeout, no explicit hook timeout
hooks/lib/morph.js hardcodes COMPACT_TIMEOUT = 60000. Unlike MORPH_COMPACT_RATIO / MORPH_COMPACT_PRESERVE_RECENT it isn't env-configurable, and 60 s is a long time for the user to stare at a stalled compaction before landing in problem feat: rewrite #1.
Proposed fix:const COMPACT_TIMEOUT = parseInt(process.env.MORPH_COMPACT_TIMEOUT || "40000", 10); plus an explicit "timeout": 120 on the PreCompact hook entry. Keeping the SDK timeout well below the hook timeout also guarantees the catch (which writes the state file, and with fix #1 the fallback) always gets to run rather than the hook being cancelled from outside.
I'm running all three fixes locally (patched 0.2.8) — verified with an invalid-key run (fallback tail stored and injected, transcript untouched), a backdated state file (discarded), and a real API run (summary stored and injected). Glad to send a PR if the approach sounds right.
Environment: morph-compact 0.2.8, Claude Code 2.1.205, macOS (darwin 24.6), Node 26.
The plugin's design makes compaction fail closed: the
# Compact Instructionsoverride installed into~/.claude/CLAUDE.mdunconditionally suppresses Claude's native compaction summary, so the summary that survives compaction is always the placeholder string. Recovery of the actual context depends entirely on theSessionStarthook injecting the Morph summary. That means any failure of the Morph API call turns into real, unrecoverable context loss for the user — the native summary that would otherwise have existed was suppressed, and there is nothing to inject in its place.This is easy to hit and I've hit it in the wild (details below). Three related problems, each with a concrete fix — happy to open a PR for all of them.
1. Any Morph API failure loses the conversation context
When
compact()throws,hooks/pre-compact.jswrites an error-only state (no summary), andhooks/session-start.jsthen injects:The user is informed, but the context is gone. PR #12 (retrying transient 429/5xx/network failures) helps, but doesn't cover non-transient failures: revoked/disabled key (401), extended outages, or requests that exceed the timeout on every attempt.
Deterministic repro (no network tricks needed):
Real-world instance from my machine: a compaction's Morph call hit the 60 s client timeout (
The operation timed out.— theAbortSignal.timeoutabort fromCompactClient), leaving exactly this error state. (Observed while on 0.2.2, but the timeout value and the error/state handling are the same at HEAD.)Proposed fix (fail soft): in the
catchofhooks/pre-compact.js, store a raw tail of the parsed transcript (e.g. last ~16 KB ofrole-prefixed messages) in the state file asfallback; inhooks/session-start.js, whenstate.erroris set, inject that tail as working context instead of only announcing the loss. Degrades "everything since the last compaction is gone" to "you have the recent tail verbatim". Patch:2. State files never expire → stale/false injections on
--resumehooks/session-start.jsconsumes a state file for itssession_idregardless ofsourceand regardless of age. If the restart that should consume the state never happens (compaction cancelled by the user while the hook/API call is pending, CLI killed, machine slept), the file stays in$TMPDIR/compact-hook/indefinitely. Resuming that session later — days later — injects either a stale summary or, worse, a false "Context … was NOT preserved" alarm into a perfectly healthy session.Not hypothetical: I found exactly such an orphan on my machine (written during the timeout incident above, still sitting there the next day; a
claude --resumeof that session would have fired the false alarm).Proposed fix: write
ts: Date.now()into the state in both success and error paths, and havesession-start.jsdiscard anything older than a few minutes (the legitimate consumer restarts within seconds of PreCompact):3. Timeouts: hardcoded 60 s SDK timeout, no explicit hook timeout
hooks/lib/morph.jshardcodesCOMPACT_TIMEOUT = 60000. UnlikeMORPH_COMPACT_RATIO/MORPH_COMPACT_PRESERVE_RECENTit isn't env-configurable, and 60 s is a long time for the user to stare at a stalled compaction before landing in problem feat: rewrite #1.hooks/hooks.jsonset notimeout, so the platform default for command hooks applies (600 s per the current hooks docs). Combined withnpm installrunning inside the same hook command (SessionStart/PreCompact hooks run npm install on every invocation: 5-minute stall on claude --resume #14), a hung API call or cold install can stall compaction for minutes.Proposed fix:
const COMPACT_TIMEOUT = parseInt(process.env.MORPH_COMPACT_TIMEOUT || "40000", 10);plus an explicit"timeout": 120on the PreCompact hook entry. Keeping the SDK timeout well below the hook timeout also guarantees thecatch(which writes the state file, and with fix #1 the fallback) always gets to run rather than the hook being cancelled from outside.I'm running all three fixes locally (patched 0.2.8) — verified with an invalid-key run (fallback tail stored and injected, transcript untouched), a backdated state file (discarded), and a real API run (summary stored and injected). Glad to send a PR if the approach sounds right.