In messages.visibleReplies "message_tool" sessions, a successful agent turn that produced a substantive private final without calling message(action=send) previously left the user with silence and only an operator log. The gateway now enqueues one protected front-of-queue retry prompting the model to deliver the reply, and falls back to a sanitized visible diagnostic when the retry cannot be enqueued or also strands. Queue overflow protection is unified with the in-flight-aware drop policy (skip in-flight or protected items, reject when nothing is droppable), rejected overflow no longer refreshes the drain debounce, heartbeat turns are excluded from recovery, and recovery retries no longer share the client turn's queued-turn lifecycle.
Fixes#85714
Thanks to Eva (@100yenadmin) for the contribution.
* fix(agents): keep model fallback turn-local instead of persisting over user pins
* fix(telegram): use live config snapshots per operation
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* test(telegram): fix config snapshot type coverage
---------
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* fix(agents): recover xAI/Grok "could not decrypt encrypted_content" 400 instead of tripping the circuit breaker
openclaw already strips a stale reasoning replay and retries the Responses call, but the
recovery is gated on isInvalidEncryptedContentError(), which only recognizes the
`invalid_encrypted_content` / `thinking_signature_invalid` codes/messages. xAI/Grok returns
a prose 400 with no error code — "Could not decrypt the provided encrypted_content. Ensure
the value is the unmodified encrypted_content from a previous response." — so the matcher
returns false, the call fails, and the per-model circuit breaker trips, blocking ALL
grok-4.3 traffic through the gateway until manual intervention.
Match that message (contains `encrypted_content` and a decrypt-failure phrase) so the
existing strip-and-retry path handles it too. Narrow enough to avoid unrelated
"could not decrypt" messages (e.g. the OAuth sidecar warning), which do not mention
`encrypted_content`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(agents): narrow xAI decrypt retry detection
Co-authored-by: rvdlaar <rvdlaar@gmail.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(gateway): re-check session runtime model against current agent defaults after hot-reload
Agent model hot-reload silently did not take effect because session entries
cached modelProvider/model from agent defaults during reset, and the resolver
returned these cached values before checking current config.
Fix (three-pronged):
1. Reset-side: only cache modelProvider/model when the resolved model came
from a user override — default-derived values are no longer persisted.
2. Resolver-side: when runtime metadata exists without overrides and an
agentId is available, the values are default-derived and may be stale —
skip them in the persisted-model fallback so current config defaults win.
3. Inheritance: only inherit runtime model metadata from parent when it
carries explicit user overrides (align with reset-side contract).
Reset response includes resolvedModel so API and TUI consumers always get
the effective model identity.
Fixes#102269
* fix(gateway): restore truncateUtf16Safe and emoji-boundary title test per ClawSweeper review
* fix(gateway): only skip stale session runtime model metadata when it actually differs from current defaults
The previous change unconditionally skipped cached modelProvider/model when no
user overrides were present and an agentId was available, assuming it was always
stale. This broke sessions that legitimately had non-default models set through
normal session creation (e.g. custom vision models).
Now the resolver resolves the current agent default first and compares: if the
cached runtime metadata matches the current default it is returned directly
(not stale); only when it differs is it treated as stale and re-resolved.
Also updates tests that set modelProvider/model without overrides to configure
their agent defaults so the expected model matches the resolution result.
* fix(session-model-ref): add stale-metadata detection for config hot-reload
* fix(test): remove strict timeoutMs assertion in provider catalog live-runtime test
The remainingTimeoutMs calculation can be off by 1ms depending on timing
(Date.now() - startedAt = 1ms on fast CI runners), causing a flaky failure.
This assertion is not the test's focus — dedicated timeout behavior is already
covered by 'uses one timeout budget across paginated live catalog discovery'.
* fix: restore AVATAR_MAX_BYTES to 2MB (revert accidental merge contamination)
* fix(gateway): resolve session models from current config
---------
Co-authored-by: Peter Steinberger <peter@steipete.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(opencode-go): remove deprecated mimo-v2-omni and mimo-v2-pro model aliases
These deprecated aliases reject agent requests from the OpenCode Go gateway.
Remove them from the provider catalog and clean up all references in probe
skip lists, CI workflows, and tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(opencode-go): complete deprecated MiMo cleanup
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(agents): use Buffer.byteLength for bash output rolling buffer accounting
The outputBytes variable tracks the rolling output buffer size for
bash command execution, but it used string .length (UTF-16 code units)
instead of Buffer.byteLength (UTF-8 bytes). When command output
contains multi-byte UTF-8 characters (emoji, CJK, etc.), the .length
undercount causes the rolling buffer to exceed maxOutputBytes.
Replace .length with Buffer.byteLength() at both increment and
decrement sites to correctly track byte-level buffer size.
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(agents): unify bash output accumulation
* refactor(agents): unify bash output accumulation
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* [AI] fix(openai): preserve native tool calls from clean streams without finish_reason
OpenAI-compatible providers that emit delta.tool_calls during streaming but
terminate with data: [DONE] without a final finish_reason chunk (e.g. Evolink
DeepSeek V4) have their tool calls silently stripped.
Introduce sawNativeToolCallDelta (structured provider intent) and
sawStreamDONE (exact SSE data: [DONE] detection via TransformStream).
Pass sawStreamDONE as a getter so the live value is read after stream
consumption. Promotion requires (sawStopFinishReason || (sawNativeToolCallDelta
&& sawStreamDONE?.())). SSE parsing uses line-boundary-aware regex; [DONE]
inside tool arguments or content does not match.
EOF without [DONE] remains fail-closed. DSML still requires sawStopFinishReason.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes#97994
* [AI] test(openai): add fetch-wrapper loopback tests for [DONE] detection proof
Prove sawStreamDONE works through the full transport chain:
- Local HTTP server → TransformStream → OpenAI SDK → processOpenAICompletionsStream
- [DONE] without finish_reason → promoted to toolUse
- EOF without [DONE] → fail-closed (tool calls stripped)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: harden clean SSE terminal detection
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(agents): recover claude-cli warm-stdin continuity when no native transcript is written
The headless warm-stdin claude-cli backend (liveSession: "claude-stdio")
never writes a native transcript, so the post-turn flush probe always
fails and the missing-transcript reuse path drops the bound session id.
Part 1 (cli-runner.ts): scope the non-destructive binding behavior to
warm-stdin sessions so they keep their binding instead of clearing it
every turn.
Part 2 (attempt-execution.ts): on a missing transcript, clear the stored
binding (no stale --resume) but still return the bound id as the reuse
candidate so prepare can re-detect the missing transcript and arm
raw-transcript reseed. Returning undefined starved reseed and lost
warm-stdin continuity.
Adds/updates regression coverage in attempt-execution.cli.test.ts and a
complementary reseed test in prepare.test.ts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(agents): align warm-session continuity coverage
* test(agents): keep cli live-session mock complete
* fix(agents): respect stateless CLI session mode
* style(agents): keep session candidate guard focused
* test(agents): preserve minimal CLI runner fixtures
* fix(agents): preserve exact Claude warm sessions
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(cron): preserve cron context in session entry for async completion wakes (#99919)
Persist bootstrapContextRunKind on the session entry after cron agent
runs, and restore provider/model/thinking/runKind from the session
entry into directAgentParams when an async completion wake (e.g. media
generation) resumes a cron session.
Before this fix, async completion wakes lost the original cron run
context and fell back to account defaults, causing multi-step cron tasks
to silently dead-end after the first async media generation call.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cron): authorize model override on trusted in-process dispatch for cron context restoration
* fix(cron): persist bootstrapContextRunKind on base session entry for async completion wakes
* fix(cron): route async completion wake to base cron session key instead of ephemeral run key
When the cron run-key fallback fires, record the base cron key as
effectiveSessionKey so deliverDirectSubagentAnnounce can route the
agent call to the persisted session row with its transcript and task
context instead of starting a fresh turn (#99919).
* fix(cron): add bootstrapContextRunKind to session entry slot keys and declare loadRequesterSessionEntry return type
* fix(cron): rebase onto upstream main, restore upstream agent-command behavior while keeping bootstrapContextRunKind persistence
* fix(cron): persist bootstrapContextRunKind from a pre-mutation snapshot to produce a real delta
* fix(cron): move pre-mutation snapshot inside guard so initialEntry is non-optional
* fix(cron): preserve async media continuation context
Co-authored-by: Peter Lee <li.xialong@xydigit.com>
* fix(cron): retain continuation after base cleanup
* test(gateway): complete agent scope mock
* test(gateway): preserve agent scope exports
* test(cron): persist mocked session mutations
* test(cron): model persisted session fixtures
* fix(agents): snapshot cron continuation retries
* fix(cron): harden async continuation settlement
* test(cron): control continuation recovery clock
* fix(gateway): narrow continuation model row
* chore(changelog): defer cron continuation note
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(agents): isolated cron busts prompt prefix cache via per-run session id
Isolated cron runs carry a per-run :run:<id> session scope (#91685) rendered
verbatim into the cached system-prompt Runtime line, re-busting byte-exact
prefix caching for the tool catalog after it every run (#96677, #43148 class).
buildRuntimeLine now renders the stable base session key and drops the per-run
id the run scope duplicates; parseCronRunScopeSuffix is gated to the
isolated-cron key shape so a :run: segment in any other session key is never
truncated.
* fix(agents): preserve mixed-case cron run markers
* test(agents): cover rotated cron session identity
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(usage): route claude-cli to Anthropic plan usage with plan label and billing
* feat(webchat): redesign context popover with plan-usage bars and API-cost gating
* fix(usage): match plan usage across CLI provider aliases and source plan label from synced auth profile
* docs(plugins): document plan metadata on usage auth token
* chore(usage): document provider-level billing attribution limits, fix overview test types
* fix(webchat): avoid map-spread in quota group collection
* test(webchat): deflake subscription popover reset fixture
* test: shorten loopback token fixtures below secret-scanner thresholds
* feat: carry gateway client capabilities through the CLI loopback backend
CLI-backed model runs now transport the originating client's declared
capabilities along the existing per-field loopback contract: RunCliAgentParams
gains clientCaps, prepare emits OPENCLAW_MCP_CLIENT_CAPS, the loopback header
template forwards x-openclaw-client-caps, the request context parses and
normalizes it (grant-authenticated callers keep ignoring spoofable headers),
and the loopback tool cache keys on a stable caps serialization so capless and
capped requests never share tool lists. Capability-gated tools such as
show_widget now work on CLI backends; the CLI session binding hash includes
caps, consistent with messageProvider. Cron and command-attempt runs stay
capless (fail closed).
Fixes#102577
* fix(agents): allow model fallback when takeover wrapper holds classifiable promptError
When EmbeddedAttemptPromptErrorWithCleanupTakeoverError wraps a real
provider-level failure (e.g. timeout, rate_limit) inside .promptError but
inherits the name "EmbeddedAttemptSessionTakeoverError", the fallback
classifier isNonProviderRuntimeCoordinationError aborts the fallback chain
without checking whether the underlying cause is a retryable provider error.
Fix: in isNonProviderRuntimeCoordinationError, check err.promptError before
returning true for takeover-named errors. If promptError is a classifiable
provider failure (timeout, rate_limit, etc.), let fallback proceed — the
takeover is a cleanup side-effect, not the root cause.
Tests:
- Unit tests for the classifier: timeout, rate_limit, unclassifiable,
and pure takeover (regression)
- Integration tests for the runWithModelFallback pipeline: timeout
and rate_limit promptError both produce correct fallback and reason;
pure takeover still aborts
Related to #99963
* fix(agents): carry preserved prompt failures
* fix(agents): keep pure takeovers fatal
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>