Skip to content

Claude path: tool results never reach the app (schema drops result, mapper never sets it) #1823

Description

@ByebyDoggy

Summary

Tool results never reach the app on the Claude path. Every tool row renders the empty-output placeholder — Bash, Read, and MCP tools alike — because two independent defects stack:

  1. The wire-level zod schema does not declare result, and createEnvelope validates through sessionEnvelopeSchema.parse(...), which drops undeclared keys.
  2. The Claude mapper never reads block.content, so it has nothing to send in the first place.

Fixing either one alone changes nothing, which is why this looks like "the tool produced no output" rather than "the output was discarded".

Where it shows up

packages/happy-app/sources/components/CommandView.tsx:136 renders [Command completed with no output] whenever stdout/stderr/error are all falsy. stdout comes from tool.result (utils/toolResult.ts), which the reducer fills from the tool-call-end envelope (sync/reducer/reducer.ts:1069, message.tool.result = c.content).

Defect 1 — the schema strips the field

packages/happy-wire/src/sessionProtocol.ts:54:

export const sessionToolCallEndEventSchema = z.object({
  t: z.literal('tool-call-end'),
  call: z.string(),
});

createEnvelope (sessionProtocol.ts:163) runs the event through sessionEnvelopeSchema.parse(...). Zod drops keys the schema does not declare, so a result added at the call site is removed before the envelope is ever sent. Verified against the shipped zod 4.6.5:

input contains result/isError
parse output: {"t":"tool-call-end","call":"abc"}
result survived: false

Note the app side already expects the field — packages/happy-app/sources/sync/typesRaw.ts:66 declares result and isError as optionals. The two ends of the protocol disagree, which suggests the field was planned and only half-implemented.

Defect 2 — the mapper never populates it

packages/happy-cli/src/claude/utils/sessionProtocolMapper.ts:685:

envelopes.push(createEnvelope('agent', {
    t: 'tool-call-end',
    call: block.tool_use_id,
}, { turn: turnId, subagent }));

block.content (which holds the actual tool output for type === 'tool_result' blocks) is never read, and the branch continues immediately after.

Why the Codex path is not a precedent

Codex emits its output as a separate envelope rather than on tool-call-end:

if (output && output.trim().length > 0) {
  envelopes.push(createEnvelope('agent', { t: 'text', text: output, thinking: true }, ...))
}
envelopes.push(createEnvelope('agent', { t: 'tool-call-end', call: item.id }, ...))

Copying this into the Claude path does not help: components/MessageView.tsx:245 returns null for props.message.isThinking, so thinking: true text is hidden by design. That is presumably correct for reasoning chains, but it means the Codex workaround only appears to work.

Suggested fix

Widen the schema and populate the field together:

// sessionProtocol.ts
export const sessionToolCallEndEventSchema = z.object({
  t: z.literal('tool-call-end'),
  call: z.string(),
  result: z.string().optional(),
  isError: z.boolean().optional(),
});
// sessionProtocolMapper.ts — Claude tool_result branch
envelopes.push(createEnvelope('agent', {
    t: 'tool-call-end',
    call: block.tool_use_id,
    result: toolResultText(block.content),
    isError: block.is_error === true,
}, { turn: turnId, subagent }));

block.content is a union (plain string, array of text blocks, image blocks), so the extractor needs to flatten it and cap the size — a large result should not bloat every sync frame.

Environment

  • happy 1.2.4, wire package as bundled
  • Client: Android app
  • Session mode: remote, Claude CLI provider

Notes

I have a local patch implementing the above that makes tool output visible on the Claude path — happy to open a PR if that is wanted. Reported while investigating why MCP tool results appeared blank in the app.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    • Status
      Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions