* fix(opencode-go): re-arm idle timer on block-boundary events to prevent false stalled-stream abort
When the opencode-go model finalizes a tool call and deliberates before
the next one, the provider emits real block-boundary SSE events
(text_end, thinking_end, toolcall_start, toolcall_end) that prove the
socket is alive, but the watchdog's isProviderProgressEvent only
returned true for token deltas (text_delta, thinking_delta,
toolcall_delta). This caused the idle timer to fire and falsely abort a
live stream, replacing a completed answer with a stalled error and
dropping the provider's real done event.
Fix: include block-boundary events in isProviderProgressEvent so the
idle timer is re-armed on any forward-progress provider event.
text_start and thinking_start are intentionally excluded because they
are synthetic preamble events that should not shorten the first-event
window.
Closes#96518
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(opencode-go): satisfy lint in stream regression
* test(opencode-go): satisfy lint in stream regression
* test(opencode-go): satisfy lint in stream regression
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(provider-transport-fetch): bound SSE buffer to prevent OOM
* fix(provider-transport-fetch): appease oxlint curly rule in test
* fix(provider-transport-fetch): drain events before cap + cancel reader on overflow
* fix(provider-transport-fetch): remove unused encoder from coalesced chunk test
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(transport): tighten SSE buffer guards
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(openai-chatgpt-responses): bound streaming success-body SSE reads at 16 MiB
Apply createSseByteGuard to src/llm/providers/openai-chatgpt-responses.ts
(parseSSE) so the ChatGPT Responses SSE parser cannot be exhausted by a
hostile or malfunctioning endpoint that streams an unbounded SSE body.
The 16 MiB cap matches the non-streaming readProviderJsonResponse cap.
What changed:
- src/llm/providers/openai-chatgpt-responses.ts: wrap body.getReader() in
createSseByteGuard before the existing parseSSE loop. The function is
also re-exported as parseSSEForTest for direct test access.
- Inline test added to openai-chatgpt-responses.test.ts: hostile 1 MiB
pull stream, asserts canonical overflow message, cancel(reason) was an
Error instance, pullCount bounded to 17-20.
Reuses the createSseByteGuard helper from src/agents/streaming-byte-guard.ts
(introduced in #96701 for the anthropic-transport-stream path; same
helper, same 16 MiB cap, same overflow-error shape). This PR is the
third of the planned per-surface rescue series for the previously closed
PR #96666, after PR #3b (#96723).
No SDK surface change. No repro script committed (proof in this body).
* fix(openai): bound ChatGPT error body reads
* fix(openai): bound chatgpt error parsing
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(agents): truncate exec command detail on code-point boundaries
compactRawCommand in src/agents/tool-display-exec.ts middle-truncated the
one-line command with raw String.prototype.slice. When the head or tail
boundary fell between the two UTF-16 code units of a surrogate pair (e.g. an
emoji like U+1F600), the slice kept a lone surrogate, which renders as the
replacement character in the tool-call summary shown in chat/transcripts.
Use the existing sliceUtf16Safe helper for both ends so the boundary falls on
a code-point boundary, dropping the whole emoji instead of half of it. This is
behavior-preserving for non-surrogate input.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor: move surrogate-safe slice helpers to browser-safe module
ClawSweeper flagged that importing sliceUtf16Safe from src/utils.ts into
tool-display-exec.ts pulls node:fs/os/path into the Control UI browser-shared
bundle (ui/vite.config.ts treats tool-display-exec.ts as browser-shared).
Move sliceUtf16Safe/truncateUtf16Safe into a self-contained, dependency-free
module src/shared/text/surrogate-safe-slice.ts. src/utils.ts re-exports them
(zero churn for existing Node-side callers), and tool-display-exec.ts now
imports directly from the node-free module so no Node built-ins can leak into
the browser bundle.
* fix(agents): use shared utf16 helper in exec display
---------
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
registerNodesCli unconditionally registered plugin CLI commands, so
lightweight built-in commands like `nodes status`/`nodes list` paid the
full plugin CLI/runtime load cost. Only resolve plugin-provided node
subcommands (e.g. `nodes canvas`) when the invoked subcommand is not
already a built-in, keeping the built-in nodes path fast.
Fixes#96697
* fix(googlechat): replace unbounded response.json() with readProviderJsonResponse
Replace the local readGoogleChatJsonResponse and
readGoogleChatCertsResponse wrappers with the existing SDK helper
readProviderJsonResponse (from openclaw/plugin-sdk/provider-http) so the
Google Chat API JSON responses are bounded at 16 MiB, matching the
non-streaming cap already used by 15+ other extensions.
What changed:
- extensions/googlechat/src/api.ts: readGoogleChatJsonResponse now
delegates to readProviderJsonResponse. Removed the local try/catch
wrapper.
- extensions/googlechat/src/auth.ts: readGoogleChatCertsResponse now
delegates to readProviderJsonResponse. Error message preserved.
Removed the local try/catch wrapper.
This PR applies the same pattern as Alix-007's #96042, #96038 (lmstudio,
provider JSON reads). No SDK promotion needed — readProviderJsonResponse
is already available in openclaw/plugin-sdk/provider-http.
* fix(googlechat): add inline bounded-read regression tests
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(googlechat): remove unused variable flagged by oxlint
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(googlechat): bound api error body reads
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(agents): truncate tool-display detail on code-point boundaries
coerceDisplayValue truncated a long first-line detail value with raw
UTF-16 slice() at half = floor((maxStringChars-1)/2). When an emoji
(surrogate pair) straddles the cut boundary, the head kept a lone high
surrogate and the tail could begin on a lone low surrogate, rendering as
the replacement character. Use the existing sliceUtf16Safe helper so the
whole code point is dropped at the boundary, matching the UTF-16-safe
truncation used elsewhere in the repo. Behavior-preserving for
non-surrogate input.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(build): import surrogate-safe slice from node-free leaf module
tool-display-common.ts is in the UI browser bundle graph; importing
sliceUtf16Safe from utils.js dragged node:fs (via infra/fs-safe) into the
bundle and broke build-artifacts/QA Smoke. Extract sliceUtf16Safe/truncateUtf16Safe
into src/shared/utf16-slice.ts (dependency-free) and re-export from utils.js to
preserve the existing import surface.
* fix(build): import surrogate-safe slice from node-free leaf module
* fix(build): import surrogate-safe slice from node-free leaf module
* fix(build): import surrogate-safe slice from node-free leaf module
---------
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(mcp): emit image content with base64 source
* fix(mcp): keep plugin tool images in SDK schema
* test(mcp): exercise image bridge end to end
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Matrix thread-starter previews truncated long bodies by raw UTF-16
slice, which could cut an astral character (e.g. emoji) and leave a lone
surrogate, rendering mojibake in the agent's thread context.
Reuse the existing sliceUtf16Safe helper so the cut backs up to a valid
surrogate boundary, preserving the 500-code-unit limit and '...' suffix.
Adds a regression test that fails against the raw-slice implementation.
Salvages the original fix from #96407 (auto-closed by the active-PR
queue cap). Preserves @ly-wang19's authorship; rebased clean onto main
by @Bartok9.
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
* fix(auth): suppress recovery hint for format failures
* test(auth): cover format failure recovery copy
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>