sessions.delete gains an explicit archivedOnly param (additive protocol
schema change, Swift models regenerated): with archivedOnly=true, the
dispatcher grants operator.write and the handler requires the target to
already be archived (archive-then-delete keeps destructive intent a
two-step action). Without the flag nothing changes: deletes require
operator.admin, so internal fallback/synthetic dispatch, subagent cleanup,
and CLI minting keep their admin contracts, and the session-kill HTTP
endpoint pins admin explicitly since it terminates live runs.
Android sends archivedOnly and offers Delete only on archived rows, where
its bounded (non-admin) operator session is now authorized; active rows
archive first. iOS/web connect with admin and keep unrestricted deletes.
Refs #100712
* fix(backup): close archive stream before retry cleanup
Own the tar source and file writer with stream.pipeline so live-file EOF failures close the partial archive before Windows cleanup. Keep one native Windows handle-release regression and focused CI coverage.
Fixes#101382
Co-authored-by: 徐闻涵0668001344 <xu.wenhan1@xydigit.com>
* test(backup): track archive retry temp directory
* test(backup): model tar archive streams
* test(backup): model retry cleanup streams
---------
Co-authored-by: 徐闻涵0668001344 <xu.wenhan1@xydigit.com>
- 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(status): surface auto-fallback model in status and session_status (#96126)
When a session falls back to an alternate model via auto-fallback
(modelOverrideSource: "auto"), both `openclaw status` and `session_status`
silently showed the active fallback model without indicating it differs
from the configured primary. The mismatch gate used
hasUserPinnedModelSelection() which returns false for auto-fallback.
- status.summary.ts: widen mismatch gate from hasUserPinnedModelSelection
to entry?.modelOverride != null; emit distinct "fallback selected"
reason alongside existing "session override"
- status.command-sections.ts: add fallback-specific wording ("auto
fallback" / "check provider availability") while keeping the
modelSelectionReason filter intact (no false-positive null-reason rows)
- status-message.ts: add sessionHasAutoFallback detection for the
session_status RPC path; show "auto fallback" / "check provider" label
instead of "pinned session" / "clear /model default"
Co-Authored-By: Claude <noreply@anthropic.com>
@
* fix(status): narrow fallback detection to real fallback provenance
Replace entry?.modelOverride != null with
hasSessionAutoModelFallbackProvenance(entry) to avoid mislabeling
configured subagent automatic model selections as provider fallback.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(status): distinguish configured models from fallback
Co-authored-by: LZY3538 <liu.zhenye@xydigit.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(gateway): persist agent request transcript media
Route inline and offloaded agent.request images through the canonical user-turn transcript recorder across embedded, CLI, and ACP runtimes. Share ordered media persistence with chat.send and cover media-only empty-reply turns.
Co-authored-by: Petros Dhespollari <info@peterdsp.dev>
* fix(gateway): avoid transcript media shadowing
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* 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>
* feat: add openclaw promos CLI for ClawHub promotional model offers
* fix: harden promos claim auth validation and sanitize remote promo text
* fix: enforce promo window client-side and validate slug contract
* fix: shell-safe model ref contract and explicit --api-key override
* fix: hold promotion aliases to the models alias contract
* docs: document argv-credential contract and env alternative for promos claim
* fix: enforce provider plugin enablement on the credential-reuse claim path
* fix: require plugin install before credential-reuse shortcut in promos claim
* fix: run runtime plugin repair on promo defaults and harden identifier parsing
* fix: distinct message for contract-invalid promo aliases
* fix(cli): validate promotion plugin contracts
* fix(cli): recheck promotion window before claim
* feat(cli): surface ClawHub promotions in models list via the hosted feed
Passive discovery for promotional model offers. A cadence-gated (24h),
fail-silent conditional GET of ClawHub's immutable promotions feed
snapshot (If-None-Match -> 304, short 2.5s timeout, unauthenticated so
CDN caches stay unfragmented) is cached in two new shared-state-DB
tables, fully separate from update_check_state. models list renders an
'Available via promotion' group for live offers whose models are not in
the user's configured set (including the zero-row fresh-install path),
tags claimed models promo / promo ended from provenance recorded at
claim time, and prints a one-time notice per newly seen offer; promos
list and claim mark offers as seen. Machine outputs stay clean, snapshot
sequence is monotonic against stale edges, and claims still revalidate
against the live API so the kill switch always wins.
* style: satisfy lint on promotions feed additions
no-useless-fallback-in-spread on the optional request headers and
no-map-spread in claim-provenance row mapping.
* fix(cli): harden promotions feed cache
* fix(cli): honor live promotion validity
* fix(ports): ignore malformed lsof listener pids
* fix(infra): strictly reject malformed lsof pid tokens
Number.parseInt truncated tokens like "111abc" to 111, silently accepting
a corrupted lsof pid record. Switch to parseStrictPositiveInteger so the
whole token must be a valid positive integer, and restore the deduped
listener test that a prior change had replaced instead of extended.
* fix(cli): strictly reject malformed lsof pid tokens
Harden the force-free lsof parser to reject digit-prefixed garbage like
p111abc instead of truncating it to pid 111, and cover the sibling parser
with a focused regression test.
* fix(infra): strictly parse stale lsof pid tokens
Harden the stale gateway cleanup parser to reject digit-prefixed garbage
like p111abc instead of truncating it to pid 111, and add a focused
regression test without dropping the existing argv verification coverage.
* fix(ports): fail closed on malformed lsof pids
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(media): allow aws-sdk auth mode for image and audio/video paths
Media understanding tools failed for amazon-bedrock deployments using
auth.mode "aws-sdk" (BYOK via role/SSO/profile creds). Each path called
requireApiKey, which throws on the empty-key sentinel before the AWS SDK
credential chain can resolve creds at call time.
The image path (image.ts) resolves auth via getApiKeyForModel; the
audio/video paths route through resolveProviderExecutionAuth. Both now
mirror the chat path's allowMissingApiKeyModes allowance: when the
resolved key is empty and the mode is aws-sdk, execute keyless and let
the SDK resolve credentials. The image path skips setRuntimeApiKey so no
empty-string secret is persisted; audio/video return the kind:"none"
execution auth so the runner bypasses key rotation.
Closes#72031
* fix(media): cover Bedrock PDF SDK auth
* fix(pdf): register plugin completion streams
* fix(media): recognize Bedrock SDK tool auth
* chore: move release note to PR
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
resolveSupportedThinkingLevelFromProfile sorts the supported levels descending,
then, when every supported level exceeds the request (e.g. a stored `off` on a
profile that has no off level), fell through to `ranked.find(non-off)` — the
first entry of a descending list, i.e. the most expensive level. A session
carrying thinking `off` that switched onto such a model was silently remapped to
maximum reasoning and persisted (src/auto-reply/reply/directive-handling.persist.ts).
Use `ranked.findLast(non-off)` so a below-range request resolves to the cheapest
non-off level instead. Regression test included (red before, green after).
This surfaces with the first bundled no-off thinking profiles (Fireworks
GPT-OSS 120B, see #92217), but the clamp lives in shared core logic, so it ships
on its own.
* fix(gateway): show last error when status probe fails
* fix(gateway): scope status log diagnostics
---------
Co-authored-by: wAngByg <wAngByg@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(tui): suppress adjacent history-replay/live-final duplicate in ChatLog (#96967)
When sessions.changed triggers loadHistory(), assistant rows are replayed
via finalizeAssistant(text) without a runId. A late live final with the
same text and a runId can land immediately afterward, producing a visible
duplicate assistant reply.
Track the most recent history-replay assistant component in ChatLog and
suppress a following live final when the text matches and no other
component was appended in between. This is simpler and more targeted
than the prior event-handler approach because it operates at the
rendering layer where the duplicate is actually visible.
Fixes#96967
* fix(tui): bound replay-final dedupe window in ChatLog
* fix(tui): bound replay-final dedupe marker with TTL expiration
* fix(tui): satisfy no-promise-executor-return lint rule in chat-log test
* fix(tui): replace ChatLog text heuristic with runId-based surrender tracking
Drop the lastHistoryAssistant text-matching dedupe in ChatLog
(posed P1 false-suppression risk). Instead, track active runIds
surrendered to loadHistory() on sessions.changed 'new' — late
events for surrendered runs are suppressed by exact runId match.
Add ChatLog.hasStreamingRun() so the event handler can detect
when loadHistory restored a surrendered run as in-flight streaming.
Retain chatFinalizedRuns / chatFinalizedRunsWithDisplay from the
original fix for post-reload delta/error dedup of finalized runs.
Incorporates the precise surrender-tracking approach from closed
PRs #96979/#96986 while keeping the scoped ChatLog API minimal.
* fix(tui): render displayable late final for surrendered non-streaming run
When a sessions.changed 'new' surrenders an in-flight run and
loadHistory does not restore it as streaming, a late displayable
final must still be rendered — otherwise the user never sees the
assistant reply. Only suppress non-displayable finals (empty
message, no errorMessage).
Add focused test for non-displayable final suppression.
* test(tui): add coverage for empty in-flight restore + late displayable final
Covers the rank-up scenario from #96979: surrendered run restored
by loadHistory as empty streaming, late displayable final passes
through surrender check and renders to chatLog.
* fix(tui): suppress late finals for surrendered finalized runs to prevent history-replay duplicate
When sessions.changed 'new' fires, now surrender chatFinalizedRuns entries
too (as 'finalized'), not just sessionRuns ('in-flight'). This prevents
the finalized-before-reload history-replay duplicate: a late final for an
already-displayed run gets suppressed because the history-replay static
row covers the visible content.
In-flight surrendered runs still render displayable finals because the
user may not have seen the streaming text before the reload.
SurrenderedToHistoryRunIds is now Map<string,'in-flight'|'finalized'>
so the surrender check can branch on source. Move surrender check before
the finalizedRuns/chatFinalizedRuns guard so surrendered finalized runs
are routed correctly.
Add focused tests for history-replayed-visible (suppress) and
history-replay-missing (render) late finals.
* fix(tui): extend surrender coverage to sessions.changed reset reload path
Reset also calls clearTrackedRunState() + loadHistory() with the same
sessionKey, so late chat events for runs from the old session window
can race with the history replay the same way as 'new'. Surrender
sessionRuns and chatFinalizedRuns on both reasons (#96979 P1 rank-up).
* fix(tui): keep surrendered finalized marker after late delta to block later final
For finalized surrendered runs, a late delta must not clear the
surrender marker — otherwise a subsequent late final would slip
through and produce a duplicate. Only in-flight surrendered runs
clear the marker on delta (their final needs to render).
* fix(tui): gate session history reloads on persistence
Co-authored-by: xialonglee <li.xialong@xydigit.com>
* docs(changelog): note TUI external-run dedupe
* test(tui): type history load harness results
* test(tui): defer history results without async mocks
* chore(changelog): defer TUI note to release
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>