Respawn one-shot macOS commands with Node's OpenSSL-backed CA store when system CA loading is inherited. This avoids an indefinite shutdown join on the Keychain loader worker while preserving system CA for Gateway, interactive, and explicit --use-system-ca processes.
* feat(clients): copy visible markdown, full-message reader for truncated messages, and independent reasoning/tool toggles
* chore(i18n): translate new reader and trace strings and regenerate catalogs
* test(ios): adopt reader display-options API in render smoke tests
* feat(ui): Home page, identity row as agent menu, Pages header, Threads-hosted actions
* fix(ui): rename Home badge local shadowing the lit state import
* test(imessage): mock core turn dispatch for media policy coverage
PR #110095 missed this iMessage media-policy test file.
* test(imessage): mock core turn dispatch for last-route coverage
* test(whatsapp): mock core turn dispatch for broadcast coverage
* test(imessage): drop type-dead terminal-admission branch in media harness
* feat: normalize web search output contract
* refactor(agents): promote web_search contract from a dedicated output module
* fix(agents): wrap unwrapped web_search text and pass unknown provider payloads through as raw
* fix(agents): gate web_search results branch on conforming rows and cover metadata fields
* fix(agents): gate every provider text path at the web_search boundary
* test(agents): align web_search fixtures with boundary-owned wrapping
* fix(agents): make the web_search boundary own the untrusted-content envelope
* fix(agents): report declared web_search errors first and align the documented contract
* fix(agents): bind envelope stripping to real markers and densify result rows
* fix(agents): emit canonical urls and a closed error code from web_search
* fix(agents): keep structured provider error diagnostics in the wrapped message
* fix(agents): satisfy production export and lint gates for the web_search contract
The Control UI rounded any every-schedule interval that is not a whole
number of minutes up to the next minute when reading a job into the edit
form (parseEverySchedule ceil): 30s and 90s and 4m6s all became a whole
number of minutes. Because addCronJob always rebuilds the schedule into
the cron.update patch, any save — including metadata-only edits that
never touch the schedule — silently rewrote the cadence (e.g. 30s → 60s,
90s → 120s) and re-anchored the job. CLI, API, and docs accept these
intervals, and the UI itself can create them, so the editor corrupted
cadences supported everywhere else.
Add a Seconds interval unit and read everyMs back into the largest unit
that divides it exactly (days → hours → minutes → seconds), rendering
sub-second remainders as exact decimal seconds built with BigInt
quotient/remainder. Every integer millisecond up to
Number.MAX_SAFE_INTEGER now round-trips losslessly, so a resave with the
same everyMs preserves the gateway's anchor inheritance. Gateway,
protocol, storage, and cron runtime are unchanged.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ui): add permanent OpenClaw navigation
* refactor(ui): drive Exit setup visibility with a typed onboarding property
The route view previously hid the button with an injected style tag (the page
file was owned by a parallel lane); render it conditionally instead.
* test(ui): use the navigation route-load cause literal
* fix(ui): request the caretaker greeting for permanent custodian visits
The onboarding welcome variant seeds the first-run setup proposal; the pinned
sidebar and Settings entries now omit it so post-setup visits get the normal
caretaker greeting, driven by the same route onboarding flag as the chrome.
* fix(ui): restart custodian session on mode change
* feat(ui): five-zone sidebar with agent identity card, threads/groups/coding zones, and footer bar
* fix(ui): promote main-session children via the gateway row's literal key
* fix(ui): filter and sort promoted main-session threads; hide empty Coding count
* fix(ui): collapsed zones no longer consume the visible session page budget
* fix(ui): a11y — offline card label and catalog lists outside the Coding list container
* fix(ui): zone headers use pre-pagination counts so paged-out zones stay visible
* chore(ui): document catalog-adoption dedup invariant on promoted threads
* chore(ui): seed five-zone sidebar fixtures in the mock-dev harness
* fix(ui): resolve the canonical global main key for the identity card under global scope
* fix(ui): global scope resolves the global sentinel as canonical main even pre-hello
* fix(ui): drop dead channelDisplayLabel, avoid map-spread, type zone test helper
* fix(qa-lab): close transport before gateway teardown
* test(qa-lab): bind teardown order to production plan
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(mcp): reject sandbox CSP metadata that normalizes to no policy
A present ?csp= value that decodes to valid JSON but is not a usable CSP
(e.g. null or a wrong-shaped object) normalized to undefined and was
indistinguishable from an absent parameter, so the gateway served proxy
HTML under the default policy. encodeCsp omits the query param entirely
for such values, so a present-but-empty policy is never legitimate;
throw so the sandbox endpoint fails closed with 400. Found by review on
the revert of 73685b4e7c946; the gap predates that commit.
* fix(mcp): treat empty csp query value as malformed, not absent
?csp= with an empty value passed the falsy absent-guard and served proxy
HTML under the default policy. Only a truly absent parameter (null) may
skip validation; an empty string now falls through to JSON.parse and
fails closed with 400.
* refactor(gateway): drop redundant sandbox CSP handler guard
decodeMcpAppSandboxCsp now throws for every present-but-unusable value
(1ca26c508d added the same fail-closed behavior at the handler seam),
so the handler-level 'present but falsy' check is unreachable. Keep the
invariant in the decoder, which owns policy decode semantics.
* fix(test): reset diagnostic listener-presence mirror between non-isolated files
resetOpenClawGlobalDiagnosticState clears the listener sets and deletes
the diagnostic-events state key, but the listener-presence counts live
under a separate globalThis record and survived, so
hasInternalDiagnosticEventListeners() stayed true for every later file
in the worker once any file leaked a registration (e.g. the import-time
listener in src/logging/diagnostic-run-activity.ts whose stop handle is
lost to the module-registry reset). Zero the counts to match the cleared
sets. Root cause of the model-call-diagnostics flake in CI run
29624203224; #110288 added the victim-side reset, this fixes the class.
* fix(tools-manager): bound GitHub releases response body to prevent unbounded read
The agent helper-tool fallback for fd and ripgrep read GitHub's
latest-release response with Response.json(). That consumes the
complete remote body before parsing it, so a faulty or compromised
response could grow agent memory without a bound.
This change makes the release-metadata fetch use the shared
readResponseWithLimit helper with a 1 MiB cap.
Ref. https://github.com/openclaw/openclaw/pull/98480
* fix(tools-manager): use bounded release reader
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* 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(discord): adopt durable ingress drain for gateway messages
Gateway MESSAGE_CREATE events dispatched with only an in-memory/persistent
5-minute dedupe; a crash between receive and dispatch lost the message (live
sessions replay via RESUME, but session state is memory-only so a cold
restart re-IDENTIFYs — stated platform limitation: the drain closes the
receive->dispatch window, not cold downtime). Raw API messages now journal
durably (event_id = message snowflake, lane per channel/thread) before
normalization; dispatch runs through the core drain with deferred claims,
fan-out adoption, gated settlement, and 30d/5k tombstones. The persistent
guard is deleted after its RESUME duplicate-delivery parity test. Edits stay
outside the queue: only MessageCreateListener is registered, MESSAGE_UPDATE
never dispatches, so snowflake tombstones cannot swallow edit turns; a future
edit dispatcher must namespace its event ids.
Autoreview blocked by codex sandbox network in the build stage; full manual
review performed. Part of the #109657 fleet adoption program (wave 2).
* style(discord): keep drain-internal types and dispatcher unexported
* refactor(discord): move message dispatcher to its own module for the dead-export gate
* fix(discord): shutdown dispatch releases claims instead of tombstoning
Landing autoreview reproduced a real loss race: a dispatch entering after
dispatcher shutdown (or with an aborted signal) returned completed, so the
drain tombstoned a message that never ran and a restarted gateway skipped it
forever. Aborted-before-dispatch now returns failed-retryable so the claim
releases for replay. Test type imports of drain-internal types converted to
factory-derived types so the dead-export gate and typecheck agree.