openclaw - ✅(Solved) Fix [Feature]: support cap thinking-event when websocket connect [2 pull requests, 1 comments, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#48995Fetched 2026-04-08 00:50:01
View on GitHub
Comments
1
Participants
1
Timeline
5
Reactions
0
Participants
Timeline (top)
cross-referenced ×2commented ×1labeled ×1renamed ×1
  • Problem 1 (gateway never received thinking tokens): streamReasoning was initialized as reasoningMode === "stream" && typeof onReasoningStream === "function". Gateway WebSocket runs never supply onReasoningStream, so the flag was always false and emitAgentEvent for stream: "thinking" was never called — even when the session had /reasoning stream configured.
  • Problem 2 (delta === text on every event): The delta was computed by formatted.startsWith(prior), but formatReasoningMessage wraps each line in _…_ markdown italics, breaking prefix matching (e.g. "_Checking files_" is not a prefix of "_Checking files done_"). When the check failed, the code fell back to delta = formatted, making every event's delta equal to text. Additionally, when a pure incremental chunk was passed (e.g. native thinking_delta fallback), lastStreamedReasoning was set to just the chunk instead of the accumulated text, corrupting all future deltas.
  • Problem 3 (no cap-gated subscription for thinking events): Thinking events previously fell through the generic broadcast("agent", …) path (same as all non-tool events), delivering raw reasoning tokens to every connected client with no opt-in mechanism.
  • What changed: Added a thinking-events client capability; thinking events are now routed exclusively to registered cap subscribers via broadcastToConnIds; nodeSendToSession is guarded against thinking events; delta computation is based on raw (unformatted) accumulated text; streamReasoning is decoupled from onReasoningStream.
  • What did NOT change: Tool-event routing, session lifecycle, chat delta throttling, nodeSendToSession behavior for all non-thinking streams, and the external format of text in thinking events (still formatReasoningMessage output).

Error Message

  • Risk: thinkingEventRecipients registry entries accumulate if markFinal is not called on error paths.
  • Mitigation: markFinal is called for both lifecyclePhase === "end" and lifecyclePhase === "error", matching the existing toolEventRecipients cleanup pattern; the TTL-based pruning in createThinkingEventRecipientRegistry provides a backstop.

Root Cause

  • Events with stream: "thinking" arrive in real time.
  • Each event's data.delta is the new incremental raw thinking text (e.g. " because", not the full accumulated string).
  • Each event's data.text is the fully formatted accumulated thinking string (e.g. "Reasoning:\n_Because it helps_").

Fix Action

Fix / Workaround

  • New permissions/capabilities? Yes — adds thinking-events cap; reasoning tokens are now opt-in rather than broadcast to all connected clients. This is a tightening of the previous behaviour (net security improvement).
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No
  • Risk: A client could advertise the cap to receive another session's thinking tokens if the session key lookup were wrong. Mitigation: thinkingEventRecipients is keyed by runId; recipients are registered only for runs initiated by (or active in) the requesting connection's session, mirroring the existing tool-events ownership model.

Risks and Mitigations

  • Risk: thinkingEventRecipients registry entries accumulate if markFinal is not called on error paths.
    • Mitigation: markFinal is called for both lifecyclePhase === "end" and lifecyclePhase === "error", matching the existing toolEventRecipients cleanup pattern; the TTL-based pruning in createThinkingEventRecipientRegistry provides a backstop.
  • Risk: Clients using the existing general broadcast to observe thinking tokens (e.g. custom integrations relying on undocumented behaviour) will no longer receive them without opting in.
    • Mitigation: The previous broadcast was never documented or intended; thinking-events cap is a straightforward one-line addition to the client connect payload.

PR fix notes

PR #50787: feat(gateway): support cap thinking-event when websocket connect

Description (problem / solution / changelog)

Summary

  • Problem 1 (gateway never received thinking tokens): streamReasoning was initialized as reasoningMode === "stream" && typeof onReasoningStream === "function". Gateway WebSocket runs never supply onReasoningStream, so the flag was always false and emitAgentEvent for stream: "thinking" was never called — even when the session had /reasoning stream configured.
  • Problem 2 (delta === text on every event): The delta was computed by formatted.startsWith(prior), but formatReasoningMessage wraps each line in _…_ markdown italics, breaking prefix matching (e.g. "_Checking files_" is not a prefix of "_Checking files done_"). When the check failed, the code fell back to delta = formatted, making every event's delta equal to text. Additionally, when a pure incremental chunk was passed (e.g. native thinking_delta fallback), lastStreamedReasoning was set to just the chunk instead of the accumulated text, corrupting all future deltas.
  • Problem 3 (no cap-gated subscription for thinking events): Thinking events previously fell through the generic broadcast("agent", …) path (same as all non-tool events), delivering raw reasoning tokens to every connected client with no opt-in mechanism.
  • What changed: Added a thinking-events client capability; thinking events are now routed exclusively to registered cap subscribers via broadcastToConnIds; nodeSendToSession is guarded against thinking events; delta computation is based on raw (unformatted) accumulated text; streamReasoning is decoupled from onReasoningStream.
  • What did NOT change: Tool-event routing, session lifecycle, chat delta throttling, nodeSendToSession behavior for all non-thinking streams, and the external format of text in thinking events (still formatReasoningMessage output).

Change Type (select all)

  • Bug fix
  • Feature

Scope (select all touched areas)

  • Gateway / orchestration
  • API / contracts

Linked Issue/PR

  • Closes #48995
  • Related #48995

User-visible / Behavior Changes

  • Clients that advertise caps: ["thinking-events"] in their connect payload now receive agent events with stream: "thinking" containing { text: <full formatted thinking so far>, delta: <new raw increment> } in real time when the session has reasoningLevel: "stream".
  • Clients that do not advertise this cap no longer receive thinking-stream tokens at all (previously they received them via the general broadcast).
  • delta is now always the incremental raw text added since the last event, not a repeat of text.

Security Impact (required)

  • New permissions/capabilities? Yes — adds thinking-events cap; reasoning tokens are now opt-in rather than broadcast to all connected clients. This is a tightening of the previous behaviour (net security improvement).
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No
  • Risk: A client could advertise the cap to receive another session's thinking tokens if the session key lookup were wrong. Mitigation: thinkingEventRecipients is keyed by runId; recipients are registered only for runs initiated by (or active in) the requesting connection's session, mirroring the existing tool-events ownership model.

Repro + Verification

Environment

  • OS: Linux / macOS / Windows
  • Runtime: Node 22+ / Bun
  • Model/provider: Any provider with reasoningLevel: "stream" (e.g. Anthropic claude-3-7-sonnet with extended thinking, or DeepSeek-R1 via OpenRouter)
  • Integration/channel: WebSocket gateway client
  • Relevant config: reasoningLevel: "stream" in session or via /reasoning stream directive

Steps

  1. Connect a WebSocket client with caps: ["thinking-events"].
  2. Send a chat message to a session whose model supports extended thinking and has reasoningLevel: "stream".
  3. Observe the agent events arriving on the connection.

Expected

  • Events with stream: "thinking" arrive in real time.
  • Each event's data.delta is the new incremental raw thinking text (e.g. " because", not the full accumulated string).
  • Each event's data.text is the fully formatted accumulated thinking string (e.g. "Reasoning:\n_Because it helps_").

Actual (before fix)

  • No stream: "thinking" events arrived at all for gateway WebSocket clients.
  • When thinking events did fire (e.g. via typing-signal path), delta equalled text on every event after the first full-text comparison failed due to markdown wrapping.

Evidence

  • Failing test/log before + passing after: streams native thinking_delta events and signals reasoning end in pi-embedded-subscribe.subscribe-embedded-pi-session.subscribeembeddedpisession.test.ts now passes; previously failed with accumulated text corruption ("Reasoning:\n_Checking files_Reasoning:" instead of "Reasoning:\n_Checking files done_").
  • TypeScript type-check (pnpm tsgo) passes with no new errors introduced by these changes.
  • Lint (pnpm lint) passes clean.
  • Format (pnpm format:check) passes clean.

Clients with the thinking-events cap receive thinking messages~ <img width="1371" height="793" alt="image" src="https://github.com/user-attachments/assets/3d48db6a-6465-4a46-9bc7-bf319f5b5056" /> Clients already connected to OpenClaw without the thinking-events cap are not affected~ <img width="838" height="495" alt="image" src="https://github.com/user-attachments/assets/26fa9334-f104-4194-bc0b-0bd6a45ab2ec" />

Human Verification (required)

  • Verified scenarios: TypeScript compilation, oxlint, oxfmt, targeted vitest run for the subscribeembeddedpisession suite.
  • Edge cases checked: (a) first thinking event where prior = ""delta = full first chunk, accumulated = full first chunk; (b) accumulated full-text update where raw text is a proper prefix of new raw text → delta = suffix, accumulated = new text; (c) pure incremental chunk (native thinking_delta fallback) where raw chunk does NOT start with prior accumulated → delta = chunk, accumulated = prior + chunk; (d) runs with isControlUiVisible: false → thinking events are skipped entirely.
  • What you did not verify: live end-to-end test against a real streaming Anthropic model; multi-connection race scenarios; iOS/Android/macOS app cap negotiation.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes — clients without thinking-events in caps are unaffected; they simply stop receiving the thinking-stream tokens they were previously getting via broadcast (which was unintentional).
  • Config/env changes? No
  • Migration needed? No — clients that want thinking events add "thinking-events" to their caps array.

Risks and Mitigations

  • Risk: thinkingEventRecipients registry entries accumulate if markFinal is not called on error paths.
    • Mitigation: markFinal is called for both lifecyclePhase === "end" and lifecyclePhase === "error", matching the existing toolEventRecipients cleanup pattern; the TTL-based pruning in createThinkingEventRecipientRegistry provides a backstop.
  • Risk: Clients using the existing general broadcast to observe thinking tokens (e.g. custom integrations relying on undocumented behaviour) will no longer receive them without opting in.
    • Mitigation: The previous broadcast was never documented or intended; thinking-events cap is a straightforward one-line addition to the client connect payload.

Changed files

  • src/agents/pi-embedded-subscribe.handlers.types.ts (modified, +2/-0)
  • src/agents/pi-embedded-subscribe.ts (modified, +35/-12)
  • src/gateway/protocol/client-info.ts (modified, +1/-0)
  • src/gateway/server-chat.agent-events.test.ts (modified, +4/-0)
  • src/gateway/server-chat.ts (modified, +80/-1)
  • src/gateway/server-methods/agent.ts (modified, +20/-5)
  • src/gateway/server-methods/chat.directive-tags.test.ts (modified, +2/-0)
  • src/gateway/server-methods/chat.ts (modified, +18/-4)
  • src/gateway/server-methods/types.ts (modified, +1/-0)
  • src/gateway/server-runtime-state.ts (modified, +4/-0)
  • src/gateway/server.impl.ts (modified, +3/-0)

PR #54821: feat: support capability subscription thinking events

Description (problem / solution / changelog)

Summary

  • Problem 1 (gateway never received thinking tokens): streamReasoning was initialized as reasoningMode === "stream" && typeof onReasoningStream === "function". Gateway WebSocket runs never supply onReasoningStream, so the flag was always false and emitAgentEvent for stream: "thinking" was never called — even when the session had /reasoning stream configured.
  • Problem 2 (delta === text on every event): The delta was computed by formatted.startsWith(prior), but formatReasoningMessage wraps each line in _…_ markdown italics, breaking prefix matching (e.g. "_Checking files_" is not a prefix of "_Checking files done_"). When the check failed, the code fell back to delta = formatted, making every event's delta equal to text. Additionally, when a pure incremental chunk was passed (e.g. native thinking_delta fallback), lastStreamedReasoning was set to just the chunk instead of the accumulated text, corrupting all future deltas.
  • Problem 3 (no cap-gated subscription for thinking events): Thinking events previously fell through the generic broadcast("agent", …) path (same as all non-tool events), delivering raw reasoning tokens to every connected client with no opt-in mechanism.
  • What changed: Added a thinking-events client capability; thinking events are now routed exclusively to registered cap subscribers via broadcastToConnIds; nodeSendToSession is guarded against thinking events; delta computation is based on raw (unformatted) accumulated text; streamReasoning is decoupled from onReasoningStream.
  • What did NOT change: Tool-event routing, session lifecycle, chat delta throttling, nodeSendToSession behavior for all non-thinking streams, and the external format of text in thinking events (still formatReasoningMessage output).

Change Type (select all)

  • Bug fix
  • Feature

Scope (select all touched areas)

  • Gateway / orchestration
  • API / contracts

Linked Issue/PR

  • Closes #48995
  • Related #48995

User-visible / Behavior Changes

  • Clients that advertise caps: ["thinking-events"] in their connect payload now receive agent events with stream: "thinking" containing { text: <full formatted thinking so far>, delta: <new raw increment> } in real time when the session has reasoningLevel: "stream".
  • Clients that do not advertise this cap no longer receive thinking-stream tokens at all (previously they received them via the general broadcast).
  • delta is now always the incremental raw text added since the last event, not a repeat of text.

Security Impact (required)

  • New permissions/capabilities? Yes — adds thinking-events cap; reasoning tokens are now opt-in rather than broadcast to all connected clients. This is a tightening of the previous behaviour (net security improvement).
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No
  • Risk: A client could advertise the cap to receive another session's thinking tokens if the session key lookup were wrong. Mitigation: thinkingEventRecipients is keyed by runId; recipients are registered only for runs initiated by (or active in) the requesting connection's session, mirroring the existing tool-events ownership model.

Repro + Verification

Environment

  • OS: Linux / macOS / Windows
  • Runtime: Node 22+ / Bun
  • Model/provider: Any provider with reasoningLevel: "stream" (e.g. Anthropic claude-3-7-sonnet with extended thinking, or DeepSeek-R1 via OpenRouter)
  • Integration/channel: WebSocket gateway client
  • Relevant config: reasoningLevel: "stream" in session or via /reasoning stream directive

Steps

  1. Connect a WebSocket client with caps: ["thinking-events"].
  2. Send a chat message to a session whose model supports extended thinking and has reasoningLevel: "stream".
  3. Observe the agent events arriving on the connection.

Expected

  • Events with stream: "thinking" arrive in real time.
  • Each event's data.delta is the new incremental raw thinking text (e.g. " because", not the full accumulated string).
  • Each event's data.text is the fully formatted accumulated thinking string (e.g. "Reasoning:\n_Because it helps_").

Actual (before fix)

  • No stream: "thinking" events arrived at all for gateway WebSocket clients.
  • When thinking events did fire (e.g. via typing-signal path), delta equalled text on every event after the first full-text comparison failed due to markdown wrapping.

Evidence

  • Failing test/log before + passing after: streams native thinking_delta events and signals reasoning end in pi-embedded-subscribe.subscribe-embedded-pi-session.subscribeembeddedpisession.test.ts now passes; previously failed with accumulated text corruption ("Reasoning:\n_Checking files_Reasoning:" instead of "Reasoning:\n_Checking files done_").
  • TypeScript type-check (pnpm tsgo) passes with no new errors introduced by these changes.
  • Lint (pnpm lint) passes clean.
  • Format (pnpm format:check) passes clean.

Clients with the thinking-events cap receive thinking messages~ <img width="1371" height="793" alt="image" src="https://github.com/user-attachments/assets/3d48db6a-6465-4a46-9bc7-bf319f5b5056" /> Clients already connected to OpenClaw without the thinking-events cap are not affected~ <img width="838" height="495" alt="image" src="https://github.com/user-attachments/assets/26fa9334-f104-4194-bc0b-0bd6a45ab2ec" />

Human Verification (required)

  • Verified scenarios: TypeScript compilation, oxlint, oxfmt, targeted vitest run for the subscribeembeddedpisession suite.
  • Edge cases checked: (a) first thinking event where prior = ""delta = full first chunk, accumulated = full first chunk; (b) accumulated full-text update where raw text is a proper prefix of new raw text → delta = suffix, accumulated = new text; (c) pure incremental chunk (native thinking_delta fallback) where raw chunk does NOT start with prior accumulated → delta = chunk, accumulated = prior + chunk; (d) runs with isControlUiVisible: false → thinking events are skipped entirely.
  • What you did not verify: live end-to-end test against a real streaming Anthropic model; multi-connection race scenarios; iOS/Android/macOS app cap negotiation.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes — clients without thinking-events in caps are unaffected; they simply stop receiving the thinking-stream tokens they were previously getting via broadcast (which was unintentional).
  • Config/env changes? No
  • Migration needed? No — clients that want thinking events add "thinking-events" to their caps array.

Risks and Mitigations

  • Risk: thinkingEventRecipients registry entries accumulate if markFinal is not called on error paths.
    • Mitigation: markFinal is called for both lifecyclePhase === "end" and lifecyclePhase === "error", matching the existing toolEventRecipients cleanup pattern; the TTL-based pruning in createThinkingEventRecipientRegistry provides a backstop.
  • Risk: Clients using the existing general broadcast to observe thinking tokens (e.g. custom integrations relying on undocumented behaviour) will no longer receive them without opting in.
    • Mitigation: The previous broadcast was never documented or intended; thinking-events cap is a straightforward one-line addition to the client connect payload.

Changed files

  • docs/.generated/plugin-sdk-api-baseline.json (modified, +1/-1)
  • docs/.generated/plugin-sdk-api-baseline.jsonl (modified, +1/-1)
  • src/agents/pi-embedded-subscribe.handlers.types.ts (modified, +4/-0)
  • src/agents/pi-embedded-subscribe.ts (modified, +114/-12)
  • src/gateway/protocol/client-info.ts (modified, +1/-0)
  • src/gateway/server-chat.agent-events.test.ts (modified, +85/-0)
  • src/gateway/server-chat.ts (modified, +80/-1)
  • src/gateway/server-methods/agent.ts (modified, +20/-5)
  • src/gateway/server-methods/chat.abort-authorization.test.ts (modified, +15/-1)
  • src/gateway/server-methods/chat.directive-tags.test.ts (modified, +6/-1)
  • src/gateway/server-methods/chat.ts (modified, +44/-22)
  • src/gateway/server-methods/types.ts (modified, +1/-0)
  • src/gateway/server-runtime-state.ts (modified, +4/-0)
  • src/gateway/server.impl.ts (modified, +3/-0)
RAW_BUFFERClick to expand / collapse

Summary

  • Problem 1 (gateway never received thinking tokens): streamReasoning was initialized as reasoningMode === "stream" && typeof onReasoningStream === "function". Gateway WebSocket runs never supply onReasoningStream, so the flag was always false and emitAgentEvent for stream: "thinking" was never called — even when the session had /reasoning stream configured.
  • Problem 2 (delta === text on every event): The delta was computed by formatted.startsWith(prior), but formatReasoningMessage wraps each line in _…_ markdown italics, breaking prefix matching (e.g. "_Checking files_" is not a prefix of "_Checking files done_"). When the check failed, the code fell back to delta = formatted, making every event's delta equal to text. Additionally, when a pure incremental chunk was passed (e.g. native thinking_delta fallback), lastStreamedReasoning was set to just the chunk instead of the accumulated text, corrupting all future deltas.
  • Problem 3 (no cap-gated subscription for thinking events): Thinking events previously fell through the generic broadcast("agent", …) path (same as all non-tool events), delivering raw reasoning tokens to every connected client with no opt-in mechanism.
  • What changed: Added a thinking-events client capability; thinking events are now routed exclusively to registered cap subscribers via broadcastToConnIds; nodeSendToSession is guarded against thinking events; delta computation is based on raw (unformatted) accumulated text; streamReasoning is decoupled from onReasoningStream.
  • What did NOT change: Tool-event routing, session lifecycle, chat delta throttling, nodeSendToSession behavior for all non-thinking streams, and the external format of text in thinking events (still formatReasoningMessage output).

Change Type (select all)

  • Bug fix
  • Feature

Scope (select all touched areas)

  • Gateway / orchestration
  • API / contracts

Linked Issue/PR

  • Closes #
  • Related #

User-visible / Behavior Changes

  • Clients that advertise caps: ["thinking-events"] in their connect payload now receive agent events with stream: "thinking" containing { text: <full formatted thinking so far>, delta: <new raw increment> } in real time when the session has reasoningLevel: "stream".
  • Clients that do not advertise this cap no longer receive thinking-stream tokens at all (previously they received them via the general broadcast).
  • delta is now always the incremental raw text added since the last event, not a repeat of text.

Security Impact (required)

  • New permissions/capabilities? Yes — adds thinking-events cap; reasoning tokens are now opt-in rather than broadcast to all connected clients. This is a tightening of the previous behaviour (net security improvement).
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No
  • Risk: A client could advertise the cap to receive another session's thinking tokens if the session key lookup were wrong. Mitigation: thinkingEventRecipients is keyed by runId; recipients are registered only for runs initiated by (or active in) the requesting connection's session, mirroring the existing tool-events ownership model.

Repro + Verification

Environment

  • OS: Linux / macOS / Windows
  • Runtime: Node 22+ / Bun
  • Model/provider: Any provider with reasoningLevel: "stream" (e.g. Anthropic claude-3-7-sonnet with extended thinking, or DeepSeek-R1 via OpenRouter)
  • Integration/channel: WebSocket gateway client
  • Relevant config: reasoningLevel: "stream" in session or via /reasoning stream directive

Steps

  1. Connect a WebSocket client with caps: ["thinking-events"].
  2. Send a chat message to a session whose model supports extended thinking and has reasoningLevel: "stream".
  3. Observe the agent events arriving on the connection.

Expected

  • Events with stream: "thinking" arrive in real time.
  • Each event's data.delta is the new incremental raw thinking text (e.g. " because", not the full accumulated string).
  • Each event's data.text is the fully formatted accumulated thinking string (e.g. "Reasoning:\n_Because it helps_").

Actual (before fix)

  • No stream: "thinking" events arrived at all for gateway WebSocket clients.
  • When thinking events did fire (e.g. via typing-signal path), delta equalled text on every event after the first full-text comparison failed due to markdown wrapping.

Evidence

  • Failing test/log before + passing after: streams native thinking_delta events and signals reasoning end in pi-embedded-subscribe.subscribe-embedded-pi-session.subscribeembeddedpisession.test.ts now passes; previously failed with accumulated text corruption ("Reasoning:\n_Checking files_Reasoning:" instead of "Reasoning:\n_Checking files done_").
  • TypeScript type-check (pnpm tsgo) passes with no new errors introduced by these changes.
  • Lint (pnpm lint) passes clean.
  • Format (pnpm format:check) passes clean.

Clients with the thinking-events cap receive thinking messages~ <img width="1371" height="793" alt="image" src="https://github.com/user-attachments/assets/3d48db6a-6465-4a46-9bc7-bf319f5b5056" /> Clients already connected to OpenClaw without the thinking-events cap are not affected~ <img width="838" height="495" alt="image" src="https://github.com/user-attachments/assets/26fa9334-f104-4194-bc0b-0bd6a45ab2ec" />

Human Verification (required)

  • Verified scenarios: TypeScript compilation, oxlint, oxfmt, targeted vitest run for the subscribeembeddedpisession suite.
  • Edge cases checked: (a) first thinking event where prior = ""delta = full first chunk, accumulated = full first chunk; (b) accumulated full-text update where raw text is a proper prefix of new raw text → delta = suffix, accumulated = new text; (c) pure incremental chunk (native thinking_delta fallback) where raw chunk does NOT start with prior accumulated → delta = chunk, accumulated = prior + chunk; (d) runs with isControlUiVisible: false → thinking events are skipped entirely.
  • What you did not verify: live end-to-end test against a real streaming Anthropic model; multi-connection race scenarios; iOS/Android/macOS app cap negotiation.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes — clients without thinking-events in caps are unaffected; they simply stop receiving the thinking-stream tokens they were previously getting via broadcast (which was unintentional).
  • Config/env changes? No
  • Migration needed? No — clients that want thinking events add "thinking-events" to their caps array.

Risks and Mitigations

  • Risk: thinkingEventRecipients registry entries accumulate if markFinal is not called on error paths.
    • Mitigation: markFinal is called for both lifecyclePhase === "end" and lifecyclePhase === "error", matching the existing toolEventRecipients cleanup pattern; the TTL-based pruning in createThinkingEventRecipientRegistry provides a backstop.
  • Risk: Clients using the existing general broadcast to observe thinking tokens (e.g. custom integrations relying on undocumented behaviour) will no longer receive them without opting in.
    • Mitigation: The previous broadcast was never documented or intended; thinking-events cap is a straightforward one-line addition to the client connect payload.

extent analysis

Fix Plan

To implement the real-time thinking stream support in OpenClaw Gateway, follow these steps:

  1. Add thinking-events capability:
    • In src/gateway/protocol/client-info.ts, add THINKING_EVENTS: "thinking-events" to GATEWAY_CLIENT_CAPS.
    • Clients must declare this capability in their connect frame:
      {
        "connect": {
          "caps": ["thinking-events"],
          "reasoningLevel": "stream"
        }
      }
  2. Modify pi-embedded-subscribe.ts:
    • Remove the onReasoningStream guard in emitReasoningStream.
    • Emit raw delta/text to WS, keeping formatted output for onReasoningStream.
  3. Update server-chat.ts and server-runtime-state.ts:
    • Route stream: "thinking" events to registered connIds only (not broadcast).
    • Instantiate thinkingEventRecipients registry.
  4. Wire registry through context in server.impl.ts:
    • Add registerThinkingEventRecipient to context interface in server-methods/types.ts.
  5. Register connId on run start:
    • In server-methods/chat.ts and server-methods/agent.ts, register connId when the thinking-events cap is declared.
  6. Add reasoningDefault to agent config:
    • In config/types.agent-defaults.ts, add reasoningDefault to AgentDefaultsConfig.
    • In config/zod-schema.agent-defaults.ts, add Zod validation for reasoningDefault.
  7. Update directive handling and reply logic:
    • In auto-reply/reply/directive-handling.levels.ts and auto-reply/reply/get-reply-directives.ts, read agentCfg.reasoningDefault as fallback.

Example Code Snippets

// src/gateway/protocol/client-info.ts
export const GATEWAY_CLIENT_CAPS = {
  //...
  THINKING_EVENTS: "thinking-events",
};

// src/agents/pi-embedded-subscribe.ts
function emitReasoningStream(delta, text) {
  // Removed onReasoningStream guard
  emitAgentEvent({ stream: "thinking", data: { delta, text } });
}

// src/gateway/server-chat.ts
function handleThinkingEvent(event) {
  const recipients = thinkingEventRecipients.get(event.stream);
  if (recipients) {
    recipients.forEach((connId) => {
      // Send event to registered connIds
      sendEventToConnId(connId, event);
    });
  }
}

Verification

To verify the fix, test the following scenarios:

  • Clients declaring the thinking-events capability receive stream: "thinking" events in real-time.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [Feature]: support cap thinking-event when websocket connect [2 pull requests, 1 comments, 1 participants]