Skip to content

transcript.js reads block.input on tool_result blocks, silently dropping all tool output from compaction #18

Description

@Shaderzx

Version: morph-compact@0.2.8 (Claude Code plugin, installed via claude plugin install morph-compact@morph)

Summary

hooks/lib/transcript.js reads block.input when extracting tool_result content blocks. Anthropic tool_result blocks carry their payload in block.content, not block.inputinput is a field on tool_use. The condition is therefore always false and every tool result in the transcript is silently dropped before the history is sent to the Compact API.

Net effect: Morph only ever receives assistant/user prose plus [tool: name] breadcrumbs. No file contents, no command output, no error messages, no search results. For a coding agent that is the majority of the useful context, and it's exactly the material that most benefits from verbatim compaction rather than lossy re-summarization.

Fails silently — no error, no warning. The only visible symptom is an implausibly small inputChars in the state file written by pre-compact.js.

Root cause

hooks/lib/transcript.js, in extractText():

      case "tool_result":
        if (typeof block.input === "string" && block.input) {   // <-- always undefined
          parts.push(`[result]: ${block.input}`);
        }
        break;

tool_result blocks in a Claude Code transcript have exactly these keys:

tool_result keys: ['content', 'tool_use_id', 'type']
has 'input': False

block.content is either a string or an array of content blocks ({type: "text", text: "..."}), depending on the tool.

Evidence

Measured by running the shipped parseTranscript() against a real ~/.claude/projects/<project>/<session>.jsonl, before vs. after fixing the field name:

BEFORE: messages=93   chars=35129
AFTER:  messages=137  chars=72302

44 messages consisted entirely of tool results and were dropped in full; total content roughly doubled. That transcript contained 40 tool_use blocks and 40 matching tool_result blocks — none of the results survived extraction.

A separate session recorded stats: {messageCount: 54, inputChars: 7952, ...} — about 147 chars per message, which is what tipped me off.

Repro

  1. Run a Claude Code session that makes several tool calls producing substantial output.
  2. Trigger /compact.
  3. Inspect the state file written by pre-compact.js ($TMPDIR/compact-hook/<session_id>) before session-start.js consumes it.
  4. stats.inputChars is far smaller than the real conversation; the resulting summary contains [tool: X] placeholders with no corresponding results.

Suggested patch

      case "tool_result": {
        // tool_result carries `content` (string | block[]), not `input`
        const c = block.content;
        if (typeof c === "string") {
          if (c) parts.push(`[result]: ${c}`);
        } else if (Array.isArray(c)) {
          for (const inner of c) {
            if (inner?.type === "text" && inner.text) {
              parts.push(`[result]: ${inner.text}`);
            }
          }
        }
        break;
      }

Verified locally: node --check passes and the measurement above is the result of this exact change.

Secondary note (not fixed here)

The tool_use case keeps only [tool: ${block.name}] and discards block.input, so the arguments are lost too — which file was read, which command ran, which pattern was searched. That may well be deliberate compression, but combined with the dropped results it means a compacted session retains no record of what the agent actually did, only that it did something. Worth a second look while the above is being fixed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions