* fix(acp): record cancelled background turns as cancelled, not succeeded
A cancelled parented ACP turn resolves without throwing (only failed turns
throw), so the turn-runner success path recorded the detached task as
succeeded. Thread the terminal status out of consumeAcpTurnStream and branch
at the runner; widen markBackgroundTaskTerminal to accept cancelled, routed
through the existing failTaskRunByRunId sink. The runTurn fallback infers
cancellation from done.stopReason, mirroring the acpx adapter.
* fix(acp): narrow cancelled task status handling
Co-authored-by: masatohoshino <g515hoshino@gmail.com>
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Tracks per-skill usage from the skill.used diagnostic event (trusted-only
delivery, file-scoped identity), sweeps workshop-created skills daily from
gateway maintenance (active -> stale 30d -> archived 90d, pinned bypass,
restore-only unarchive, files never touched), filters archived skills from
snapshots fail-open, reports workspace-scoped overlap candidates, and adds
openclaw skills curator CLI, additive gateway methods, and a warn-only
doctor finding. Zero new config keys; SQLite/Kysely state only.
* fix(agent-model): omit synthesized maxTokens fallback (Fixes#98295)
The configured-fallback resolver used DEFAULT_CONTEXT_TOKENS (200_000) as
the last-resort value for model.maxTokens when no configured, provider,
or bundled-catalog value was known. For strict OpenAI-compatible providers,
model.maxTokens is forwarded as max_completion_tokens on every request,
so the synthesized 200k value became a wire-level output cap that exceeded
the provider ceiling and produced HTTP 400 (Param Incorrect) on providers
like Xiaomi MiMo (131_072 max) — reported for mimo-v2.5 / mimo-v2.5-pro
added via 'models add' on a custom provider entry.
The sibling applyConfiguredProviderOverrides path already omits maxTokens
when nothing resolves; align resolveConfiguredFallbackModel with that
behavior. When the transport sees an undefined model.maxTokens, it omits
max_completion_tokens entirely and the provider applies its own default,
which is the correct behavior when the user has configured no output limit.
contextWindow retains DEFAULT_CONTEXT_TOKENS as its local budgeting
fallback (it is not shipped on the wire).
* test(agents): prove unknown output cap omission
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Compaction failure and timeout recovery can no longer leave a session transcript assistant-last and wedged after restoring pre-compaction state.
Transcript-continuation failures now carry a typed agent-core error code and no longer demote the active model through model fallback scoring.
Surface: embedded agent runner compaction recovery, model fallback classification, packages/agent-core.
Refs #100312. Includes regression coverage for #99943.
Stalled sessions could leave a session lane occupied by an orphaned active task after the embedded run handle was gone. Diagnostics repeatedly reported the stuck state, but the lane stayed held and queued turns could remain blocked indefinitely.
Release ownerless active lane tasks after the stale threshold, while guarding against fresh lane work and raised compaction safety windows so recovery does not double-run serialized session work or overlap transcript compaction. Stalled model-call recovery also no longer requires an in-process embedded run handle, so no-handle stalls can be reclaimed by the existing conservative recovery path.
Refs #99847.
Refs #94650.
Wedged model calls could hold an agent session lane for the normalized max timer duration when the user disabled the run timeout. In production that path resolves to MAX_TIMER_TIMEOUT_MS, so a zero-progress model call could block sibling work for about 24.8 days instead of failing through the normal terminal outcome path.
Cap the lane watchdog at the default agent deadline plus the existing grace window while preserving explicit shorter run timeouts. The disabled/max-timeout case now releases the lane at the default 48h deadline instead of inheriting the timer sentinel.
Refs #97588.
Refs #94650.
* fix(chat.abort): pass stored sessionId to match active embedded runs
The chat.abort RPC path was missing sessionId in the abort resolver
call, unlike the /stop path which passes entry?.sessionId. This meant
resolveAuthorizedRunsForSessionKeys could not match active runs
registered by sessionId, causing chat.abort to report aborted:false
while the tool subprocess continued running.
Fix: load the session entry in the chat.abort handler and pass
entry?.sessionId to abortChatRunsForSessionKeyWithPartials, giving
it the same match dimension that /stop already uses.
* test(gateway): tighten chat abort session matching proof
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* test(qa): canonicalize channel routing scenarios
* test(qa): enforce Crabline actor allowlists
* test(qa): preserve canonical flow integration
* test(matrix): use retained topology fixture id
* test(qa): preserve WhatsApp live defaults
* QA: declare channel transport policy in scenarios
* refactor(qa): keep transport policy type local
* refactor(qa): keep transport policy on leaf contract
* test(matrix): use retained routing fixture
* fix(gateway): don't over-claim a crash on a 1006 abnormal close
A 1006 close code means abnormal closure with no close frame, most often a
connection dropped under a burst of concurrent tool calls or event-loop
saturation — the gateway process typically stays healthy. The troubleshooting
text listed "Gateway crashed or was terminated unexpectedly" as a co-equal
cause, so an operator could pattern-match it and restart a healthy gateway.
Lead with the actionable dropped-under-load cause and demote the
unreachable/terminated case to "rare; confirm it is still running".
Addresses ask 3 of #100941. The connection-pooling and concurrency-cap asks
are architectural and intentionally out of scope, so this does not auto-close
the issue.
Refs #100941
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(gateway): keep 1006 diagnostics cause-neutral
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(web-shared): bound response.text() fallback to honor maxBytes
readResponseText's .text() fallback path ignored the maxBytes option,
reading the full body unbounded. When a foreign Response lacks a body
stream (no getReader) and no arrayBuffer(), the .text() call was the
last resort — but maxBytes was never applied.
- Honor maxBytes in the .text() fallback: truncate + set truncated=true
- Under-cap responses pass through unchanged
- No maxBytes set → original behavior preserved (regression safe)
* fix(web-shared): use byte-level truncation in .text() fallback
Replace text.length/text.slice (character-level) with TextEncoder/
TextDecoder (byte-level) so the maxBytes cap is respected at byte
granularity, consistent with the bounded-stream path at lines 240-298.
- bytes.byteLength > maxBytes instead of text.length > maxBytes
- TextDecoder().decode(bytes.slice(0, maxBytes)) for byte-safe truncation
- bytesRead reports actual bytes, not character count
* fix(web-shared): refuse unbounded .text() when maxBytes is set
When maxBytes is set the caller expects bounded memory. The .text()
fallback path buffers the full body before any post-read check,
defeating the cap. Fail-closed by returning empty with truncated=true
instead of calling .text() unbounded.
Without maxBytes .text() is safe — the caller accepts full allocation.
* fix(web-shared): byte-level truncation in .text() fallback with maxBytes
The .text() fallback used text.length (characters) and text.slice
(char-level) against a byte-level maxBytes cap. Use TextEncoder/
TextDecoder for byte-accurate comparison and truncation.
- bytes.byteLength > maxBytes instead of text.length > maxBytes
- TextDecoder().decode(bytes.slice(0, maxBytes)) for byte-safe truncation
- bytesRead reports actual byte count, not character count
* fix(web): fail closed on unbounded response fallbacks
* test(web): use streaming response fixtures
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Budget context engine assembly against the reserve and rendered prompt
pressure, and carry the preflight estimated prompt tokens, prompt budget,
and overflow tokens into the outer overflow recovery loop so compaction
engines compact against the prompt OpenClaw actually rendered instead of
a minimally over-budget guess.
* fix(edit): show candidate lines on oldText match failure
When edit tool cannot find oldText, include up to 3 near-match candidate
lines with similarity scores and line numbers. Caps input size (MAX_LINES
3000, MAX_LINE_LEN 200) to avoid unbounded CPU on large files. Reuses
existing levenshteinDistance helper.
Closes#97032
* fix(edit): use toSorted instead of sort for candidate ranking
Fixes lint violation: unicorn(no-array-sort)
* fix(edit): keep candidate truncation branch-compatible
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
sessions.patch becomes a params-aware dynamic-scope method: operator.write
now authorizes patches that touch only user-level chat-organization fields
(label, category, pinned, archived, unread); every other field, mixed
patches, and unknown keys keep requiring operator.admin (fail closed).
This unblocks the session controls shipped in #100814 for the Android app,
whose bounded operator session intentionally never requests operator.admin.
Also: session list ordering gains a deterministic key tiebreaker for equal
pinnedAt/updatedAt (stable offset paging and prompt-cache friendliness), a
user-assigned label now beats stored channel-derived display names in the
row projection so renames survive refreshes, and subagent spawn pins its
gateway calls to admin via the params-aware least-privilege resolver
instead of the static admin-only method check (#59428 contract preserved).
Refs #100712
* fix: require video media provider models
* fix video media test typing and skip coverage
* fix(media-understanding): preserve self-defaulting video providers
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>