- Raise typing-breaker default from 2 to 5 consecutive failures via a
named constant, keeping explicit overrides intact.
- Extend rich-plain-fallback trigger map to classify
RICH_MESSAGE_CONTENT_REQUIRED alongside existing invalid-entity
errors so both durable and streaming send funnels benefit.
- Add empty rendered-rich-HTML guards before sendRichMessage in both
the durable chunk loop (send.ts) and the streaming delivery funnel
(delivery.send.ts), preventing doomed Bot API calls and preserving
later valid chunks.
- Add regression tests for the default typing breaker, delivery-side
and durable-side RICH_MESSAGE_CONTENT_REQUIRED fallback behavior.
* fix(anthropic): resolve thinking as disabled when legacy budget is below 1024
When adjustMaxTokensForThinking collapses the thinking budget below the
Anthropic minimum (1024), option resolution now sets thinkingEnabled to
false instead of always forcing it to true. This keeps every downstream
consumer (payload, replay, temperature, tool-choice) consistent — they
all see the same disabled state instead of an enabled flag with a
missing or API-rejected thinking block.
|| → ?? in both builders is defensive: the resolution layer already
prevents invalid budgets from reaching the builder through the normal
path, but ?? preserves an explicit zero when the builder is called
directly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(anthropic): guard raw streamAnthropic builder path against sub-minimum budgets
Add budget guards in both Anthropic payload builders so direct
streamAnthropic and bundled-plugin callers (e.g. Mantle) that bypass
option resolution also get the disabled-state rule instead of producing
API-rejected { type: "enabled", budget_tokens: < 1024 } requests.
Add proof-anthropic-thinking-budget.mts driving real production
functions with terminal output and negative control.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(anthropic): normalize legacy thinking budgets
Co-authored-by: Pick-cat <huang.ting3@xydigit.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix: show exported tool results in trace viewers
* fix(diagnostics-otel): emit semconv response key and execute_tool identity so trace viewers show tool results
---------
Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
* feat(codex): scope app-server rate limits to the physical client
Replace the process-global rate-limit cache with a WeakMap keyed by the
physical app-server client, tracking per-limitId revisions. Rolling
account/rateLimits/updated notifications merge sparsely per the protocol
contract (credits/individualLimit/planType survive nulls), and
usage-limit errors only trust a snapshot for auth-profile blocking when
the same client observed a primary update during the failing turn's
startup. Fixes cross-client rate-limit bleed in usage-limit error
messages. A new client-runtime module installs the
account/chatgptAuthTokens/refresh handler and the rate-limit observer
once per physical client, replacing per-start inline handlers in
shared-client, run-attempt, and side-question.
* refactor(codex): split thread/resume subscription safety into thread-resume
Move the thread/resume request out of thread-lifecycle into a dedicated
thread-resume module that retires the exact physical client when resume
acceptance is indeterminate: only a structured RPC rejection proves
Codex holds no subscription, so any other failure abandons the client
instead of returning it to the shared pool. Resume responses naming a
different thread now fail closed (assertCodexThreadResumeSubscription),
and the fresh-thread fallback requires a released subscription unless
the resume was a proven RPC rejection.
* refactor(codex): replace client-factory positional DI with shared-client factory
Delete the lazy positional-argument CodexAppServerClientFactory and use
the options-object factory type exported from shared-client. Callers in
run-attempt, compact, bounded-turn, provider-capabilities, and the
web-search provider now default to getLeasedSharedCodexAppServerClient
directly; the lazy indirection was ineffective because those modules
already import shared-client statically.
* feat(codex): route app-server turn traffic through a keyed turn router
Install one turn router per physical app-server client and replace the
broad per-attempt notification/request fanout with explicit per-thread
routes. Attempt startup reserves the thread route (before thread/resume
on the resume path, so early notifications buffer instead of racing),
run-attempt activates it with receive-time, queued, and request
handlers, arms the route before turn/start, binds the accepted turn id
to flush buffered traffic in wire order, and releases the route on
cleanup. Requests for a pending turn wait for binding instead of being
auto-declined, native turn completion waits use route state instead of
scanning buffered notifications, and correlation readers now match the
canonical v2 wire shapes only (top-level threadId, nested turn.id). The
unscoped response-delta lease-count attribution and its client API are
deleted along with the retired correlation predicates.
* test(codex): reset the shared binding store between thread-lifecycle tests
SQLite bindings are keyed by session identity rather than the per-test
temp dir, so earlier tests leaked resumable threads into fresh-start
expectations. The old silent resume-failure fallback masked the leak;
subscription safety surfaces it.
* test(codex): reset the binding store between delivery-hint iterations
The loop reuses one session identity across iterations, so the previous
iteration's thread would resume against a harness that cannot serve it.
* refactor(codex): raise app-server floor to 0.142 and drop range-compat protocol paths
* refactor(codex): model subagent mirror state as one map
* style(codex): format event-projector
* test(codex): drop unused shared-client test import
* refactor(codex): drop v1-era notification field aliases
* fix(codex): teach models to load deferred native spawn_agent via tool_search
* docs(codex): realign harness config tables
* docs(changelog): note Codex app-server protocol update
* fix(telegram): add UND_ERR_CONNECT_TIMEOUT to PRE_CONNECT_ERROR_CODES
UND_ERR_CONNECT_TIMEOUT occurs during TCP/TLS connect handshake,
before any HTTP request data is sent. Adding it to the pre-connect
set allows isSafeToRetrySendError to safely retry sendMessage when
undici's connect timeout fires — the message was never transmitted.
* test(telegram): cover connect-timeout retry funnels
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(browser): direct extension→gateway relay path for remote Chrome (#53599)
Let the OpenClaw Chrome extension pair directly to a remote gateway over
wss:// with no OpenClaw node host on the browser machine — the managed-hosting
path from #53599 (extension is the only thing installed on the laptop).
- Gateway route /browser/extension registered by the browser plugin with
auth:"plugin" + no nodeCapability, so the gateway does not pre-enforce token
auth (browser WebSockets cannot send an Authorization header). The upgrade
handler self-validates the host-local relay secret from ?token=, origin-checks
chrome-extension://, resolves the extension profile, then attaches the socket
to the same ExtensionRelayBridge the loopback relay uses. All CDP synthesis,
tab-group scoping, and the in-process Playwright /cdp client are unchanged.
- `openclaw browser extension pair --gateway-url wss://host` prints a
wss://host/browser/extension#<secret> string; the path ends in /extension so
the extension's existing pairing parser accepts it with zero extension code
changes.
- relay-server: extract attachExtensionWebSocket + export requestToken /
isAllowedExtensionOrigin / EXTENSION_RELAY_MAX_PAYLOAD_BYTES so loopback and
gateway paths share one bind + one frame cap.
- runtime-lifecycle: dispose the shared gateway WebSocketServer on shutdown.
- docs: three remote topologies (same host / direct-to-gateway / via node host).
Coverage: 6 unit tests for the handler's path/503/403/404/401/attach branches.
The full extension→bridge→CDP→Chrome loop over /browser/extension was live-proven
with a real Chrome + the built extension. The real gateway upgrade→handleUpgrade
dispatch for an auth:"plugin" unprotected route is verified against core
(server-http.ts, plugins-http.ts, route-auth.ts).
* fix(browser): harden remote extension pairing
* fix(diagnostics-otel): surface error message on run/harness error spans
Errored openclaw.run / openclaw.harness.run spans only carried a
low-cardinality errorCategory (or a hardcoded "error"), so trace UIs
showed "outcome error" with no message. Thread the redacted error
message through run.completed / harness.run.completed / harness.run.error
onto an openclaw.error span attribute + span status, mirroring
recordWebhookError. The raw message stays off metric attrs to preserve
cardinality; support-bundle redaction covers the new error field by name.
* fix(diagnostics-otel): harden run failure telemetry
Co-authored-by: Alex Knight <aknight@atlassian.com>
* test(diagnostics-otel): assert wire-level redaction
* chore(changelog): defer release note to release process
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(codex): project guardianWarning circuit-breaker notification
Codex emits a `guardianWarning` notification from its rejection
circuit-breaker (e.g. 3 consecutive or 10 total denials in a turn)
right before ending the turn as interrupted. The app-server event
projector had no case for it, so the rejection-limit reason was
silently dropped and the turn degraded to a generic interruption.
Project it on the `codex_app_server.guardian` stream with phase
"warning", alongside the existing autoApprovalReview handling. The
exact v2 param shape is not bundled in OpenClaw's protocol schemas,
so the human-readable reason is read defensively.
Closes#101207
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(codex): route guardianWarning through thread-scoped filter
`guardianWarning` is thread-scoped — codex emits it with only
{message, threadId} (no turnId). The turn-strict pre-switch filter
therefore dropped it before it reached the projection switch, making
the handler added in the previous commit dead code for real traffic.
Correlate it on the thread alone (mirroring hook notifications) so it
reaches the switch, and read the `message` field per the codex schema.
The regression test now sends the real {threadId, message} shape
instead of a synthetic turnId, so it actually exercises the routing.
Refs #101207
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* repro(codex): add real behavior proof for guardianWarning projection
* fix(codex): narrow guardian warning projection
Co-authored-by: 曾文锋0668000834 <zeng.wenfeng@xydigit.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(imessage): handle stdout/stderr stream errors in the RPC client child
A dead imsg RPC helper can emit an async error on any of its stdio streams.
On a raw stream an unhandled 'error' event throws and surfaces as an
uncaughtException, crashing the gateway. #75438 added this guard for stdin
but left stdout/stderr — on the same long-lived child — unguarded.
Route stdout/stderr stream errors through the existing failAll path via a
shared failFromStreamError helper, mirroring the stdin handler. Add a
regression test that emits 'error' on each stream and asserts the child does
not throw and in-flight requests reject cleanly.
* fix(imessage): terminate failed RPC transports
* test(imessage): exercise real stream failure
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>