Skip to content

Claude progress updates written before a tool call never show #55298

Open

Description

@moritzWa

Description

Claude 5.5 models (and Fable 5+, Mythos 5.1+) send the short notes they write for the user between tool calls as "progress update" thinking blocks, not as text blocks. See https://platform.claude.com/docs/en/build-with-claude/thinking#progress-updates.

opencode never shows these notes, for two reasons:

  1. With adaptive thinking, Anthropic's default display is omitted, so the progress-update block comes back with empty text. Only the signature is left.
  2. Even when the block has text (display summarized), the TUI renders it as collapsed reasoning, not as a message.

So when the model writes "Here is the draft: ..." and then calls a tool, the user never sees the draft. The model then refers to "the draft above", which isn't on screen.

This is the same bug as #52735, which was closed when v1 was frozen. It still happens on dev (v2).

Steps to reproduce

  1. Use anthropic/claude-sonnet-5-5 (or claude-opus-5-5).
  2. Ask for something where the model writes a message for you and then runs a tool, e.g. "write a two-line poem, then run ls".
  3. The poem doesn't show. In the stored session, the assistant message contains a reasoning part with empty text whose signature decodes to a narration block, right before the tool call.

OpenCode version

dev

Operating System

macOS

Activity

  1. opencode-agent commented on Oct 14, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I reproduced both halves of this on 2.0.28 (the latest release), and both still happen on the latest dev (1c28c2fe). Neither check used real Anthropic traffic.

    Request side. I fed hand-written Anthropic responses through OpenCode's request and response handling:

    • With no variant selected, the request for claude-opus-5-5 / claude-sonnet-5-5 turns on adaptive thinking but sends no display. Anthropic then falls back to its default, omitted.
    • A response containing a progress-update block with only a signature (decoding to narration), followed by a tool call, is stored as a reasoning part with empty text and no text part. That is the shape you described.
    • Variants already ask for display: "summarized", so only the no-variant default is affected.

    TUI side. Even when the note has text, it is shown only as a collapsed + Thought line above the tool call. The closing reply's "the draft above" then points at nothing visible; the recording and screenshot below show this. The test harness's simulated model can't send Anthropic responses, so I sent the note as a plain reasoning block. OpenCode stores both the same way, and the TUI has no special handling for narration blocks.

    This is the same symptom as #52735, which was a v1 report. Your PR #55299 targets both halves.

    recording.mp4

    Screenshot

    Reproduction script (drive.ts)
    // Reproduces anomalyco/opencode#55298 (TUI side): a Claude progress update arrives as a reasoning part
    // (an Anthropic `thinking` block with a "narration" signature) right before a tool call. The TUI treats it
    // like any other reasoning, so the note written for the user is not shown as a message.
    // Drive's simulated model speaks OpenAI chat, so the progress update is sent as a reasoning delta here;
    // opencode stores both as the same `reasoning` part type and the TUI has no notion of narration.
    import { Effect } from "effect"
    import { Llm, OpenCodeDriver } from "opencode-drive"
    
    const NOTE = "Here is the draft poem: Roses in the terminal glow / bugs retreat where tests now grow."
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        project: { git: true, files: { "a.txt": "hi\n" } },
        config: { permission: { "*": "allow" } } as any,
        tui: { recording: true },
      },
      ({ ui, llm }) =>
        Effect.gen(function* () {
          yield* llm.queue(
            Llm.reasoning(NOTE),
            Llm.toolCall({ index: 0, id: "call_ls", name: "shell", input: { command: "ls", description: "List files" } }),
            Llm.finish("tool-calls"),
          )
          yield* llm.queue(Llm.text("Done. The poem is in the draft above."))
          yield* ui.submit("write a two-line poem, then run `ls`")
          yield* ui.waitFor("draft above", { timeout: 30000 } as any)
          yield* Effect.sleep("1500 millis")
          yield* ui.screenshot("after-turn")
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

  2. self-assigned this
    on Oct 14, 2026
  3. rekram1-node commented on Oct 14, 2026

    @rekram1-node
    Contributor

    Yeah recommend selecting a reasoning effort for now using /variants or /thinking

    We will make the unselected experience better soon prefer doing a larger cleanup across all the providers than just a hack here or there

  4. drsmile1001 commented on Oct 23, 2026

    @drsmile1001

    Same symptom here through a third-party provider. There is also one detail about this path that affects what #55299 can do.

    Setup: opencode 2.1.3 with @openchamber/opencode-claude 1.3.8, which wraps the Claude Code Agent SDK behind a local proxy and registers as an @opencode/ai/providers/openai-compatible provider (claude-code). Model claude-opus-5-5, variant medium.

    The stored assistant message for the affected step has no text part:

    [
      {
        "type": "reasoning",
        "text": "<text>",
        "native": { "reasoningField": "reasoning_content" }
      },
      {
        "type": "tool",
        "name": "question",
        "state": {
          "status": "error",
          "error": { "type": "aborted", "message": "Tool execution interrupted" }
        }
      }
    ]

    Unlike the case in the report above, the text is not empty here. This plugin sets thinking-display to summarized and showThinkingSummaries to true, so a block that would come back empty under omitted arrives with text. The problem is classification, not an empty block.

    The OpenAI-compatible path never reaches anthropic-messages.ts. The proxy maps thinking_delta to a reasoning_content delta, and opencode stores that as a reasoning part. No top-level signature is produced: anthropic-messages.ts and bedrock-converse.ts write one into provider metadata, and this route does not. Since #55299 reads that top-level field, the check will not apply here even after it lands.

    Two consecutive blocks arrive as one part. For the same turn, Claude Code's own session transcript recorded two successive thinking blocks (309 and 148 characters), while opencode stored a single reasoning part whose text equals the two concatenated (457 characters). Even a classifier that receives both blocks cannot say afterwards where one ended and the next began.

    So the gap is at the part level: AssistantContent has text, reasoning and tool, and none of them can carry "this came from a separate block written for the user". A provider cannot report that distinction even when it knows it.

    Two things would help, in either order:

    • Let a provider pass the classification through without interpreting it. If reasoning_details entries with a signature field are preserved in provider metadata, a wrapper could forward the value verbatim and opencode could decide what to do with it. openai-chat.ts already keeps every field on the reasoning.text variant, so the receiving side may be most of the way there.
    • Keep separate blocks separate. One part per block would let a later classifier, or a UI, treat the two differently.

    One unrelated question, in case you know: Anthropic documents a connector_text block described as carrying "narration text that some tool-using responses include between tool calls". I could not find it in the Messages API response schema, and neither 2.1.3 nor dev references it, so I may be reading a type that never reaches a client. If it does reach one, that would be the natural home for this text.

  5. moritzWa commented on Oct 28, 2026

    @moritzWa
    Author

    @rekram1-node, #55299 is the scoped fix for this: it keeps the narration blocks in the request and shows them in the transcript. I know you'd prefer a larger change for the unselected-effort case. Would you rather merge this as an interim fix, or should I fold it into that work?

    @drsmile1001, you're right that the OpenAI-compatible route never sets the top-level signature, so #55299 doesn't cover it. That route needs its own follow-up.

  6. drsmile1001 commented on Nov 9, 2026

    @drsmile1001

    Follow-up for the route I described above: @openchamber/opencode-claude has since been archived in favour of the built-in claude-agent provider, so I re-checked on 2.2.6 with claude-agent/opus, variant medium.

    Blocks are no longer merged. convert-claude-stream.ts uses the message ID plus the block index as the part ID, so each thinking block becomes its own reasoning part. Stored steps include the shape reasoning, reasoning, tool.

    The signature is dropped, so #55299 will not apply here either. Every thinking block is emitted as reasoning-start / reasoning-delta / reasoning-end, signature_delta is not handled, and the reasoning-end provider metadata carries only sessionID and messageID. On this route, the stored reasoning parts have exactly those two keys under native and no signature. query-options.ts sets display: "summarized", so these parts always have text and always render as a Thought block.

    Since the provider now lives in packages/core, there is no third-party layer in between: the stream converter sees signature_delta and could keep its value on the reasoning part's provider metadata, the way anthropic-messages.ts does. That would let #55299, or whatever replaces it in the larger cleanup, cover this route without a separate change.

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions