fix(agents/cli): bridge CLI assistant deltas into channel preview (#76869)#76914
Conversation
|
Codex review: needs changes before merge. Summary 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 Next step before merge Security Review findings
Review detailsBest 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:
Overall correctness: patch is incorrect Acceptance criteria:
What I checked:
Likely related people:
Remaining risk / open question:
Codex review notes: model gpt-5.5, reasoning high; reviewed against 73437fdb6576. |
|
Hi @kai-jar, picked this up. Could you take a look when you have time, thanks. |
733a735 to
1bd7009
Compare
|
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. |
1bd7009 to
68a5120
Compare
|
Rebased onto latest main, CHANGELOG conflict resolved. 60/60 tests still pass. Ready for review when you have time, thanks. |
|
Hi @steipete , How are you? |
68a5120 to
927cdc9
Compare
|
Rebased onto latest main, CHANGELOG conflict resolved. 61/61 tests still pass. |
9f8ce84 to
4887c08
Compare
|
Hi @steipete |
4887c08 to
0f10c47
Compare
|
covered here |
|
Running this patch locally on OpenClaw Happy to contribute whatever form of proof would actually clear the Before-vs-after, from
|
|
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 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. |
|
Landed via rebase onto
Thanks @jack-stormentswe! |
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
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
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
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)
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)
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
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
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
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)
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)
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)
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 firesonPartialReplyafter the final result is assembled. Telegram and other channel previews listen foronPartialReply, so users on Anthropic Max plan (forced throughclaude-cli) see no live preview - only a typing indicator and then the full message.Register an
onAgentEventlistener for the active runId beforerunCliAgent, forward each new accumulated assistant text intoopts.onPartialReply(andtypingSignals.signalTextDelta), and unsubscribe before the post-run final emit so we do not double-fire on completion.pnpm test src/auto-reply/reply/agent-runner-execution.test.ts-> 59/59 PASS (1 new regression case for CLI backend (claude-cli) does not stream text deltas to Telegram preview — only native API path is wired #76869)pnpm test extensions/telegram/src/bot-message-dispatch.test.ts-> 45/45 PASSpnpm test src/agents/cli-runner.spawn.test.ts-> 111/111 PASSpnpm exec oxfmt --checkcleanReal behavior proof
Behavior addressed: CLI-backed reply turns (e.g.
claude-clion Anthropic Max / Bedrock) silently dropping incrementaltext_deltaevents on the floor betweenclaude-live-session.tsand the channel preview, so Telegram (and any otheronPartialReplyconsumer) 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.7deployment with theclaude-clibackend drivinganthropic/claude-opus-4-7on 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 stock2026.5.7and the patched build on the same box.Exact steps or command run after this patch:
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
claudebinary OpenClaw spawns, with the exact--include-partial-messagesflag OpenClaw passes, to confirm that realtext_deltaevents 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.7deployment.Stock
2026.5.7(no patch) -- a single turn emits exactly onesendMessageat the end, even though the live session is parsing 59 raw JSONL events including incremental deltas: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
claudebinary really does emittext_deltaunder the flags OpenClaw passes (so there is real content for this PR's listener to forward):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_deltaevents from the upstream binary now flow throughonAgentEvent->onPartialReply->signalTextDelta-> Telegram preview-edit, where stock2026.5.7was dropping all four. The PR'ssilentExpectedshort-circuit +handlePartialForTypingpass-through +lastBridgedAssistantTextdedup were also exercised in the same run: heartbeat /NO_REPLY/silentExpectedruns do not surface partial text into the Telegram preview, and CLI backends that re-emit accumulatedtextrather than delta-only do not duplicate edits. Both security guards behave as intended.What was not tested: a
codex-clibackend run (onlyclaude-cliwas 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.