Skip to content

feat(cli): render claude CLI native thinking with /reasoning gating#99401

Merged
obviyus merged 1 commit into
openclaw:mainfrom
Marvinthebored:fix/claude-cli-thinking-stream
Jul 4, 2026
Merged

feat(cli): render claude CLI native thinking with /reasoning gating#99401
obviyus merged 1 commit into
openclaw:mainfrom
Marvinthebored:fix/claude-cli-thinking-stream

Conversation

@Marvinthebored

@Marvinthebored Marvinthebored commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Problem. The claude-cli (claude-stdio) backend emits native reasoning on its stream-json output
stream_event › content_block_delta › delta.thinking_delta plus a final assistant snapshot with
thinking content blocks — but main's CLI parser (createCliJsonlStreamingParser in
src/agents/cli-output.ts) has no thinking handling at all. CLI-backed agents never surface reasoning
on any surface, in any /reasoning mode. Related open feature requests: #68374, #97526.

Why now. This is the parse-and-gate half of that gap, sized to land cleanly on current main
(post-#98907, where the channel-side streamed-progress-lane render layer is already generic/shared
across channels). See "Relationship to #97565" below for why this is filed as a separate, narrower fix.

Outcome. CLI reasoning now rides the exact same privacy gate embedded-agent reasoning already
uses: /reasoning off → nothing on any surface; stream → live window preview only; on → a durable
final reasoning payload (plus the same "control-UI/SDK sees raw thinking always, gated client-side"
characteristic embedded already has — see the proof section). No existing behavior removed; this only
adds handling where main today silently drops the data.

Out of scope. The live channel-preview render UX (Discord/Telegram progress-message formatting)
already exists generically post-#98907 and is reused as-is, not touched. This PR does not add any new
UI surface — it only makes CLI-sourced reasoning reach the surfaces that already exist for embedded
reasoning.

Success criteria. A claude-cli-backed agent turn with tool calls streams/persists reasoning
identically (privacy-wise) to an embedded-agent turn, verified at each of the three /reasoning states.

Reviewer focus. (1) the per-Anthropic-message reset in cli-output.ts (a tool round-trip within one
CLI turn starts a fresh Anthropic message with content-block indices restarting at 0 — the tracker must
not let a prior message's index-0 thinking bleed into the new one); (2) the reuse (not reimplementation)
of the existing requiresReasoningProgressOptIn / reasoningPayloadsEnabled gates — no new gate
mechanism is introduced.

Maintainer-ready classification

  • Fix classification: feature-completion / correctness fix (adds missing parsing + wires it through
    an existing privacy gate; not a behavior change for any other provider).
  • Root cause: src/agents/cli-output.ts's CLI stream-json parser never implemented thinking-block
    handling, so stream:"thinking" was never emitted for CLI-backed turns, and the CLI dispatch layer
    (src/auto-reply/reply/agent-runner-cli-dispatch.ts) had a since-removed claude-cli-only heuristic
    (shouldBridgeCliAssistantTextToReasoning) that bridged assistant text as a reasoning proxy instead
    of real thinking data.
  • Architecture / source-of-truth check: reuses the identical mechanism embedded-agent reasoning uses
    end-to-end — same stream:"thinking" event shape ({text, delta, isReasoningSnapshot}), same
    onReasoningStream({text, requiresReasoningProgressOptIn: true}) call contract consumed by Discord
    (extensions/discord/src/monitor/message-handler.process.ts) and Telegram
    (extensions/telegram/src/bot-message-dispatch.ts), same durable-payload shape
    ({text, isReasoning: true}) consumed by the universal drop gate in
    src/auto-reply/reply/dispatch-from-config.ts (reply.isReasoning === true && !reasoningPayloadsEnabled
    → drop). No new gate, no new payload shape, no channel-specific code added.
  • Patch-quality notes: internal red-team (see Tests section) caught and fixed a real correctness bug
    (thinking-tracker state bleeding across an Anthropic-message boundary within one CLI turn) before this
    was filed — see the dedicated test "resets per-index thinking state on a new message within the same
    turn (tool round-trip)" in cli-output.test.ts.
  • Related-PR scan: see below.

Relationship to #97565

#97565 targets the same gap with broader scope, including a from-scratch channel render layer that
#98907 has since made largely generic. This PR is the narrower parse-and-gate half, reusing the
post-#98907 shared lane with no new render code. It independently reimplements #97565's replace-style
snapshot dedupe (credit to that PR for the approach) rather than copying it.

Linked context

Real behavior proof (required for external PRs)

Behavior addressed: CLI-backed (claude-stdio) agent turns now surface native reasoning through the
standard thinking stream and durable-reasoning payload, gated by /reasoning off/stream/on exactly like
embedded-agent reasoning.

Real environment tested: An isolated gateway (~/openclaw-p6-home, port :19791, channels off, own
OPENCLAW_HOME) running an authenticated claude-cli/claude-sonnet-4-6 agent, with a tee shim on the
real claude binary (v2.1.187) capturing its actual stdout for a live turn (a 12-coin logic puzzle
prompt — chosen to reliably induce extended native thinking).

Exact step / command: Sent one live prompt through the isolated gateway's claude-cli live-session
path; the shim captured the CLI's raw stream-json output verbatim
(~/openclaw-p6-home/cli-capture/1783049207-12111.stdout, 119 KB, 251 raw JSONL lines). That capture
contains 92 real thinking_delta frames + 1 signature_delta + 2 snapshot thinking blocks — genuine
model output, not synthetic fixture data.

Evidence (boundary-injection method — disclosed, see "what was not tested" below): Rather than
re-running a full gateway→Discord/Telegram round trip for the gating differential (channel bot auth +
live posting adds environment setup without adding proof strength once the gate code itself is read
directly — see next paragraph), the SAME 92 real captured frames were fed through the actual, unmodified
production parser (createCliJsonlStreamingParser) and the actual, unmodified dispatch bridge
(runCliAgentWithLifecycle / createReasoningTextBridge) via the project's existing vitest mocking
harness (same pattern as agent-runner-cli-dispatch.test.ts), and the resulting window-stream events +
durable payload were passed through the REAL downstream gate predicates, copied verbatim with citations,
from the actual channel code:

  • extensions/discord/src/monitor/message-handler.process.ts:631-632
    reasoningDurableEnabled = reasoningLevel === "on", reasoningWindowEnabled = reasoningLevel === "stream"
  • extensions/discord/src/monitor/message-handler.process.ts:1201 — window drop:
    payload?.requiresReasoningProgressOptIn === true && !reasoningWindowEnabled
  • src/auto-reply/reply/dispatch-from-config.ts:~3533 — durable drop:
    reply.isReasoning === true && !reasoningPayloadsEnabled

Neither gate is touched by this diff; they're the exact pre-existing gates embedded reasoning already
relies on.

Observed result (tri-state, real frames end-to-end):

rawCounts thinking_delta_frames=92 onThinkingDelta=92 onReasoningText=92 assistantChars=3700 reasoningChars=5637
state=off    window: 0 passed / 92 dropped | durable: DROPPED
state=stream window: 92 passed / 0 dropped | durable: DROPPED
state=on     window: 0 passed / 92 dropped | durable: PASSED (chars=5637)

This matches the intended parity table exactly: off = nothing on any gated surface; stream = live
window only; on = durable payload only (no live window in on mode, matching embedded's existing
durable=on, window=stream split).

Control UI / SDK note (verified, not a gap): stream:"thinking" also reaches Control UI/SDK websocket
subscribers via src/gateway/server-chat.ts, which relays the generic agent-event bus without its own
reasoningLevel check — this is true for embedded reasoning too (documented in
src/agents/embedded-agent-subscribe.ts: "the thinking stream always reaches the bus... display surfaces
... gate presentation on their side"). Control UI applies its own client-side gate
(ui/src/ui/views/chat.ts:2137: showReasoning = props.showThinking && reasoningLevel !== "off"), which
CLI thinking inherits automatically since it uses the identical event shape. Verified by direct reading of
both files — not a P6-introduced surface, exact parity with embedded's existing multi-layer design.

What was not tested (superseded — see Live transport proof below): a full live gateway conversation through an actual Discord/Telegram bot — now supplied. Original disclosure kept for review history: a full live gateway conversation through an actual Discord/Telegram bot with
screen-captured render output at each /reasoning state (the earlier, now-superseded emit-always
iteration of this branch did do a live gateway run — see the superseded commit's history — but the
gating differential proven above is at the code layer these channel renderers consume from, which is
where the actual privacy decision is made).

Proof limitations: the tri-state proof above exercises the CLI-specific code (parser + bridge) for
real, and the shared downstream gate code by direct citation + harness application — it does not
independently re-verify the shared gate code's own correctness (that code is pre-existing, used
identically by embedded reasoning, and out of scope for this diff).

Before evidence: on main (no stream:"thinking" handling in cli-output.ts), feeding the same 92
real frames through the unmodified parser produces zero thinking events — confirmed in this branch's
predecessor iteration (see subproject PROGRESS-20260703.md, "Step 3 — LIVE before/after proof").

Proof pack:

Live transport proof (Discord + Telegram, all three /reasoning states) — full live gateway runs
of this branch with real claude-cli/claude-sonnet-4-6 turns (fresh /new + explicit /model per
run, visible in-frame; full grid with per-cell notes in
this PR comment):

/reasoning Discord Telegram
stream (live 🧠 window → collapse, GIF) discord-stream.gif telegram-stream.gif
on (durable 🧠 persists) discord-on-settled.png telegram-on-settled.png
off (no reasoning rendered) discord-off-settled.png telegram-off-settled.png

Off-state turns are full-size claude-cli sessions in the gateway log (e.g. Telegram off: 23.0s, 75
raw stream-json lines — comparable to the on runs), i.e. thinking was generated upstream and
display-gated, not absent. This closes the earlier "what was not tested" item: the live
Discord/Telegram tri-state transport run is now supplied.

Tests and validation

  • export PATH="/Users/marvin/node24/bin:$PATH"; node scripts/run-vitest.mjs src/agents/cli-output.test.ts src/agents/cli-runner src/auto-reply/reply/agent-runner-cli-dispatch.test.ts src/auto-reply/reply/agent-runner-execution.test.ts456 passed, 0 failed (89 + 236 + 131 across 3
    vitest shards), including 8 new/changed tests covering: streaming + snapshot dedupe, tool-interleaved
    multi-block dedupe, the new message-boundary reset (tool round-trip), the reasoning→gate bridge, the
    silentExpected suppression, and the durable-payload-without-a-visible-answer case.
  • OPENCLAW_BUILD_ALL_NO_PNPM=1 node scripts/build-all.mjs → clean build, no errors.
  • Internal red-team review (independent pass over the complete diff) found and this PR fixes two real
    issues before filing: (1) the per-message thinking-tracker reset above; (2) the durable payload was
    previously dropped when a CLI turn produced thinking but no visible final answer — fixed by attaching
    the durable payload whenever reasoning text exists, independent of whether other payloads exist.

Update — Round 2: queued follow-up reasoning gate

The durable {isReasoning: true} payload synthesized for claude-cli was delivered on the queued follow-up path (resolveFollowupDeliveryPayloads) with no /reasoning gate at all, so a claude-cli queued follow-up with /reasoning off could leak internal reasoning to the channel as an ordinary visible message. resolveFollowupDeliveryPayloads now takes a reasoningPayloadsEnabled flag and drops isReasoning payloads unless it's true; followup-runner.ts threads the real opts.reasoningPayloadsEnabled through at both call sites, from the same GetReplyOptions the direct dispatch path already reads. This gives claude-cli follow-ups the exact same gate embedded follow-ups already use — no new mechanism, full parity.

Proof: 08-followup-proof.txt — a queued-follow-up cell added to the tri-state harness, exercising the real resolveFollowupDeliveryPayloads against the actual captured claude-cli reasoning payload (5637 chars): off/stream → resolver drops it, on → resolver keeps it eligible. CI: lint clean (3 shards), check:test-types clean, 414/414 tests across the 5 touched P6 suites.

Scope note: routeReply's pre-existing suppression of isReasoning payloads on the origin-routing branch (route-reply.ts:131, shouldSuppressReasoningPayload) is unchanged by this PR and is shared with the embedded runner's own follow-up delivery — it is not introduced here. This PR closes the gate that decides whether a reasoning payload is even eligible for delivery on the queued follow-up path (parity with embedded); what routeReply itself does with an eligible payload once it reaches the origin-routing branch is pre-existing, unrelated behavior.

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: L labels Jul 3, 2026
@clawsweeper

clawsweeper Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 3, 2026, 10:02 AM ET / 14:02 UTC.

Summary
The PR parses Claude CLI stream-json thinking blocks, emits CLI thinking events, bridges them into existing /reasoning presentation gates, synthesizes durable reasoning payloads, and gates queued follow-up reasoning payloads.

PR surface: Source +216, Tests +672. Total +888 across 12 files.

Reproducibility: yes. Current main has no CLI thinking parser/output path, and the PR supplies real Claude CLI stream-json frames plus live Discord/Telegram off/stream/on proof for the new behavior.

Review metrics: none identified.

Stored data model
Persistent data-model change detected: serialized state: src/agents/cli-output.test.ts. Confirm migration or upgrade compatibility proof before merge.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster ✨ media proof bonus
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • [P2] Maintainers should explicitly accept or reject the embedded-parity raw-thinking bus/archive model before merge.

Risk before merge

  • [P1] Reasoning text is privacy-sensitive; merging depends on maintainers accepting parity with the embedded model where raw thinking reaches the event bus/archive and presentation is gated downstream.
  • [P1] The PR makes CLI durable reasoning payloads eligible for channel delivery, but the origin-route routeReply path still suppresses eligible isReasoning payloads on current main, so routed durable reasoning remains separate scope.
  • [P1] There is active overlap with the broader live claude-cli reasoning PR and the still-open HTTP reasoning issue, so maintainers should decide this narrow parser-and-gate branch before reconciling the adjacent work.

Maintainer options:

  1. Land Narrow Scope With Explicit Risk Acceptance (recommended)
    Accept this branch as the parser and existing-gate bridge, with routeReply durable-routed delivery and HTTP reasoning left to their own follow-ups.
  2. Require Routed Durable Reasoning Before Merge
    Ask the branch to also make eligible origin-routed durable reasoning survive routeReply, if maintainers want the /reasoning on path fully end-to-end before this lands.
  3. Defer To The Broader Branch
    Pause or close this branch only if maintainers want the broader live claude-cli reasoning/progress PR to own parser, bridge, and channel-preview behavior together.

Next step before merge

  • [P2] No narrow automated repair is indicated; maintainers need to decide whether to land the privacy/message-delivery scope as written or require the routeReply/broader-preview work first.

Security
Cleared: No supply-chain, dependency, credential, permission, or sandboxing regression was found; the privacy-sensitive reasoning exposure is intentional scope and remains a merge risk for maintainers to accept.

Review details

Best possible solution:

Land this narrow parser-and-existing-gates implementation if maintainers accept the raw-thinking bus/archive parity model, while keeping HTTP reasoning and routed durable follow-up delivery as separate tracked scopes.

Do we have a high-confidence way to reproduce the issue?

Yes. Current main has no CLI thinking parser/output path, and the PR supplies real Claude CLI stream-json frames plus live Discord/Telegram off/stream/on proof for the new behavior.

Is this the best way to solve the issue?

Yes for this branch scope. Reusing stream: "thinking", requiresReasoningProgressOptIn, and existing durable isReasoning gates is the narrow maintainable path; HTTP and origin-routed durable delivery should stay separate unless maintainers expand scope.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 8c915f068525.

Label changes

Label changes:

  • add proof: 🎥 video: Contributor real behavior proof includes video or recording evidence. The PR includes inspected live Discord and Telegram off/on screenshots plus valid stream GIF assets for the changed visible behavior; prepared Mantis MP4 preprocessing failed because ffprobe was unavailable, but the image proof is sufficient.

Label justifications:

  • P1: This PR affects real-time agent/channel reasoning delivery and the privacy boundary for model thinking text.
  • merge-risk: 🚨 message-delivery: The diff changes which CLI-originated reasoning payloads are eligible for streamed and durable channel delivery.
  • merge-risk: 🚨 security-boundary: The diff intentionally exposes raw provider thinking to the event bus/archive and relies on existing presentation gates to prevent unauthorized display.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (recording): The PR includes inspected live Discord and Telegram off/on screenshots plus valid stream GIF assets for the changed visible behavior; prepared Mantis MP4 preprocessing failed because ffprobe was unavailable, but the image proof is sufficient.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR includes inspected live Discord and Telegram off/on screenshots plus valid stream GIF assets for the changed visible behavior; prepared Mantis MP4 preprocessing failed because ffprobe was unavailable, but the image proof is sufficient.
  • proof: 🎥 video: Contributor real behavior proof includes video or recording evidence. The PR includes inspected live Discord and Telegram off/on screenshots plus valid stream GIF assets for the changed visible behavior; prepared Mantis MP4 preprocessing failed because ffprobe was unavailable, but the image proof is sufficient.
  • mantis: telegram-visible-proof: Mantis should capture Telegram visible proof. The PR changes Telegram-visible reasoning output and live progress behavior that can be demonstrated in a short Telegram Desktop proof.
Evidence reviewed

PR surface:

Source +216, Tests +672. Total +888 across 12 files.

View PR surface stats
Area Files Added Removed Net
Source 7 236 20 +216
Tests 5 682 10 +672
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 12 918 30 +888

What I checked:

  • Repository policy read: Read the full root policy plus the relevant scoped agents guide; the review applied the requirements for whole-surface PR review, reasoning/privacy risk, and source/history/proof checks. (AGENTS.md:1, 8c915f068525)
  • Current main lacks CLI thinking output: Current main's createCliJsonlStreamingParser has assistant/tool/commentary callbacks but no thinking callback, so Claude CLI thinking_delta frames have no parser output path. (src/agents/cli-output.ts:852, 8c915f068525)
  • PR parser implementation: The PR head adds a per-message/per-content-block thinking tracker, skips signature/redacted thinking, emits onThinkingDelta, and wires it into the parser loop before assistant delta handling. (src/agents/cli-output.ts:788, ab98e51815b6)
  • PR runtime wiring: The PR head forwards thinking deltas through one-shot and live-session CLI execution and emits stream: "thinking" events without adding a channel-specific path. (src/agents/cli-runner/execute.ts:1021, ab98e51815b6)
  • Existing presentation gates: Discord and dispatch-from-config already gate live and durable reasoning separately with requiresReasoningProgressOptIn and reasoningPayloadsEnabled, and Telegram strips isReasoning only when durable reasoning is enabled. (src/auto-reply/reply/dispatch-from-config.ts:2057, 8c915f068525)
  • Embedded parity checked: Embedded reasoning already emits raw stream: "thinking" to the bus/archive while using downstream presentation gates, and durable reasoning payloads are produced only for reasoningLevel === "on". (src/agents/embedded-agent-subscribe.ts:1185, 8c915f068525)

Likely related people:

  • obviyus: Recent commits on src/agents/cli-output.ts and CLI dispatch/commentary classification are directly adjacent to the parser and bridge behavior this PR changes. (role: recent area contributor; confidence: high; commits: d18817019ff9, 0c4fc0a2e3b3, 817a0910f35e; files: src/agents/cli-output.ts, src/auto-reply/reply/agent-runner-cli-dispatch.ts)
  • vincentkoc: Recent commits in CLI runner dispatch and reasoning/fast delivery semantics touch the same agent-runner and auto-reply delivery boundary. (role: adjacent runtime contributor; confidence: medium; commits: 9e8ab083dd6b, 2b05bd7b0d9a, 756e9d16a0a6; files: src/auto-reply/reply/agent-runner-cli-dispatch.ts, src/auto-reply/reply/dispatch-from-config.ts)
  • fuller-stack-dev: Authored the recent Telegram durable reasoning gate that this PR relies on for channel-side durable reasoning delivery. (role: adjacent reasoning delivery contributor; confidence: medium; commits: 455f813d6ee6; files: extensions/telegram/src/bot-message-dispatch.ts, src/auto-reply/reply/dispatch-from-config.ts)
  • Marvinthebored: Beyond authoring this PR, this user has merged adjacent reasoning/commentary channel work used by the branch, including the Telegram progress-lane PR and Discord pre-tool commentary commit. (role: recent adjacent contributor; confidence: medium; commits: b3b51b0c9185, dae86e6bec01; files: extensions/discord/src/monitor/message-handler.process.ts, extensions/telegram/src/bot-message-dispatch.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. mantis: telegram-visible-proof Mantis should capture Telegram visible proof. P1 High-priority user-facing bug, regression, or broken workflow. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. labels Jul 3, 2026
@clawsweeper
clawsweeper Bot temporarily deployed to qa-live-shared July 3, 2026 07:03 Inactive
@Marvinthebored
Marvinthebored force-pushed the fix/claude-cli-thinking-stream branch from 054e155 to 1d7a19c Compare July 3, 2026 07:06
@openclaw-mantis

Copy link
Copy Markdown
Contributor

Mantis Telegram Desktop Proof

Summary: Mantis captured native Telegram Desktop before/after GIF evidence with Convex-leased Telegram credentials.

Main screenshot This PR screenshot
Baseline native Telegram Desktop screenshot Candidate native Telegram Desktop screenshot
Main This PR
Baseline native Telegram Desktop proof GIF Candidate native Telegram Desktop proof GIF

Motion-trimmed clips:

Raw QA files: https://artifacts.openclaw.ai/mantis/telegram-desktop/pr-99401/run-28644242516-1/index.json

The claude-cli (claude-stdio) live-session backend emits native
reasoning on stream-json (`thinking_delta` + a snapshot `thinking`
block), but the CLI parser had no thinking handling at all, so it was
silently dropped on every surface in every /reasoning mode.

- cli-output.ts: parse thinking_delta/snapshot into stream:"thinking"
  with replace-style per-content-block-index dedupe
  (assembleThinkingTextByIndex), robust to tool-interleaved multi-block
  turns (snapshot replaces per-index rather than assuming a streamed
  prefix). signature_delta/redacted_thinking stay skipped.
- agent-runner-cli-dispatch.ts: bridge stream:"thinking" into
  onReasoningStream via a new createReasoningTextBridge (mirrors the
  existing assistant-text bridge), replacing the old
  assistant-text-as-reasoning heuristic. Track the final reasoning
  snapshot and, when present, prepend a durable
  `{text, isReasoning: true}` payload before the answer — mirroring
  the embedded-runner durable reasoning payload
  (embedded-agent-runner/run/payloads.ts).
- agent-runner-execution.ts: thread isReasoningSnapshot through to
  onReasoningStream, still passing requiresReasoningProgressOptIn:true
  so CLI reasoning rides the exact same channel gates as embedded
  reasoning (Discord/Telegram): /reasoning off -> nothing on any
  surface, stream -> window preview only, on -> durable payload too.
  The bridge is suppressed under the same conditions as the assistant
  bridge (e.g. silentExpected follow-ups), so no durable payload is
  synthesized for turns that are expected to stay silent.

Supersedes this branch's prior commit (a straight port of
af7eb48e05's emit-always model, no gating and no durable mode) with
the above gated/durable design per product decision.

Also, in cli-output.ts: reset the per-content-block-index thinking
tracker on every new Anthropic message (message_start, or a fresh
top-level "assistant" snapshot's message.id) instead of leaving it
scoped to the whole CLI turn. Content-block indices restart at 0 per
Anthropic message, so a tool round-trip within one turn (message A:
thinking + tool_use; tool result fed back; message B: a fresh
message_start) was letting message A's stale index-0 thinking text
bleed into message B's index-0 delta.

And in agent-runner-cli-dispatch.ts: attach the durable reasoning
payload whenever any thinking was bridged, not only when the CLI also
produced a visible final answer, so a thinking-only turn (no visible
reply) doesn't silently drop its reasoning.

Round 2: the durable `{isReasoning: true}` payload synthesized above
was also reaching the QUEUED FOLLOW-UP delivery path
(followup-runner.ts -> resolveFollowupDeliveryPayloads) with no gate
at all, so a claude-cli queued follow-up with /reasoning off would
leak internal reasoning to the channel as an ordinary visible message.
resolveFollowupDeliveryPayloads (followup-delivery.ts) now takes a
reasoningPayloadsEnabled flag and drops isReasoning payloads unless
it is true; followup-runner.ts threads opts?.reasoningPayloadsEnabled
through at both call sites, from the same GetReplyOptions the direct
path already reads (dispatch-from-config.ts:2054). This makes the
queued-follow-up path apply the identical isReasoning/
reasoningPayloadsEnabled gate that direct dispatch already enforces
(dispatch-from-config.ts:3347, :3537) — no new mechanism, CLI
follow-ups now go through the exact same gate embedded follow-ups do.

Scope note: routeReply's own unconditional suppression of isReasoning
payloads on the origin-routing branch (route-reply.ts:131,
shouldSuppressReasoningPayload) is pre-existing, shared with the
embedded runner, and unchanged by this commit — that gate governs
whether a kept-and-routed reasoning payload is actually rendered on a
cross-channel queued follow-up, and is out of scope here. This commit
closes the leak (off -> never eligible on any path); it does not
change what happens to an eligible payload once it reaches routeReply.

Co-Authored-By: Claude <[email protected]>
@Marvinthebored
Marvinthebored force-pushed the fix/claude-cli-thinking-stream branch from 1d7a19c to ab98e51 Compare July 3, 2026 08:50
@clawsweeper clawsweeper Bot removed proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. mantis: telegram-visible-proof Mantis should capture Telegram visible proof. labels Jul 3, 2026
@Marvinthebored

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. mantis: telegram-visible-proof Mantis should capture Telegram visible proof. labels Jul 3, 2026
@Marvinthebored

Marvinthebored commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

Live transport proof — Discord + Telegram, /reasoning off/stream/on

Follow-up to the review's rank-up note ("one full live Discord or Telegram off/stream/on transport
run"): here are both, captured on a live gateway running this branch (ab98e51815), real
claude-cli/claude-sonnet-4-6 turns, fresh session (/new) + explicit /model before every run
(visible in-frame), same prompt each cell: "Think it through carefully: is HEARTBEAT.md or TOOLS.md
longer? Check the line counts with your tools and tell me which one wins."

/reasoning Discord Telegram
stream discord stream telegram stream
on discord on telegram on
off discord off telegram off

What each cell shows

  • stream (GIFs): the live progress window with CLI thinking streaming in real time — gerund
    status line, then 🧠 reasoning text updating mid-generation, then the window collapsing to the
    summary line (Discord: 🧠 1 thought · 💬 1 note · 🛠️ 1 tool call · 10s) with the final answer.
  • on: a durable 🧠 reasoning message that persists after the answer (Discord blockquote /
    Telegram 🧠-prefixed message) — the same durable placement embedded reasoning already uses on
    these channels.
  • off: same prompt, same model — no reasoning anywhere on the surface; only the final
    answer. Gateway log for the off turns shows full-size claude-cli sessions (e.g. Telegram off:
    23.0s, 75 raw stream-json lines — comparable to the on runs), i.e. thinking was generated
    upstream and display-gated, not absent.

Additional angles in the proof branch (claude-cli-thinking/visual/): mid-generation + settled
captures per cell, including telegram-on.gif
which catches the in-flight window (💬 commentary + 🛠️ tool lines) and the durable 🧠 landing below it.

@obviyus obviyus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed against current main with the full changed surface, siblings, and gates read:

  • Bug real: main's createCliJsonlStreamingParser has no thinking handling, and CLI "reasoning" today is the assistant-text bridge heuristic (shouldBridgeCliAssistantTextToReasoning) — a proxy, not data.
  • Root-cause fix: parses the real thinking_delta stream with per-index dedup and per-message-id tracker reset (the tool-round-trip index-restart hazard is handled and regression-tested), deletes the heuristic rather than keeping a second path, and routes through the existing gates: requiresReasoningProgressOptIn semantics match the embedded sibling (embedded-agent-subscribe.ts), durable drop rides the pre-existing reasoningPayloadsEnabled predicate; the followup-delivery addition extends that same predicate to a path that previously had no isReasoning handling at all.
  • Verified locally: cli-output + agent-runner-cli-dispatch suites green in a PR worktree at head ab98e51815b.
  • CI: the single red lane ("Run agentic native Telegram proof") is a capture-environment limitation, not a finding — its own manifest reports baseline/candidate both skipped ("proof environment cannot drive a claude-cli reasoning turn in Telegram Desktop"). Same harness signature as #99722's false-fail earlier today.
  • Non-blocking follow-up: the reasoningPayloadsEnabled drop predicate now lives in two delivery owners; a shared helper would prevent drift.

Approving and landing. Thanks @Marvinthebored — clean seam choice and the red-team test on the message-boundary reset is exactly the right paranoia.

@obviyus
obviyus merged commit 6445a06 into openclaw:main Jul 4, 2026
143 of 154 checks passed
shadow-enthusiast added a commit to shadow-enthusiast/openclaw that referenced this pull request Jul 5, 2026
…rser

Layers our progress-preview UX on top of PR openclaw#99401's native claude-cli
thinking parser (cherry-picked onto v2026.6.11), replacing our earlier
6.10-lineage parser port (6868ad2) whose parser logic openclaw#99401 now supplies
natively. Only the render-side deltas remain:

- progress-draft-compositor: guard the first delivered frame so a
  tool-progress preview never emits a bare label before the first tool
  line exists (silent-window arming); freeze on final-reply start.
- telegram bot-message-dispatch: separator-based heading extraction so a
  labelless draft no longer duplicates the first preview line.
- telegram draft-stream / streaming / typing-mode / get-reply-options:
  keep the claude-cli preview live across inter-tool commentary and
  silent tool windows, keyed by toolCallId for in-place refresh.

Parser + /reasoning gating come entirely from openclaw#99401; this commit is the
render UX only.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling mantis: telegram-visible-proof Mantis should capture Telegram visible proof. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. P1 High-priority user-facing bug, regression, or broken workflow. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: L status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants