* fix(whatsapp): resolve abortPromise when stop signal already fired
waitForClose() races on abortPromise, but a pre-aborted gateway stop
signal never settled that promise because the abort listener was skipped.
Resolve abortPromise immediately when abortSignal.aborted is already true.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(whatsapp): resolve tsgo TS18048 and test mock in pre-aborted abortPromise fix
- Extract params.abortSignal to local const so tsgo can narrow the type
through else-if control flow, fixing TS18048: possibly undefined
- Remove setupAbortController/ownerAcquireAbortController.abort() from
the constructor's pre-aborted branch so openConnection() still works
when the stop signal was already fired at construction time
- Use createSocketWithTransportEmitter() mock in the new test so
shutdown() cleanup has sock.end() and ws.removeListener() available
* fix(whatsapp): preserve pre-aborted setup cancellation
* fix(whatsapp): unify connection abort handling
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(process): clarify command lane timeout cause
* fix(process): complete timeout cause formatting
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(openai): cancel realtime reconnect backoff on close
* test(openai): cover reconnect after bridge close
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(gateway): allow clearing agent model overrides
* fix(protocol): preserve Swift agent model compatibility
* fix(protocol): preserve null when Swift decodes agent updates
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(google-meet): reject nondecimal transcript cursors
Use parseStrictNonNegativeInteger for --since validation so hex,
exponent, and fractional spellings are rejected at the CLI boundary
instead of silently coercing to different cursor offsets.
Remove the redundant file header comment to stay within the
oversized-file LOC ratchet.
* test(google-meet): harden transcript cursor coverage
* style(google-meet): format cursor tests
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(usage-bar): cap warnedTemplateOverrides warn-once dedupe cache
Replace unbounded warnedTemplateOverrides Set with createDedupeCache(maxSize=256)
for consistency with the bounded fileCache Map (MAX_CACHED_TEMPLATE_FILES=64)
already present in the same file.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(usage-bar): bound invalid-template warnings
Co-authored-by: ZengWen-DT <ceng.wen@xydigit.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(imessage): cap per-chat group-allowlist warn-once cache
Replace unbounded perChatWarned Set with createDedupeCache(maxSize=512)
to keep long-running iMessage monitor memory stable. The cache grows
with every distinct group chat the gateway sees; without a cap it can
accumulate entries indefinitely.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(imessage): prove warning cache eviction
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(openai): stop double-prefixing SSE bodies mislabeled as JSON
OpenAI-compatible gateways that stream real SSE (data: {...}) but label the
response application/json hit the streaming JSON-wrap fallback, which re-prefixed
each frame as 'data: data: {...}' and broke JSON.parse in the OpenAI SDK. Sniff
JSON-labeled streaming bodies and relabel genuine SSE as text/event-stream so the
SSE sanitizer parses them verbatim.
* test(openai): prove mislabeled SSE stays streaming
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix: release stale reply work during reset cleanup
* fix: release archived reply runs during reset
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(agents): keep background exec exit notifications UTF-16 safe at the snippet cut
Background exec exit notifications truncate the command's tail output with a
raw String.slice, which can split a UTF-16 surrogate pair when an emoji lands
at the cut. Use the existing truncateUtf16Safe helper so the message delivered
to the channel never ends with a dangling surrogate half.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(agents): protect exec notify tail boundaries
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(agents): protect node exec follow-up tails
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(agents): type node exec follow-up assertions
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(agents): keep bounded exec output Unicode-safe
* docs(changelog): note exec output Unicode safety
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(browser): resolve act targetId aliases before mismatch check
The /act top-level and batch targetId guards compared the caller-supplied
targetId against the resolved canonical tab.targetId with raw string
equality. Any supported alias form (tabId, label, suggestedTargetId, or a
unique id prefix) resolves to a different canonical id, so act requests that
followed the documented 'prefer suggestedTargetId/tabId/label' guidance were
rejected with 403 ACT_TARGET_ID_MISMATCH even though they named the correct
tab. snapshot/open/close/tabs lack this guard and kept working, matching the
reported symptom matrix.
Resolve the action targetId through the same tab alias resolution the route
used and reject only ids that resolve to a different tab.
* fix(browser): canonicalize act targetId aliases before Playwright dispatch
The /act gate accepted tabId/label/suggested/prefix aliases of the request
tab but left them on action.targetId. The managed executor reads
action.targetId ?? targetId for an exact page lookup (executeSingleAction ->
getPageForTargetId), so an alias missed the lookup and broke the action at
runtime whenever more than one page was open (single-page masked it via the
pages.length===1 fallback). Canonicalize the action targetId (top-level and
nested batch sub-actions) to the resolved tab id before dispatch; reject ids
that resolve to a different tab. Replace the mock-masked contract assertions
with executor-action assertions plus direct canonicalizer unit tests.
* test(browser): brace act-targetId guard clauses for curly lint
* docs(changelog): note browser target alias fix
* docs(changelog): note browser target alias fix
* fix(browser): reject ambiguous batch target aliases
* test(browser): type targetless act fixture
* docs(changelog): move browser alias fix to unreleased
* chore: drop nonessential browser changelog entry
---------
Co-authored-by: Peter Steinberger <peter@steipete.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(status): bound systemd service probes so status cannot hang on a wedged systemctl (#84698)
* test(daemon): assert readRuntime receives the threaded status timeout opts (#84698)
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cron): preserve provider/model on isolated-run timeout row
* fix(cron): carry provider/model/session into timer-built timeout rows
The reporter's blank cron_run_logs row (cron: job execution timed out
(last phase: ...)) comes from the wall-clock/cancel outcome built in
executeJobCoreWithTimeout, not the inner run catch the prior commit
patched (that catch result loses the Promise.race). Carry the
already-resolved attribution from watchdog-visible execution state into
the timer-built timeout and post-runner cancel outcomes so the persisted
row keeps provider/model/session. Pre-runner setup timeouts stay blank.
Refs #95873
* fix(cron): attribute timeout-disabled isolated-run operator-cancel rows
The timeoutSeconds:0 branch in executeJobCoreWithTimeout has no watchdog,
so it returned createOperatorCancellationOutcome() with no execution data
and never wired the execution callbacks. An operator-cancelled isolated run
with timeouts disabled still wrote a blank provider/model/session row.
Track the resolved identity locally from the same onExecutionStarted/
onExecutionPhase callbacks and carry it into the cancel outcome, matching
the timed branch. Adds a focused regression with negative control (#95873).
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(message-tool): apply messages.responsePrefix to outbound sends
* fix(message-tool): interpolate responsePrefix templates on sends and skip unresolved model tokens
* fix(code-mode): surface QuickJS error name and message to the model
* fix(code-mode): preserve host error diagnostics
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(cron): clear agentTurn thinking override when patched with null
Cron agentTurn patches could clear model/fallbacks/toolsAllow overrides by
sending an explicit null, but thinking had no clear path: the patch schema and
normalizer dropped thinking:null before it reached the merge logic, and the
payload merge only handled string values. Blanking the Thinking/Effort field in
the Cron Control UI therefore silently preserved the old value.
Add thinking:null support across the patch schema, exported type, normalizer,
and payload merge (mirroring model). The Control UI now sends an explicit clear
for model/thinking when an edited job blanks a previously stored override, and
the CLI gains --clear-thinking for parity with --clear-model.
* docs(cron): document --clear-thinking beside sibling clear flags
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
Summary:
- The PR moves Gateway `chat.history` omission accounting to a whole-pipeline reporter and adds focused helper plus real WebSocket request regression tests.
- PR surface: Source +35, Tests +219. Total +254 across 4 files.
- Reproducibility: yes. Current-main source shows the zero-count keep-last helper branch and the positive-coun ... he PR body includes a negative-control real WebSocket run where the same request test fails before the fix.
Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(gateway): count unique omitted chat-history messages + prove diag…
- PR branch already contained follow-up commit before automerge: test(gateway): prove chat.history request emits omission diagnostic
Validation:
- ClawSweeper review passed for head 414f885880.
- Required merge gates passed before the squash merge.
Prepared head SHA: 414f885880
Review: https://github.com/openclaw/openclaw/pull/96788#issuecomment-4799553366
Co-authored-by: ZengWen-DT <ceng.wen@xydigit.com>
Approved-by: takhoffman
* fix(document-extract): render PDF image fallback per page so multi-page scans don't starve later pages
clawpdf's mode:"images" extract applies a single maxPixels budget across
every page, so the first page consumes it and later pages collapse to ~1x1
PNGs that vision OCR models reject. Render each selected page in its own
extract() call so the pixel budget resets per page and every page yields a
usable image.
* fix(document-extract): preserve aggregate PDF render budget
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(workboard): hide archived cards in CLI list by default
The `openclaw workboard list` CLI printed soft-archived cards, while the
`workboard_list` agent tool and the `/workboard list` command both hide
cards with `metadata.archivedAt` set unless archives are requested. Users
who archived cards still saw them in CLI output and assumed archive failed.
Filter archived cards by default in the CLI list handler and add an
`--include-archived` flag mirroring the tool's `includeArchived` option, so
all three list surfaces share one default. Docs updated to match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(workboard): preserve json list archive visibility
* fix(workboard): preserve json list archive visibility
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Overview surfaces counted any job whose lastRunStatus was "error" as a
current failure, so an intentionally disabled job that previously failed
kept inflating the top-level "failed cron" badge and attention callout.
Add a shared isCronJobActiveFailure predicate that gates the error status
on enabled, matching the adjacent overdue filter, and use it in both the
overview card and the attention items list. Historical status stays
visible in detail views via resolveCronJobLastRunStatus.
A concurrent atomic rewrite (write-temp + rename) of a memory-wiki source
page by the bridge re-export made fs-safe's opened-fd identity check fail
with `path-mismatch`, which the page write rethrew as a fatal "Refusing to
write" error and aborted the whole wiki_status / source-sync call. The race
is transient and benign: the file is replaced under the open handle and the
concurrent writer lands equivalent content.
Retry briefly on `path-mismatch` (the rename window closes sub-ms) and
rethrow unchanged on exhaustion, so persistent failures (directory
collision, not-file) and symlink/path-alias swaps still hard-fail exactly
as before. The identity guard is untouched; only the benign rename race is
retried, matching the sibling read path that already treats path-mismatch
as transient.
Extracts the guarded-write logic duplicated by source-page-shared.ts and
okf.ts into one writeGuardedVaultPage helper so both write paths get the
fix and the copy is removed.
Closes#92134
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(openai-completions): seal native reasoning before the answer
deepseek-style providers stream reasoning via reasoning_content deltas
then switch to the answer via content deltas with no boundary event.
thinking_end was only emitted by the end-of-stream finishBlock loop, so
it landed after the answer's text_delta and channels merged the answer
into the reasoning block.
Seal the open native thinking block when visible text (or a tool call)
begins so thinking_end precedes the answer; tag-based <think> reasoning
is unaffected (closed by the partitioner). finishBlock is now idempotent
so the end-of-stream loop never re-emits thinking_end.
* fix(openai-completions): preserve co-streamed reasoning
* fix(openai-completions): order co-streamed reasoning
* fix(openai-completions): seal co-streamed reasoning
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Clarify that `networkidle` is supported for managed and raw-CDP browser sessions but rejected for existing-session mode.
Fixes#80587.
Co-authored-by: ZengWen-DT <ceng.wen@xydigit.com>
* fix(feishu): paginate wiki node and space listing (fixes#37626)
client.wiki.spaceNode.list / wiki.space.list return at most one page (max
50 items); the tool ignored has_more/page_token and silently dropped every
node past the first page. Drain both endpoints via a bounded shared helper
that loops on has_more with a 100-page safety cap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(feishu): expose wiki pagination cursors
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
QQBot is the only channel that root-sandboxes outbound local files. Its three
gate sites (resolveOutboundMediaPath, the voice send re-check, and
structured-payload validation) only trusted the QQ Bot media storage roots, so
framework-generated scratch media written under OpenClaw's hardened temp root
(e.g. cron auto-TTS voice files from speech-core) was rejected. The send then
returned a no-identity error, the message was silently lost, yet cron still
recorded it as delivered.
Add one shared resolver (resolveTrustedOutboundMediaPath) that also trusts the
preferred OpenClaw temp root — already a sanctioned media root in core
(buildMediaLocalRoots) — and route all three gates through it so the trust set
agrees everywhere. Fixes#92816.
Co-authored-by: zengwen <zeng_wen@foxmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(tui): show activity indicator for system-injected runs
System-injected runs (bridge-notify, webhook, cron) never go through the
TUI submit path, so no active/pending run id exists when their lifecycle
"start" event arrives. handleAgentEvent dropped events for untracked runs,
leaving the status bar idle until the response landed.
Adopt an untracked lifecycle "start" for the current session (lifecycle
events always carry sessionKey) so the activity indicator shows work is
happening, mirroring how chat deltas adopt runs in handleChatEvent. Local
side-question (btw) runs never claim the active slot.
Closes#51825
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(tui): preserve concurrent injected run activity
---------
Co-authored-by: zengwen <zeng_wen@foxmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
Thread the existing agents.defaults.timeFormat setting through the Control UI
bootstrap config so WebChat/Control UI timestamps render in the configured
12h/24h clock instead of always using the browser locale default. "auto"
keeps the browser default, so existing deployments are unchanged.
Closes#58147
Co-authored-by: zengwen <zeng_wen@foxmail.com>
* fix(tui): keep spinner active when toggling tools
* fix(tui): preserve finishing status when toggling tools
---------
Co-authored-by: zengwen <zeng_wen@foxmail.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Fixes#44922
Preserve heartbeat-owned cron reminders as a single model input during heartbeat runs while keeping normal-turn fallback delivery when a heartbeat is skipped.
Proof: focused local Vitest/oxlint/format, clean autoreview, Crabbox AWS run_67abc286250a, Crabbox AWS check:changed run_bddebf014d58, and exact-head GitHub CI green for 341e807d7a.