* fix(ui): fork active sessions from stable history
* fix(protocol): align active fork types
* test(ui): match main-session fork routing
* fix(sessions): unify stable fork admission
* fix(native): fork active sessions from stable history
* fix(android): fork active sessions from stable history
* style(android): simplify active fork result handling
* fix(native): preserve legacy fork transport API
* test(native): complete legacy fork transport stub
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
chat links used --accent while user bubbles are filled from --accent-subtle (and peer bubbles from a per-sender hue), so links sat on their own hue at 3.95:1 worst case. New --link/--link-hover tokens derive from each palette's audited --accent-hover and clear WCAG AA on every bubble surface and sender hue; the hover opacity fade, which lowered contrast, is replaced by a color shift.
install_build_tools_linux ran run_quiet_step and then returned 0
unconditionally from every package-manager branch, so a failed build-tool
install was printed as an error and immediately overwritten with a success
line. The installer emitted "Installing build tools failed" followed by
"Build tools installed", and the caller's "Continuing without auto-installing
build tools" warning was unreachable. The npm-failure recovery path likewise
reported "Build tools setup complete" and retried an install that could not
succeed. install_build_tools_macos already propagated its status; only the
Linux sibling swallowed it.
Return the package manager's exit status and collapse the five duplicated
is_root/sudo branches into one command list, which removes the bug class
rather than patching a single branch.
Verified against the live openclaw.ai script: a stubbed apt-get failure
reproduced the false success before the change and reports the honest warning
after it.
* fix(approvals): sanitize plugin titles and exec paths at the creation boundary
Plugin approval title/description/detail were stored raw and flowed
unescaped into channel messages, iOS lock-screen push, and the web modal —
bidi/invisible characters could spoof what the operator approves. Exec
approvals already sanitize command text at creation; mirror that for the
plugin request and for exec's display-only cwd/resolvedPath fields.
Re-check the protocol size caps after escaping so a spoof-heavy title fails
as INVALID_REQUEST instead of a misleading registration throw.
* fix(gateway): sanitize node-policy plugin approvals through the same boundary
The node-policy approval runtime truncated title/description but never
sanitized them, so the identical broadcast/forwarder/push paths stayed
spoofable through this sibling creator (ClawSweeper P1 finding). Normalize
first so a whitespace-only title still fails closed at register.
* fix(approvals): sanitize plugin metadata and cap stored detail
ClawSweeper follow-ups: pluginId/toolName/fallback agentId are interpolated
into channel approval text and were stored raw; the stored detail skipped
the 16,384 cap the durable presentation applies, so escape expansion could
exceed it. Sanitize the metadata at the same boundary (host-minted runtime
identity stays authoritative) and cap detail at storage.
* fix(gateway): sanitize node-policy approval metadata too
toolName and fallback agentId from node-policy code are interpolated into
channel approval text; escape them like the RPC ingress. Host-minted
runtime identity values stay authoritative.
* fix(approvals): normalize exec policy enums and escape host at creation
security/ask are closed enums — arbitrary strings now null out via the
canonical normalizers instead of reaching reviewer meta rows; host gets the
display escape (identity for valid values). nodeId/agentId/sessionKey stay
raw by design: they are matched against the node registry and session
routing, noted inline.
The header-to-transcript blend spanned the full pane width, so it painted over the strip where the transcript scrollbar lives and the thumb vanished under the fade while scrolling. Stop the blend one transcript gutter short of both pane edges and make that gutter a token shared with the transcript column, which keeps every pixel of content faded while the scrollbar stays visible (symmetric so RTL keeps the same clearance).
* perf(ci): cut hosted CI critical path toward five-minute walls
Stripe the serial core test-type graphs across two hosted jobs and drop the
duplicated tsgo:test:root pass; gate the six-part QA Smoke matrix off pull
requests unless a QA-owned surface changed; split the fat multi-config Node
shards (cli/cli-process, unit-fast isolated/fake-timers, infra
logging-process/runtime-config) and lower the hosted split ceiling to 150
predicted seconds so no compact lane owns a ~280s wall; expand tooling to
seven stripes.
* perf(ci): widen hosted test-type striping to three jobs
Run 31825922122 measured ~40s per core test-type graph on loaded hosted
runners (282s worst stripe body of the two-way split); three stripes keep
each lane near 150s body under load.
scripts/mac-elevation-host.sh ships as the portable installer added in
#123675, so it is deliberately self-contained and cannot source shared
constants. That forces it to duplicate ELEVATION_TEAM_ID and
ELEVATION_IDENTITY from scripts/codesign-mac-app.sh, with nothing keeping
the two copies in sync. Add a test asserting they agree, so rotation drift
fails at build time instead of on a target Mac.
Also document why the sign-time and install-time elevation verification
functions both exist: verify_elevation_app requires a notarized and stapled
bundle, so it cannot replace the sign-time check, and deleting that check
would defer every identity failure until after a notarization submission is
already spent.
No behavior change: no artifact bytes, filenames, modes, receipt fields,
signing, notarization, or install paths are touched.
An auto-disabled job (10 consecutive run failures / 3 schedule errors)
rendered as plain 'Paused' and simultaneously vanished from the failed
chip, the failing count, and the error dot — the escalation removed
visibility exactly when the problem became permanent. The recorded fact
(state.autoDisabled, already public and used by CLI/doctor) now renders as
a distinct danger note with the reason and count, and auto-disabled jobs
stay in the active-failure predicate.
* chore(cli): drop unused resolveClawHubTargetWorkspaceDir
#116489 migrated its last callers to resolveClawHubTargetWorkspace but left
the wrapper, failing check-prod-types (TS6133) on every merge-ref CI run.
* test: split dedupe maintenance suite below the max-lines cap
server-maintenance.test.ts crossed the 1000-counted-line test cap when
#123642 added payload-retention coverage, failing check-lint-core-2 on
every merge-ref CI run. Move the dedupe TTL/overflow block into a themed
sibling; module mocks are hoisted per file so the mock preamble is
repeated while fixtures stay local. Test parity 41 = 41.
#123761 removed the last caller of resolveClawHubTargetWorkspaceDir; the
stranded helper fails tsgo prod types (TS6133) and lint on every main push
since 877f2ec083.
* fix(ui): report connection loss instead of silently dropping session actions
patchSession returned 'stale' with no message when the gateway dropped
before a queued dialog submit (rename and similar) — the dialog closed and
the edit vanished with no visible outcome. loadCheckpoint had the sibling
gap: expanding a row while disconnected left the drawer claiming 'No
checkpoints' beside a nonzero checkpoint badge. Both now surface the
existing actionRequiresConnection message at their owning error surface.
* test: widen patchSession test-support patch type for label patches
The session row's terminal stamp (status/endedAt/runtimeMs) and its usage
persist (outputTokens) are separate gateway writes. resolveTurnRecap settled
on the first fresh terminal row and froze whatever outputTokens it carried —
frequently the PREVIOUS turn's count. Treat an unchanged-from-baseline value
as that lag and fall back to the watched run's live usage-stream counter
(captured while watching, since the usage map entry dies at lifecycle end).
* fix(ui): complete pairing setup lifecycle
Redeemed and expired mobile setup codes stayed on screen as usable QR
codes, so a successful pairing had no visible outcome and expired bearer
material still looked live.
The Gateway now mints an opaque, non-authorizing setupId beside each
setup credential, returns its authoritative expiresAtMs, records the
terminal outcome of the exact redemption, and broadcasts
device.pair.setup.completed. Because that broadcast is dropped for
buffered operator sockets, the completion is persisted first and can be
reconciled through the new device.pair.setupStatus method: the Control UI
asks for the recorded outcome before it may present a credential as
expired, so a pairing that succeeds is never shown as a failure.
The Control UI models one closed lifecycle (selection, loading, waiting,
success, expired, error) correlated only by the active setupId, and
Pairing help now carries the external-link affordance.
* fix(ui): preserve unknown pairing outcomes
* test(ui): target pairing terminal headings
* test(ui): align pairing completion fixture
* fix(gateway-protocol): decode setup-code results from older gateways
Older protocol-v4 gateways omit the new setupId and expiresAtMs
lifecycle fields, so requiring them in the generated native model broke
decoding an existing device.pair.setupCode response. Keep both optional
at the wire boundary, require lifecycle metadata before the Control UI
enters its waiting state so a missing outcome stays visible, and cover
the legacy payload with Swift and schema regressions.
* fix(ui): surface rejected pairing dialog loads
The lazy pairing chunk could reject while its overlay was already open,
leaving the shell rendering nothing at all. Record the rejection on the
shell and render a recoverable modal with a reason and a retry so the
open action always ends in a visible outcome.
* fix(state): preserve pairing setup schema compatibility
* test(gateway): cover pairing setup release train
* fix(gateway): commit pairing setup completion atomically
* refactor(state): distinguish setup transaction helpers
* refactor(state): remove obsolete bootstrap restore path
* fix(gateway): preserve setup handoff type safety
* fix(gateway): keep pairing completion terminal after consume
* fix(gateway): validate Watch binding during setup commit
* fix(gateway): revalidate setup credential expiry at commit
* chore: refresh Plugin SDK API contracts after rebase
* fix(pairing): prune expired setup completions
* chore: retrigger CI
* fix(protocol): deduplicate setup expiry field
* fix(protocol): refresh pairing setup clients
* fix(gateway): make pairing setup completion durable
* fix(ui): retire expired pairing credentials immediately
* fix(ui): keep pairing dialog visible while loading
* fix(macos): align setup result initializer order
* fix(gateway): restore generic bootstrap retries
* chore(ui): record pairing startup budget
* chore(ui): refresh pairing startup budget
* style(gateway): format maintenance imports
* test(gateway): cover session-sharing mock
* fix(state): defer setup correlation schema
---------
Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
The archive toast outlives the Sessions page (toast host is app-shell
level), but its Undo action was gated on the page's request scope, which
dies the moment the operator navigates away — clicking Undo within the 6s
window then silently did nothing. Run the un-archive through the shared
sessions mutations store directly (it already fails closed on connection
replacement and publishes errors), mirroring the always-mounted sidebar
undo sibling.