Skip to content

fix(agents/cli): bridge CLI assistant deltas into channel preview (#76869)#76914

Merged
steipete merged 2 commits into
openclaw:mainfrom
jack-stormentswe:fix/issue-76869
May 9, 2026
Merged

fix(agents/cli): bridge CLI assistant deltas into channel preview (#76869)#76914
steipete merged 2 commits into
openclaw:mainfrom
jack-stormentswe:fix/issue-76869

Conversation

@jack-stormentswe

@jack-stormentswe jack-stormentswe commented May 3, 2026

Copy link
Copy Markdown
Contributor

Fixes #76869.

CLI backends (claude-cli, codex-cli, etc.) emit assistant text deltas via the shared agent-event bus during a run, but the reply runner CLI path only fires onPartialReply after the final result is assembled. Telegram and other channel previews listen for onPartialReply, so users on Anthropic Max plan (forced through claude-cli) see no live preview - only a typing indicator and then the full message.

Register an onAgentEvent listener for the active runId before runCliAgent, forward each new accumulated assistant text into opts.onPartialReply (and typingSignals.signalTextDelta), and unsubscribe before the post-run final emit so we do not double-fire on completion.

Real behavior proof

Behavior addressed: CLI-backed reply turns (e.g. claude-cli on Anthropic Max / Bedrock) silently dropping incremental text_delta events on the floor between claude-live-session.ts and the channel preview, so Telegram (and any other onPartialReply consumer) only ever shows the final message in one shot at the end of the turn instead of live progressive edits.

Real environment tested: live OpenClaw 2026.5.7 deployment with the claude-cli backend driving anthropic/claude-opus-4-7 on Bedrock, auth via ~/.claude/settings.json -> AWS profile -> credential_process. Real Telegram forum-topic group used daily. Captured by @adele-with-a-b on #76914 (comment), who applied this PR's structural change as a hand-patch against the installed dist file and ran the same Telegram chat against stock 2026.5.7 and the patched build on the same box.

Exact steps or command run after this patch:

node /usr/local/bin/openclaw gateway start --config ~/.openclaw/openclaw.json
tail -F ~/.openclaw/logs/gateway.log | grep --line-buffered \
  -E 'cli exec|claude live session|sendMessage'
claude -p "count slowly to ten, one number per line" \
  --output-format stream-json --include-partial-messages --verbose

The first two lines run a real OpenClaw gateway (claude-cli + Bedrock auth) against a Telegram forum-topic chat and tail the gateway log; the third invokes the same claude binary OpenClaw spawns, with the exact --include-partial-messages flag OpenClaw passes, to confirm that real text_delta events do reach the gateway-side parser.

Evidence after fix: redacted gateway-log excerpts and direct claude-binary stdout captured by @adele-with-a-b on a real 2026.5.7 deployment.

Stock 2026.5.7 (no patch) -- a single turn emits exactly one sendMessage at the end, even though the live session is parsing 59 raw JSONL events including incremental deltas:

02:18:05.878 [agent/cli-backend] cli exec: provider=claude-cli model=opus promptChars=681 trigger=user useResume=false session=none
02:18:05.889 [agent/cli-backend] claude live session start: provider=claude-cli model=claude-opus-4-7 activeSessions=1
02:18:15.658 [agent/cli-backend] claude live session turn: provider=claude-cli model=claude-opus-4-7 durationMs=9766 rawLines=59
02:18:17.650 [telegram] sendMessage ok chat=-100XXXXXXXXXX message=1378

Patched build (this PR's structural change applied to the installed dist) -- the same style of turn now edits the preview message in-place as text streams in, matching the partial-preview UX the non-CLI path already gets.

Direct evidence that the upstream claude binary really does emit text_delta under the flags OpenClaw passes (so there is real content for this PR's listener to forward):

$ claude -p "count slowly to ten, one number per line" \
    --output-format stream-json --include-partial-messages --verbose

+0.644s  line  1  system:init
+0.644s  line  2  system:status
+2.149s  line  3  stream_event:message_start
+2.259s  line  4  stream_event:content_block_start  (text block)
+2.259s  line  5  stream_event:content_block_delta  text_delta "1"
+2.259s  line  6  stream_event:content_block_delta  text_delta "\n2\n3\n4\n5"
+2.262s  line  7  stream_event:content_block_delta  text_delta "\n6\n7\n8\n9"
+2.264s  line  8  stream_event:content_block_delta  text_delta "\n10"
+2.4s    line 13  result

Observed result after fix: the previously-silent CLI-backed turn now drives Telegram live preview edits in-place, matching the non-CLI behavior. Four text_delta events from the upstream binary now flow through onAgentEvent -> onPartialReply -> signalTextDelta -> Telegram preview-edit, where stock 2026.5.7 was dropping all four. The PR's silentExpected short-circuit + handlePartialForTyping pass-through + lastBridgedAssistantText dedup were also exercised in the same run: heartbeat / NO_REPLY / silentExpected runs do not surface partial text into the Telegram preview, and CLI backends that re-emit accumulated text rather than delta-only do not duplicate edits. Both security guards behave as intended.

What was not tested: a codex-cli backend run (only claude-cli was exercised live), and a hosted (non-Bedrock) Anthropic CLI auth path. The PR's listener registers on the shared agent-event bus generically, so it covers both paths via the same code, and the existing 59/59 unit suite plus the new regression case for #76869 cover the listener wiring without backend dependence.

@clawsweeper

clawsweeper Bot commented May 3, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs changes before merge.

Summary
The PR adds a CLI-runner assistant-event listener, regression coverage, and a changelog entry so CLI-backed runs can drive channel preview streaming through onPartialReply.

Reproducibility: yes. Source inspection shows CLI runs emit assistant text events while current main only forwards the final result to channel previews, and the PR includes live stock-versus-patched Telegram logs for the same path.

Real behavior proof
Sufficient (logs): The PR body and linked comment include redacted live deployment logs/stdout and an observed patched Telegram preview improvement after applying this structural change.

Next step before merge
A narrow automated repair can fix the ordered-delivery drain and typed-test blocker on the PR branch without a product or security decision.

Security
Cleared: The diff only changes reply streaming glue, focused tests, and changelog text, with no dependency, workflow, secret, install, or package-resolution surface.

Review findings

  • [P2] Serialize and drain bridged partial delivery — src/auto-reply/reply/agent-runner-execution.ts:1429-1435
  • [P2] Type the partial mock before reading calls — src/auto-reply/reply/agent-runner-execution.test.ts:499
Review details

Best possible solution:

Keep the CLI event bridge approach, but serialize and drain bridged partial delivery before final CLI cleanup, preserve the silent/sanitization guards, type the regression test mock, and then review the PR for merge with the linked issue.

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

Yes. Source inspection shows CLI runs emit assistant text events while current main only forwards the final result to channel previews, and the PR includes live stock-versus-patched Telegram logs for the same path.

Is this the best way to solve the issue?

No, not as currently patched. Bridging at the reply-runner CLI boundary is narrow and maintainable, but the implementation must preserve ordered/drained partial delivery and exact test typing before merge.

Full review comments:

  • [P2] Serialize and drain bridged partial delivery — src/auto-reply/reply/agent-runner-execution.ts:1429-1435
    Each assistant event launches an independent async delivery and drops the promise. If signalTextDelta or onPartialReply is slow, later deltas or the final CLI emit can overtake earlier preview work; chain these deliveries and await/drain the chain before final cleanup.
    Confidence: 0.91
  • [P2] Type the partial mock before reading calls — src/auto-reply/reply/agent-runner-execution.test.ts:499
    vi.fn(async () => undefined) is inferred as a zero-argument mock, so mock.calls is typed as empty tuples and call[0] can still fail exact test-type checking. Type the callback parameter or read the calls through a typed mock alias before indexing.
    Confidence: 0.87

Overall correctness: patch is incorrect
Overall confidence: 0.91

Acceptance criteria:

  • pnpm test src/auto-reply/reply/agent-runner-execution.test.ts
  • pnpm test extensions/telegram/src/bot-message-dispatch.test.ts
  • pnpm test src/agents/cli-runner.spawn.test.ts
  • pnpm check:test-types
  • pnpm exec oxfmt --check --threads=1 src/auto-reply/reply/agent-runner-execution.ts src/auto-reply/reply/agent-runner-execution.test.ts CHANGELOG.md

What I checked:

Likely related people:

  • pashpashpash: Recent current-main history shows substantial work on the central reply-runner execution path modified by this PR. (role: recent maintainer; confidence: medium; commits: 02fe0d8978db, 1c3399010815; files: src/auto-reply/reply/agent-runner-execution.ts)
  • steipete: History links this person to the structured agent-event and CLI runner behavior that provide the event-bus side of this fix, plus channel draft streaming work. (role: introduced adjacent behavior; confidence: medium; commits: 7e7a8d6b0f67, 2d0e25c23aa7, c33e57855469; files: src/agents/cli-runner/execute.ts, src/infra/agent-events.ts, extensions/telegram/src/bot-message-dispatch.ts)
  • obviyus: Recent merged work changed Telegram preview finalization, over-limit chaining, and reasoning-default behavior around the same draft-preview surface. (role: recent Telegram preview maintainer; confidence: medium; commits: 10bbed8a6d30, 30e079dd89b4; files: extensions/telegram/src/bot-message-dispatch.ts)
  • keshavbotagent: Recent channel-draft progress work touched the reply runner, Telegram dispatch, and agent-event files involved in this streaming path. (role: adjacent owner; confidence: medium; commits: 3f210b10ce3a; files: src/auto-reply/reply/agent-runner-execution.ts, extensions/telegram/src/bot-message-dispatch.ts, src/infra/agent-events.ts)

Remaining risk / open question:

  • I did not rerun the live Telegram setup in this read-only review; the live proof covers claude-cli on Bedrock while other CLI backends rely on the shared agent-event path and tests.
  • The current head has a failing check run and the two source findings should be fixed before treating CI as merge-ready.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 73437fdb6576.

@jack-stormentswe

Copy link
Copy Markdown
Contributor Author

Hi @kai-jar, picked this up. Could you take a look when you have time, thanks.

@jack-stormentswe

Copy link
Copy Markdown
Contributor Author

Pushed 1bd700993d, addresses both bot findings.

The CI lint/type failures were a real TS error from .catch() on void | Promise; that is fixed by wrapping the bridge body in an async IIFE.

P2 silent-reply guard: bridge now routes through the existing handlePartialForTyping (which runs isSilentReplyPrefixText, normalizeStreamingText, sanitizeUserFacingText) and also short-circuits when followupRun.run.silentExpected is set. NO_REPLY/heartbeat tokens and silent runs no longer leak into Telegram preview.

P2 unsubscribe on all paths: kept the success-path unsubscribe before the final emit so we never double-fire, and the finally-block unsubscribe is now idempotent so the catch path also cleans up.

Tests: 60/60 agent-runner (1 new silentExpected case), 45/45 telegram dispatch, 111/111 cli-runner spawn.

@jack-stormentswe

Copy link
Copy Markdown
Contributor Author

Rebased onto latest main, CHANGELOG conflict resolved. 60/60 tests still pass. Ready for review when you have time, thanks.

@jack-stormentswe

Copy link
Copy Markdown
Contributor Author

Hi @steipete , How are you?
Please have a look at this PR and let me know your recommendation. ❤️

@jack-stormentswe

Copy link
Copy Markdown
Contributor Author

Rebased onto latest main, CHANGELOG conflict resolved. 61/61 tests still pass.

@jack-stormentswe jack-stormentswe force-pushed the fix/issue-76869 branch 2 times, most recently from 9f8ce84 to 4887c08 Compare May 5, 2026 00:22
@jack-stormentswe

Copy link
Copy Markdown
Contributor Author

Hi @steipete
Hope you are doing well, Please have a look at this PR. ❤️

@anagnorisis2peripeteia

Copy link
Copy Markdown
Contributor

#71817

covered here

@adele-with-a-b

Copy link
Copy Markdown
Contributor

Running this patch locally on OpenClaw 2026.5.7 with the claude-cli backend driving anthropic/claude-opus-4-7 on Bedrock (auth via ~/.claude/settings.json → AWS profile → credential_process). Real deployment, Telegram forum-topic group, daily use.

Happy to contribute whatever form of proof would actually clear the triage: needs-real-behavior-proof label. Screenshots don't work here — streaming behavior (or lack of it) is only visible over time, so the realistic artifact is a short video / asciicast. Listing what I can dump cleanly below — tell me which flavor would help.

Before-vs-after, from ~/.openclaw/logs/gateway.log

Before the bridge patch (stock 2026.5.7), a single turn emits exactly one sendMessage, at the end of the claude live-session duration, even though the live session itself is parsing deltas throughout:

02:18:05.878 [agent/cli-backend] cli exec: provider=claude-cli model=opus promptChars=681 trigger=user useResume=false session=none
02:18:05.889 [agent/cli-backend] claude live session start: provider=claude-cli model=claude-opus-4-7 activeSessions=1
02:18:15.658 [agent/cli-backend] claude live session turn: provider=claude-cli model=claude-opus-4-7 durationMs=9766 rawLines=59
02:18:17.650 [telegram] sendMessage ok chat=-100XXXXXXXXXX message=1378

rawLines=59 is the claude live-session JSONL tally, which includes incremental stream_event:content_block_delta events carrying text_delta (verified by stdout-tracing the spawned claude process, see next section).

After applying the bridge as a hand-patch against the installed dist file (same structural change as this PR, minus the tests), the same style of turn now edits the preview message in-place as text streams in — matching the partial-preview UX the non-CLI path already gets.

Direct evidence that claude CLI emits text_delta under the OC-passed flags

Spawning the same claude binary OpenClaw uses, with the same --include-partial-messages flag OC passes, outside of OpenClaw to isolate the source side:

$ claude -p "count slowly to ten, one number per line" \
    --output-format stream-json --include-partial-messages --verbose

+0.644s  line  1  system:init
+0.644s  line  2  system:status
+2.149s  line  3  stream_event:message_start
+2.259s  line  4  stream_event:content_block_start  (text block)
+2.259s  line  5  stream_event:content_block_delta  text_delta "1"
+2.259s  line  6  stream_event:content_block_delta  text_delta "\n2\n3\n4\n5"
+2.262s  line  7  stream_event:content_block_delta  text_delta "\n6\n7\n8\n9"
+2.264s  line  8  stream_event:content_block_delta  text_delta "\n10"
+2.4s    line 13  result

Four text_delta events in the stream, so there is real content for the event path downstream of claude-live-session.ts to forward. This matches the PR's premise.

Confirms the security guards work

I had a first attempt at the same bridge locally that skipped the silent-reply filter, and the review bot flagged it correctly as a content-leak risk for silentExpected / NO_REPLY / heartbeat runs. Re-applying the shape of this PR (silentExpected short-circuit + handlePartialForTyping pass-through + lastBridgedAssistantText dedup) — heartbeat turns no longer surface partial text into the Telegram preview. The guards in this PR do the right thing.

Forms of proof I can attach

  • Redacted before/after gateway-log excerpts from the same chat.
  • Short asciicast of the Telegram chat rendering under the patched version (shows progressive edits).
  • Same asciicast pair captured against a stock vs. patched gateway on the same box for a side-by-side.

Let me know which of these actually checks the proof label and I'll attach here. Happy to run any additional command you want me to capture.

Unrelated: the lastBridgedAssistantText dedup is the right call for CLI backends that re-emit with accumulated text rather than delta-only — that's the shape I see coming out of the claude-on-Bedrock path.

@jack-stormentswe

Copy link
Copy Markdown
Contributor Author

Thank you so much @adele-with-a-b - this is exactly the live evidence the gate has been asking for. Used your captured before/after gateway-log excerpts and the direct claude binary stdout (with full attribution back to your comment) to fill the structured Real behavior proof section in the PR body. Local check against scripts/github/real-behavior-proof-policy.mjs now returns passed, so the gate should re-run green and triage: needs-real-behavior-proof should clear automatically.

If you do end up capturing the asciicast as well I would love to attach it to the PR for the maintainer review pass, but that is no longer blocking.

@openclaw-barnacle openclaw-barnacle Bot added proof: supplied External PR includes structured after-fix real behavior proof. and removed triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels May 9, 2026
@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 9, 2026
@jack-stormentswe jack-stormentswe marked this pull request as ready for review May 9, 2026 08:43
@steipete steipete merged commit 560c744 into openclaw:main May 9, 2026
111 of 113 checks passed
@steipete

steipete commented May 9, 2026

Copy link
Copy Markdown
Contributor

Landed via rebase onto main.

  • Local proof: pnpm test src/auto-reply/reply/agent-runner-execution.test.ts; pnpm check:test-types; pnpm exec oxfmt --check --threads=1 src/auto-reply/reply/agent-runner-execution.ts src/auto-reply/reply/agent-runner-execution.test.ts CHANGELOG.md; git diff --check
  • Remote proof: Testbox tbx_01kr60yvy5tfvs5m25qsct52g3, pnpm check:changed, exit 0
  • Source head: 0fcc72f
  • Landed commit: 560c744

Thanks @jack-stormentswe!

adele-with-a-b added a commit to adele-with-a-b/openclaw that referenced this pull request May 10, 2026
CLI backends (claude-cli) emit tool_use content blocks and tool_result
user-messages through their --output-format stream-json output, but
parseClaudeCliStreamingDelta filters every record except text_delta, so
those events never reach the agent-event bus. Channel previews that
subscribe to stream:"tool" for live tool-progress decoration (Telegram
"📖 Read:", "🛠️ Bash:", etc.) stay silent during tool-heavy claude-cli
turns even though the underlying CLI is producing the data.

Mirrors PR openclaw#76914 (assistant text deltas) layer-for-layer:

  parser (cli-output.ts) → emit via execute.runtime → bus subscribed
  by agent-runner.runtime → forwards to params.opts.onToolStart

- New dispatchClaudeCliStreamingToolEvent in src/agents/cli-output.ts
  recognises content_block_start (tool_use), the post-block assistant
  snapshot carrying full args, and user-message tool_result blocks.
  Emits via two new optional callbacks on createCliJsonlStreamingParser
  (onToolUseStart, onToolResult) so existing callers are unaffected
  when neither is subscribed.
- Both CLI execution paths in src/agents/cli-runner/execute.ts and
  runClaudeLiveSessionTurn/createTurn in claude-live-session.ts wire
  the new callbacks to emitAgentEvent({stream:"tool", data:{...}})
  using the same shape the embedded native runtime emits at
  pi-embedded-subscribe.handlers.tools.ts:696,998.
- src/auto-reply/reply/agent-runner-execution.ts adds a parallel
  rawUnsubscribeToolBridge next to the assistant-text bridge from
  openclaw#76914. Mirrors that PR's serialize/drain pattern (toolBridgeDelivery
  Promise chain + drainToolBridgeDelivery) so async onToolStart
  callbacks land in order. Filters by runId, respects silentExpected
  (heartbeat/NO_REPLY runs do not leak), and unsubscribes at success,
  catch, and finally so the listener never outlives a turn.
- 4 new unit tests in cli-output.test.ts (full happy path, fallback at
  content_block_stop without assistant snapshot, dedup when both
  content_block_start and assistant snapshot announce the same tool,
  error tool_result with name passthrough).
- 2 new bridge tests in agent-runner-execution.test.ts (forwards tool
  events to onToolStart with correct args; respects silentExpected).
- pnpm test src/agents/cli-output.test.ts → 20/20 PASS
- pnpm test src/auto-reply/reply/agent-runner-execution.test.ts → 73/73 PASS
- pnpm test src/agents/cli-runner → 50/50 PASS
- pnpm check:changed → all gates green
adele-with-a-b added a commit to adele-with-a-b/openclaw that referenced this pull request May 10, 2026
CLI backends (claude-cli) emit tool_use content blocks and tool_result
user-messages through their --output-format stream-json output, but
parseClaudeCliStreamingDelta filters every record except text_delta, so
those events never reach the agent-event bus. Channel previews that
subscribe to stream:"tool" for live tool-progress decoration (Telegram
"📖 Read:", "🛠️ Bash:", etc.) stay silent during tool-heavy claude-cli
turns even though the underlying CLI is producing the data.

Mirrors PR openclaw#76914 (assistant text deltas) layer-for-layer:

  parser (cli-output.ts) → emit via execute.runtime → bus subscribed
  by agent-runner.runtime → forwards to params.opts.onToolStart

- New dispatchClaudeCliStreamingToolEvent in src/agents/cli-output.ts
  recognises content_block_start (tool_use), the post-block assistant
  snapshot carrying full args, and user-message tool_result blocks.
  Emits via two new optional callbacks on createCliJsonlStreamingParser
  (onToolUseStart, onToolResult) so existing callers are unaffected
  when neither is subscribed.
- Both CLI execution paths in src/agents/cli-runner/execute.ts and
  runClaudeLiveSessionTurn/createTurn in claude-live-session.ts wire
  the new callbacks to emitAgentEvent({stream:"tool", data:{...}})
  using the same shape the embedded native runtime emits at
  pi-embedded-subscribe.handlers.tools.ts:696,998.
- src/auto-reply/reply/agent-runner-execution.ts adds a parallel
  rawUnsubscribeToolBridge next to the assistant-text bridge from
  openclaw#76914. Mirrors that PR's serialize/drain pattern (toolBridgeDelivery
  Promise chain + drainToolBridgeDelivery) so async onToolStart
  callbacks land in order. Filters by runId, respects silentExpected
  (heartbeat/NO_REPLY runs do not leak), and unsubscribes at success,
  catch, and finally so the listener never outlives a turn.
- 4 new unit tests in cli-output.test.ts (full happy path, fallback at
  content_block_stop without assistant snapshot, dedup when both
  content_block_start and assistant snapshot announce the same tool,
  error tool_result with name passthrough).
- 2 new bridge tests in agent-runner-execution.test.ts (forwards tool
  events to onToolStart with correct args; respects silentExpected).
- pnpm test src/agents/cli-output.test.ts → 20/20 PASS
- pnpm test src/auto-reply/reply/agent-runner-execution.test.ts → 73/73 PASS
- pnpm test src/agents/cli-runner → 50/50 PASS
- pnpm check:changed → all gates green
adele-with-a-b added a commit to adele-with-a-b/openclaw that referenced this pull request May 10, 2026
CLI backends (claude-cli) emit tool_use content blocks and tool_result
user-messages through their --output-format stream-json output, but
parseClaudeCliStreamingDelta filters every record except text_delta, so
those events never reach the agent-event bus. Channel previews that
subscribe to stream:"tool" for live tool-progress decoration (Telegram
"📖 Read:", "🛠️ Bash:", etc.) stay silent during tool-heavy claude-cli
turns even though the underlying CLI is producing the data.

Mirrors PR openclaw#76914 (assistant text deltas) layer-for-layer:

  parser (cli-output.ts) → emit via execute.runtime → bus subscribed
  by agent-runner.runtime → forwards to params.opts.onToolStart

- New dispatchClaudeCliStreamingToolEvent in src/agents/cli-output.ts
  recognises content_block_start (tool_use), the post-block assistant
  snapshot carrying full args, and user-message tool_result blocks.
  Emits via two new optional callbacks on createCliJsonlStreamingParser
  (onToolUseStart, onToolResult) so existing callers are unaffected
  when neither is subscribed.
- Both CLI execution paths in src/agents/cli-runner/execute.ts and
  runClaudeLiveSessionTurn/createTurn in claude-live-session.ts wire
  the new callbacks to emitAgentEvent({stream:"tool", data:{...}})
  using the same shape the embedded native runtime emits at
  pi-embedded-subscribe.handlers.tools.ts:696,998.
- src/auto-reply/reply/agent-runner-execution.ts adds a parallel
  rawUnsubscribeToolBridge next to the assistant-text bridge from
  openclaw#76914. Mirrors that PR's serialize/drain pattern (toolBridgeDelivery
  Promise chain + drainToolBridgeDelivery) so async onToolStart
  callbacks land in order. Filters by runId, respects silentExpected
  (heartbeat/NO_REPLY runs do not leak), and unsubscribes at success,
  catch, and finally so the listener never outlives a turn.
- 4 new unit tests in cli-output.test.ts (full happy path, fallback at
  content_block_stop without assistant snapshot, dedup when both
  content_block_start and assistant snapshot announce the same tool,
  error tool_result with name passthrough).
- 2 new bridge tests in agent-runner-execution.test.ts (forwards tool
  events to onToolStart with correct args; respects silentExpected).
- pnpm test src/agents/cli-output.test.ts → 20/20 PASS
- pnpm test src/auto-reply/reply/agent-runner-execution.test.ts → 73/73 PASS
- pnpm test src/agents/cli-runner → 50/50 PASS
- pnpm check:changed → all gates green
obviyus pushed a commit to anagnorisis2peripeteia/openclaw that referenced this pull request May 14, 2026
Add a CLI-runtime-gated bridge in runAgentTurnWithFallback that subscribes
to `stream: "assistant"` agent-events for the current runId and re-emits
them as reasoning content through `params.opts.onReasoningStream`. Mirrors
the assistant-text bridge from openclaw#76914 and the tool-event bridge from openclaw#80046:
same Promise-chain serialization + drain, same silentExpected gate, same
unsubscribe pattern at success/catch/finally.

The reply lane is untouched -- `onPartialReply` continues to settle the
final assistant text via openclaw#76914. The reasoning lane now reflects the
model's live text output during streaming, which is the only "what is the
model producing right now" signal available for claude-opus-4-7 over
claude-cli (Anthropic suppresses readable thinking_delta events on the
wire for opus-4-7; only thinking content_block + signature_delta arrive).

The bridge is gated on isCliProvider so API/native runtimes that already
get reasoning content from real thinking_delta events do NOT double-receive
text_delta as reasoning.

Tests cover:
- Forwards assistant agent-events to onReasoningStream with correct text
- Respects silentExpected (heartbeat / NO_REPLY runs don't emit)
- Does not fire on the API/native runtime path (gate works)
obviyus pushed a commit that referenced this pull request May 14, 2026
Add a CLI-runtime-gated bridge in runAgentTurnWithFallback that subscribes
to `stream: "assistant"` agent-events for the current runId and re-emits
them as reasoning content through `params.opts.onReasoningStream`. Mirrors
the assistant-text bridge from #76914 and the tool-event bridge from #80046:
same Promise-chain serialization + drain, same silentExpected gate, same
unsubscribe pattern at success/catch/finally.

The reply lane is untouched -- `onPartialReply` continues to settle the
final assistant text via #76914. The reasoning lane now reflects the
model's live text output during streaming, which is the only "what is the
model producing right now" signal available for claude-opus-4-7 over
claude-cli (Anthropic suppresses readable thinking_delta events on the
wire for opus-4-7; only thinking content_block + signature_delta arrive).

The bridge is gated on isCliProvider so API/native runtimes that already
get reasoning content from real thinking_delta events do NOT double-receive
text_delta as reasoning.

Tests cover:
- Forwards assistant agent-events to onReasoningStream with correct text
- Respects silentExpected (heartbeat / NO_REPLY runs don't emit)
- Does not fire on the API/native runtime path (gate works)
lykeion-dev pushed a commit to lykeion-dev/openclaw--rev that referenced this pull request May 14, 2026
adele-with-a-b added a commit to adele-with-a-b/openclaw that referenced this pull request May 19, 2026
CLI backends (claude-cli) emit tool_use content blocks and tool_result
user-messages through their --output-format stream-json output, but
parseClaudeCliStreamingDelta filters every record except text_delta, so
those events never reach the agent-event bus. Channel previews that
subscribe to stream:"tool" for live tool-progress decoration (Telegram
"📖 Read:", "🛠️ Bash:", etc.) stay silent during tool-heavy claude-cli
turns even though the underlying CLI is producing the data.

Mirrors PR openclaw#76914 (assistant text deltas) layer-for-layer:

  parser (cli-output.ts) → emit via execute.runtime → bus subscribed
  by agent-runner.runtime → forwards to params.opts.onToolStart

- New dispatchClaudeCliStreamingToolEvent in src/agents/cli-output.ts
  recognises content_block_start (tool_use), the post-block assistant
  snapshot carrying full args, and user-message tool_result blocks.
  Emits via two new optional callbacks on createCliJsonlStreamingParser
  (onToolUseStart, onToolResult) so existing callers are unaffected
  when neither is subscribed.
- Both CLI execution paths in src/agents/cli-runner/execute.ts and
  runClaudeLiveSessionTurn/createTurn in claude-live-session.ts wire
  the new callbacks to emitAgentEvent({stream:"tool", data:{...}})
  using the same shape the embedded native runtime emits at
  pi-embedded-subscribe.handlers.tools.ts:696,998.
- src/auto-reply/reply/agent-runner-execution.ts adds a parallel
  rawUnsubscribeToolBridge next to the assistant-text bridge from
  openclaw#76914. Mirrors that PR's serialize/drain pattern (toolBridgeDelivery
  Promise chain + drainToolBridgeDelivery) so async onToolStart
  callbacks land in order. Filters by runId, respects silentExpected
  (heartbeat/NO_REPLY runs do not leak), and unsubscribes at success,
  catch, and finally so the listener never outlives a turn.
- 4 new unit tests in cli-output.test.ts (full happy path, fallback at
  content_block_stop without assistant snapshot, dedup when both
  content_block_start and assistant snapshot announce the same tool,
  error tool_result with name passthrough).
- 2 new bridge tests in agent-runner-execution.test.ts (forwards tool
  events to onToolStart with correct args; respects silentExpected).
- pnpm test src/agents/cli-output.test.ts → 20/20 PASS
- pnpm test src/auto-reply/reply/agent-runner-execution.test.ts → 73/73 PASS
- pnpm test src/agents/cli-runner → 50/50 PASS
- pnpm check:changed → all gates green
adele-with-a-b added a commit to adele-with-a-b/openclaw that referenced this pull request May 22, 2026
CLI backends (claude-cli) emit tool_use content blocks and tool_result
user-messages through their --output-format stream-json output, but
parseClaudeCliStreamingDelta filters every record except text_delta, so
those events never reach the agent-event bus. Channel previews that
subscribe to stream:"tool" for live tool-progress decoration (Telegram
"📖 Read:", "🛠️ Bash:", etc.) stay silent during tool-heavy claude-cli
turns even though the underlying CLI is producing the data.

Mirrors PR openclaw#76914 (assistant text deltas) layer-for-layer:

  parser (cli-output.ts) → emit via execute.runtime → bus subscribed
  by agent-runner.runtime → forwards to params.opts.onToolStart

- New dispatchClaudeCliStreamingToolEvent in src/agents/cli-output.ts
  recognises content_block_start (tool_use), the post-block assistant
  snapshot carrying full args, and user-message tool_result blocks.
  Emits via two new optional callbacks on createCliJsonlStreamingParser
  (onToolUseStart, onToolResult) so existing callers are unaffected
  when neither is subscribed.
- Both CLI execution paths in src/agents/cli-runner/execute.ts and
  runClaudeLiveSessionTurn/createTurn in claude-live-session.ts wire
  the new callbacks to emitAgentEvent({stream:"tool", data:{...}})
  using the same shape the embedded native runtime emits at
  pi-embedded-subscribe.handlers.tools.ts:696,998.
- src/auto-reply/reply/agent-runner-execution.ts adds a parallel
  rawUnsubscribeToolBridge next to the assistant-text bridge from
  openclaw#76914. Mirrors that PR's serialize/drain pattern (toolBridgeDelivery
  Promise chain + drainToolBridgeDelivery) so async onToolStart
  callbacks land in order. Filters by runId, respects silentExpected
  (heartbeat/NO_REPLY runs do not leak), and unsubscribes at success,
  catch, and finally so the listener never outlives a turn.
- 4 new unit tests in cli-output.test.ts (full happy path, fallback at
  content_block_stop without assistant snapshot, dedup when both
  content_block_start and assistant snapshot announce the same tool,
  error tool_result with name passthrough).
- 2 new bridge tests in agent-runner-execution.test.ts (forwards tool
  events to onToolStart with correct args; respects silentExpected).
- pnpm test src/agents/cli-output.test.ts → 20/20 PASS
- pnpm test src/auto-reply/reply/agent-runner-execution.test.ts → 73/73 PASS
- pnpm test src/agents/cli-runner → 50/50 PASS
- pnpm check:changed → all gates green
adele-with-a-b added a commit to adele-with-a-b/openclaw that referenced this pull request May 22, 2026
CLI backends (claude-cli) emit tool_use content blocks and tool_result
user-messages through their --output-format stream-json output, but
parseClaudeCliStreamingDelta filters every record except text_delta, so
those events never reach the agent-event bus. Channel previews that
subscribe to stream:"tool" for live tool-progress decoration (Telegram
"📖 Read:", "🛠️ Bash:", etc.) stay silent during tool-heavy claude-cli
turns even though the underlying CLI is producing the data.

Mirrors PR openclaw#76914 (assistant text deltas) layer-for-layer:

  parser (cli-output.ts) → emit via execute.runtime → bus subscribed
  by agent-runner.runtime → forwards to params.opts.onToolStart

- New dispatchClaudeCliStreamingToolEvent in src/agents/cli-output.ts
  recognises content_block_start (tool_use), the post-block assistant
  snapshot carrying full args, and user-message tool_result blocks.
  Emits via two new optional callbacks on createCliJsonlStreamingParser
  (onToolUseStart, onToolResult) so existing callers are unaffected
  when neither is subscribed.
- Both CLI execution paths in src/agents/cli-runner/execute.ts and
  runClaudeLiveSessionTurn/createTurn in claude-live-session.ts wire
  the new callbacks to emitAgentEvent({stream:"tool", data:{...}})
  using the same shape the embedded native runtime emits at
  pi-embedded-subscribe.handlers.tools.ts:696,998.
- src/auto-reply/reply/agent-runner-execution.ts adds a parallel
  rawUnsubscribeToolBridge next to the assistant-text bridge from
  openclaw#76914. Mirrors that PR's serialize/drain pattern (toolBridgeDelivery
  Promise chain + drainToolBridgeDelivery) so async onToolStart
  callbacks land in order. Filters by runId, respects silentExpected
  (heartbeat/NO_REPLY runs do not leak), and unsubscribes at success,
  catch, and finally so the listener never outlives a turn.
- 4 new unit tests in cli-output.test.ts (full happy path, fallback at
  content_block_stop without assistant snapshot, dedup when both
  content_block_start and assistant snapshot announce the same tool,
  error tool_result with name passthrough).
- 2 new bridge tests in agent-runner-execution.test.ts (forwards tool
  events to onToolStart with correct args; respects silentExpected).
- pnpm test src/agents/cli-output.test.ts → 20/20 PASS
- pnpm test src/auto-reply/reply/agent-runner-execution.test.ts → 73/73 PASS
- pnpm test src/agents/cli-runner → 50/50 PASS
- pnpm check:changed → all gates green
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
Add a CLI-runtime-gated bridge in runAgentTurnWithFallback that subscribes
to `stream: "assistant"` agent-events for the current runId and re-emits
them as reasoning content through `params.opts.onReasoningStream`. Mirrors
the assistant-text bridge from openclaw#76914 and the tool-event bridge from openclaw#80046:
same Promise-chain serialization + drain, same silentExpected gate, same
unsubscribe pattern at success/catch/finally.

The reply lane is untouched -- `onPartialReply` continues to settle the
final assistant text via openclaw#76914. The reasoning lane now reflects the
model's live text output during streaming, which is the only "what is the
model producing right now" signal available for claude-opus-4-7 over
claude-cli (Anthropic suppresses readable thinking_delta events on the
wire for opus-4-7; only thinking content_block + signature_delta arrive).

The bridge is gated on isCliProvider so API/native runtimes that already
get reasoning content from real thinking_delta events do NOT double-receive
text_delta as reasoning.

Tests cover:
- Forwards assistant agent-events to onReasoningStream with correct text
- Respects silentExpected (heartbeat / NO_REPLY runs don't emit)
- Does not fire on the API/native runtime path (gate works)
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
Add a CLI-runtime-gated bridge in runAgentTurnWithFallback that subscribes
to `stream: "assistant"` agent-events for the current runId and re-emits
them as reasoning content through `params.opts.onReasoningStream`. Mirrors
the assistant-text bridge from openclaw#76914 and the tool-event bridge from openclaw#80046:
same Promise-chain serialization + drain, same silentExpected gate, same
unsubscribe pattern at success/catch/finally.

The reply lane is untouched -- `onPartialReply` continues to settle the
final assistant text via openclaw#76914. The reasoning lane now reflects the
model's live text output during streaming, which is the only "what is the
model producing right now" signal available for claude-opus-4-7 over
claude-cli (Anthropic suppresses readable thinking_delta events on the
wire for opus-4-7; only thinking content_block + signature_delta arrive).

The bridge is gated on isCliProvider so API/native runtimes that already
get reasoning content from real thinking_delta events do NOT double-receive
text_delta as reasoning.

Tests cover:
- Forwards assistant agent-events to onReasoningStream with correct text
- Respects silentExpected (heartbeat / NO_REPLY runs don't emit)
- Does not fire on the API/native runtime path (gate works)
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
Add a CLI-runtime-gated bridge in runAgentTurnWithFallback that subscribes
to `stream: "assistant"` agent-events for the current runId and re-emits
them as reasoning content through `params.opts.onReasoningStream`. Mirrors
the assistant-text bridge from openclaw#76914 and the tool-event bridge from openclaw#80046:
same Promise-chain serialization + drain, same silentExpected gate, same
unsubscribe pattern at success/catch/finally.

The reply lane is untouched -- `onPartialReply` continues to settle the
final assistant text via openclaw#76914. The reasoning lane now reflects the
model's live text output during streaming, which is the only "what is the
model producing right now" signal available for claude-opus-4-7 over
claude-cli (Anthropic suppresses readable thinking_delta events on the
wire for opus-4-7; only thinking content_block + signature_delta arrive).

The bridge is gated on isCliProvider so API/native runtimes that already
get reasoning content from real thinking_delta events do NOT double-receive
text_delta as reasoning.

Tests cover:
- Forwards assistant agent-events to onReasoningStream with correct text
- Respects silentExpected (heartbeat / NO_REPLY runs don't emit)
- Does not fire on the API/native runtime path (gate works)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

proof: sufficient ClawSweeper judged the real behavior proof convincing. proof: supplied External PR includes structured after-fix real behavior proof. size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CLI backend (claude-cli) does not stream text deltas to Telegram preview — only native API path is wired

4 participants