build-artifacts is the wall's pole in 4 of the last 5 main runs (171-186s,
~15-20s ahead of the next lane), so its serial steps are the wall. The
Doctor plugin-index proof, singleton smoke, and startup-memory check ran
as their own 13s step even though they are independent dist readers that
the 47s artifact-check wave could absorb.
They now run inside that wave: on Blacksmith all seven start together, so
the verifiers cost the wave's max instead of 13s of serial time; hosted
runners still serialize the three through run_verifier so the RSS ceiling
measures an unloaded process. The step drops its selection gate because
the verifiers always run -- each artifact check already self-gates on its
own RUN_* flag, so a run with no checks selected still verifies.
Proof: extracted the step body and ran it with stubbed pnpm/node. Both
modes behave (Blacksmith 7 checks started, hosted-with-nothing-selected
still runs the 3 verifiers), and a failing verifier exits 1 with its
::error annotation in both -- the wave cannot swallow it.
checks-node-core-tooling-2 failed on main (run 31943910358) with
'expected 1786879225197 to be >= 1786879225197.2246': the repaired output
mtime landed a fraction of a millisecond below the input it had to clear.
isArtifactSetFresh repairs output mtimes to exactly ceil(newestInput), so
it leaves zero headroom for sub-millisecond write rounding or lagging
metadata on CI filesystems. When the repair lands at or below its input
the mtime fast path never engages, and every later invocation in that
checkout falls back to re-hashing every input byte -- the expensive path
this repair exists to avoid, gating the d.ts emit that is the slowest
build-all phase (25.9s of a 61s build).
Neither macOS APFS nor an idle Linux ext4 Testbox reproduces the
shortfall in 300 runs, so the repair now clears the newest input by a
whole millisecond instead of matching it. The assertion pins that
headroom, making the test deterministic where it was previously
load-dependent (fails 615 vs 616 without the fix).
* fix(computer-use): unblock the macOS live-rig proof flow
The rig ran its operator CLI and its proof runner from one state dir, so both
shared one device identity. A paired operator device is pinned to the scopes of
its first connect, and `nodes list` connects first for `node.pair.list`
(operator.pairing); the proof runner then needs operator.write, which is a scope
upgrade the gateway never approves silently and which no rig client can approve
for itself. The proof runner is a GATEWAY_CLIENT/BACKEND client, so on a
loopback auth-none gateway it is admitted unpaired with the scopes it asks for:
giving the CLI its own `cli-state` identity is enough, and `agent-state` now
never accumulates a pairing row.
`nodes list` also read `node.list` through the plain CLI client while
`nodes status`/`describe` used the diagnostics ladder. On any gateway where the
CLI must pair, the unfiltered list silently dropped connected/commands/
computerUse and `--connected` failed outright, so the documented rig gate could
not confirm the node. Both call sites now use `callNodeDiagnosticsGatewayCli`.
Docs drop the `devices approve <requestId>` instruction, which was circular:
that invocation is its own new device identity.
* test(cli): share the runtime-log formatter across nodes CLI e2e files
The extracted diagnostics-auth file stringified captured log arguments directly, which the type-aware core lint stripe rejects (no-base-to-string). Move the existing formatter into the shared node test helpers instead of duplicating it.
* fix(cron): keep error class names out of run history
* fix(cron): preserve lifecycle abort reason
* fix(cron): preserve coded abort reasons
* docs: record cron abort follow-up
* fix(ui): always surface terminal chat send failures
Terminal chat send failures were recorded only on the queue item when the
owning pane was not visible (reconnect, alias drift, split-pane routing),
so the operator saw nothing at all — an action ending in silence.
Root cause: every error surface in the send/drain path was gated on
visibleSessionMatches(...), and the FIFO outbox drain treated every
chat.history reconcile rejection as a silent retryable "blocked", so a
non-retryable rejection (e.g. auth loss) wedged the head — and everything
behind it — permanently with no visible outcome.
Fix, at the owner:
- New surfaceChatDeliveryFailure() in steer-lifecycle.ts (the shared
error-text owner): visible pane keeps the inline chat error; otherwise
the failure routes through the existing global toast host, naming the
session. All terminal failure sites in chat-send-delivery,
chat-outbox-drain, chat-send-queue-state, and steer-lifecycle now use it.
- reconcileStoredChatOutboxHead: a non-retryable GatewayRequestError on the
head now terminally fails a never-attempted head (unblocking the lane)
or parks an attempted head as unconfirmed — both with a visible outcome —
instead of blocking the lane forever.
- Composer disabled-reason: the reason now also renders while a draft hides
the placeholder, and the cloud-startup-pending gate gets a reason instead
of a silently disabled composer.
Regression tests: hidden-pane terminal failure surfaces via toast; wedged
head fails visibly and the lane drains the next message; attempted head
parks unconfirmed; disabled reason visible with draft text present.
* fix(ci): raise startup JS baseline for global failure surfacing and pin workspace-sync test clock
The chat send path now imports session-display naming for the global
failure toast, adding ~1 KiB gzip to startup JS (335452 B on CI's Linux
builder, still well under the 358400 B committed cap).
workspace-sync "never commands" asserted the exact dispatch timeoutMs
(777) but the impl derives it from a Date.now() deadline, so any elapsed
ms between admission and dispatch failed the exact-equality assertion on
a loaded runner. Pin the clock like the sibling timeout tests do.
* fix(ui): surface route-switched command failures and agent-scope global toast naming
ClawSweeper review findings on #124473:
- A queued local command failing after the operator navigated away hit
failCommand(error) with expose=false; the dispatcher's stale-scope
guard had already withheld the inline error, so a successful state
write recorded the failure invisibly — the silent class this PR
removes. Expose it globally when the scope is stale and the owning
pane is hidden; a stale scope with the pane still visible keeps the
failed queue chip (the new connection owns the inline surface).
- Global session rows are agent-scoped behind one shared "global" key,
so the toast row lookup could borrow another agent's label. Match the
row's agentId to the failed outbox's agent for global keys.
Both regression tests fail pre-fix (stash-verified).
* fix(doctor): never report unpersisted config fixes
doctor --fix printed "Doctor changes" panels while computing candidate
mutations, then crashed with a raw Error and persisted nothing when the
repaired candidate still failed write validation (e.g. an unknown root
key repaired alongside an unrepairable schema type error).
Root cause: the compute->print->validate->persist ordering was wrong.
Panels were printed at mutation time, but validation only ran inside the
atomic writer, after all panels were visible.
Fix at the owner boundaries:
- doctor-config-flow queues repair-mode "Doctor changes" panels in a
sink instead of printing them; preview panels still print immediately.
Committed side-effect repair notes (SQLite/filesystem) keep printing
at repair time; candidate-config notes from the repair sequence are
routed through the same deferred sink.
- io.write throws a typed CONFIG_VALIDATION_FAILED error (with the full
issue list) via a new createConfigValidationFailedError owner in
io.write-errors.
- runWriteConfigHealth prints queued panels only after the atomic write
commits, and renders a validation refusal as a "Doctor warnings" panel
stating no config changes were written plus the exact paths to fix by
hand. The contribution loop stops after refusal (same invariant as the
cron-ownership deferral) and doctor exits 1 without a raw Error leak.
Regression tests: pre-fix, the new validation-refusal e2e test fails on
the lying "Doctor changes" panel; contribution-level tests cover the
refusal note, held panels printing exactly once after commit, and no
retry of the identical candidate.
* fix(doctor): report partial persistence accurately after a later write refusal
Post-rebase CI and ClawSweeper follow-ups:
- repair-sequencing tests now assert the deferred configChangeNotes contract
(candidate-only mutation notes moved out of changeNotes by the parent
commit); committed side-effect notes stay in changeNotes.
- formatConfigValidationFailure is module-private; its guidance formatting is
covered through createConfigValidationFailedError, fixing the knip
unused-export gate.
- When the initial write pass committed and only the later post-repair write
is refused, the warnings panel says earlier fixes were saved instead of
claiming no config changes were written, and the outro says "some config
fixes were not applied". New regression test covers commit-then-refusal.
A failed message send (e.g. missing channel credentials on a fresh
install) durably rewrote the agent's folded main session route before
delivery was even attempted: prepareOutboundMirrorRoute called
ensureOutboundSessionEntry pre-send, stamping delivery.route/origin and
minting a conversations-registry row for the never-reached target. The
Control UI then showed the phantom channel identity and the composer
bound to a dead conversation.
Route resolution stays read-only in prepareOutboundMirrorRoute; the
durable write commits once per send at the first success proof:
identified platform evidence (onDeliveryResult) or plugin action
acceptance (onPluginSendAccepted), both before the in-delivery
transcript mirror so first-contact routes still create their session
row, with a post-return safety net for adapters whose results carry no
platform identity. The gateway send RPC sibling had the same pre-send
persistence and gets the same commit-on-evidence ordering.
Regression test proves a failed send leaves the seeded main-session
origin and conversation identity untouched while a successful send
still persists the mirror route; fails on pre-fix code.
Gateway agents.delete closed the agent database (unlinking -wal/-shm)
during preparation, then awaited config/cron/session-purge work that
reopened the database and recreated the sidecars. The sweep classified
those recreated files as foreign replacements ("cleanup path appeared
after deletion preparation"), preserved them, cascaded ancestor
protection over the whole agent directory, and finished the deletion
journal with exit 0 while ~157 files survived under agents/<id>/.
The deletion journal fence already blocks legitimate claims beneath
prepared paths, so a file that appears at a prepared-absent path can
only be leaked deleted-agent state: adopt its identity and sweep it
instead of preserving it. Prepared-present paths keep the existing
identity-mismatch protection for genuine operator replacements.
Regression tests: recreated WAL sidecars between preparation and
cleanup are trashed with the agent directory, and recovery sweeps a
file that appeared at a journaled prepared-absent path.
Hide model fallback and recovery notices in group and channel conversations while preserving direct-chat notices, persisted state, and lifecycle events.
Co-authored-by: NehoraiHadad <nehorai.hadad.projects@gmail.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Preserve RFC 5322 angle-address emails without weakening namespaced tag stripping, and keep the iMessage security projection aligned.
Co-authored-by: Aria Ghasedi <drariaghasedi@gmail.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Allow group members to type an agent name without optional emoji or symbol decoration while preserving literal separators and Unicode boundaries. Bound raw joiner matching and replay QA startup patches in order.
Co-authored-by: 許元豪 <146086744+edenfunf@users.noreply.github.com>
Co-authored-by: Eden <146086744+edenfunf@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
A runtime auth-store mutation queues an auth refresh task on the prepared
model runtime publication tail. When a config hot reload superseded that
task (markPreparedModelRuntimeSnapshotsStale marks every owner stale and
opens a replacement gate before the queued task runs), the auth task still
ran drainPendingAuthMutations + replyDispatchPublication.rebuild against
the invalidated owners. That either threw
PreparedModelRuntimeOwnerNotPublishedError ('prepared reply dispatch
runtime owner was not published for main'), logging 'auth-triggered model
runtime refresh failed' on every reload and emitting a spurious
phase:'failed' publication event, or announced phase:'published' while the
replacement gate was still pending. The gateway chat metadata lifecycle
listens to those events: a spurious event triggered a metadata refresh
whose captureGenerationFacts saw no published owner (gate pending), failed
with ChatMetadataSnapshotUnavailableError, and runtime.fail() latched the
error while preparedModelRuntimeAvailable=false blocked the subordinate
skills/auth retry path - wedging chat.metadata until restart.
The auth task now fails closed against a pending replacement gate: the
queued config publication owns draining queued auth mutations against the
new config and rebuilding/announcing the dispatch publication
(refreshPreparedModelRuntimeSnapshots already drains the queue before its
own rebuild), so deferring loses nothing.
Regression tests reproduce both failure shapes and fail pre-fix.
A fresh openclaw onboard (and agents add) writes agents.entries without any
default:true marker; runtime resolves the sole agent as default via
tryResolveSoleAgentId, and read-time migration strips valid legacy markers
before the audit ever sees the config. The audit still demanded exactly one
default=true marker on non-explicit rosters, so every fresh install got a
false config.agent_roster.invalid_default_count warning.
Make the audit mirror runtime truth: a non-explicit roster is valid exactly
when the canonical resolver (tryResolveLegacyCompatibilityAgentId) finds an
owner - sole agent, one legacy marker, or the retained migration owner.
Explicit fleets keep the strict no-marker rule. This also fixes the sibling
false alarm for migrated legacy multi-agent configs whose marker was removed
at load time with the owner retained in-process.
Regression test: fresh-install-shaped sole-agent roster produces no roster
finding (fails pre-fix).