agents_list exposed the raw configured primary (routing alias) as the
model field while agentRuntime was derived from the resolved model,
giving agents two contradictory identities. Publish the resolved
provider/model from resolveDefaultModelForAgent instead, matching the
gateway agents.list fix (#122907).
* fix(acp): cancel pending prompts on shutdown
ACP shutdown now aborts accepted Gateway runs and releases bridge session state rather than leaving prompts pending.
* fix(acp): preserve injected store ownership
Keep caller-provided ACP session stores generic while disposing only the concrete store created by the agent.
check-lint-core-4 (run 31872054461) flagged no-shadow: the failure
diagnostics refactor in c59ef0b40c named its parameter 'error' while
two inner catches reused the name.
Root cause of the checks-ui-e2e boot-stall flake (run 31867505277): the
built index.html references modules relatively (./assets/*), and the real
Gateway resolves /assets/ at any path depth (src/gateway/control-ui.ts),
but the e2e preview server let Vite's SPA fallback answer
/chat/assets/index-*.js with index.html as text/html. The browser rejects
that module silently (console-only, no pageerror), so a document reloaded
on a deep link like /chat/research never boots. Under CPU load the
harness's replaceState("/")+reload pair raced the router's redirect and
reloaded on the deep link, producing the all-null 30s poll.
- Preview server now mirrors the Gateway's depth-insensitive /assets/
resolution; the racy replaceState canonicalization hack is deleted.
- Deterministic regression test boots a document directly on
/chat/research; mutation-proven (fails with the middleware disabled).
- Failure diagnostics generalized: boot waits capture screenshot, script/
resource evidence, pageErrors, and a navigation-traced event ring.
Proof: Testbox tbx_01m022feapn38ypws3mb2b9jhv saturated loops — pre-fix
2/10 failed with the exact CI signature; post-fix 0/14 at the same pace.
Keep routine Codex commands from pausing for approval in default and team YOLO sessions. The default exec policy omitted its full mode, so a generic tool hook could promote approvalPolicy from never to untrusted.
Serialize node worker installs per Gateway namespace so a later bundle hash reclaims interrupted staging from older hashes instead of leaking it indefinitely.
* docs(agents): fact-check, disambiguate, and further slim root AGENTS.md
Second cleanup pass over root AGENTS.md driven by three audit lanes
(fact-check vs repo, confusion/contradiction hunt, cut candidates):
- Fact fixes: delete the @buape/carbon pin rule (dependency no longer exists
in any manifest or pnpm-lock.yaml); fix the reply-format example to a real
file (extensions/telegram/src/bot-access.ts).
- Confusion fixes: define checkout classes once in Commands and merge the
three conflicting test/check bullets onto them; state that the mock-gateway
harness verdict satisfies channel-visible proof as an explicit exception to
mandatory UI screenshots; complete the Map scoped-guide list (src/tui, test)
and note deeper subtree guides; inline the dangling 'Start-section evidence
bar' reference; gate issue-closing and public-comment posting on explicit
authority; scope the NO_REPLY rule to ClawSweeper hook sessions; clarify
auto-provisioning vs Architecture's compat-fallback deletion rules.
- Cuts/moves: scripts/pr gotchas, throttle-lock recovery, non-main merge
procedure, media-upload mechanics, and merge-ref-race recognition into
$openclaw-pr-maintainer; QA private build flag into $openclaw-qa-testing;
release-branch CI dispatch refs and release-ci-summary limits into
$release-openclaw-ci; Vitest ENOTEMPTY race into $openclaw-testing;
WebVNC screenshot etiquette into $crabbox; delete bullets duplicated by
those skills (prompt snapshots, QA YAML) and the obsolete rebrand-doctor
bullet; compress the preflight, Codex gate, SQLite-additive, ClawSweeper,
and Execution Identity wording without dropping any invariant.
* docs(agents): exempt extended-stable canonical dispatch from the target_context_ref rule
* fix(codex): keep prior finals when a coda follows without tool work
Live persist last-won a single assistant slot, so a trailing final_answer
replaced the real summary. Keep explicit finals after the last native tool;
phase-less coordination text stays last-wins.
* fix(codex): record the native-work barrier from item notifications
turn/completed.items is a last_agent_message Summary, so result-time
tool scans cannot see intervening work. Invalidate persistable finals
when native tools arrive, drop trailing silent tokens, and let a later
unphased message replace earlier finals.
* fix(codex): make the persistable-text barrier once-per-handoff
Delayed native completions must not move the barrier past an answer that
arrived while that tool was already active. Dynamic tool calls are the
same handoff. If no audible text exists after the barrier, recover the
last persistable answer instead of dropping it.
* fix(codex): keep a post-handoff silent final authoritative
A tool after an audible final starts a new answer segment. If that
segment is NO_REPLY, do not revive the pre-tool text. Recover a prior
answer only when the post-handoff segment is empty.
* fix(codex): classify silent finals with the delivery payload predicate
Token-only matching missed JSON and reasoning-wrapped NO_REPLY
payloads, so a trailing control envelope could join into the visible
answer. Use the shared payload-level predicate.
* fix(codex): honor unphased replacement and sleep handoffs
A later unphased item must invalidate older finals even when a later
final skips it, and sleep is a native answer barrier.
* fix(ui): keep the chat transcript anchored while the pane resizes
The width-change path wiped the virtualizer's entire item-size cache
(instance.measure()), collapsing every offscreen row to the 120px estimate
with no scroll compensation: the reader teleported mid-transcript and the
trickle of remeasure corrections made text visibly jump for several frames
on every pane or window resize.
Keep the stale offscreen sizes as estimates instead. The existing
synchronous resizeItem pass plus TanStack's per-row ResizeObserver re-seed
connected rows with fold-based scroll compensation, so the anchor row holds
still; offscreen rows correct lazily as they connect. End-pinned transcripts
keep following the tail via the existing height-change scrollToEnd path.
Regression e2e: mid-transcript anchor stays within 60px across
1280->1000->820->1280 resizes (pre-fix: anchor scrolled out of the viewport
entirely and scrollTop drifted by 250px+). Wipe introduced in #115059,
refined by #117687/#123713 without addressing anchoring.
* test(infra): deflake ephemeral-port rebind proof under runner contention
The bind->release->rebind cycle races foreign processes on shared CI
runners: a stolen port between release and rebind fails the assertion
(EADDRINUSE, seen on checks-node-compact-large-17). Retry the whole cycle
on a fresh ephemeral port, bounded at 5 attempts; a genuine release
regression still fails every attempt because the collision is with our
own lingering listener.
* test: prove port release on the allocated port; add end-pin resize proof
Address ClawSweeper review on #124013:
- ports-probe: replace the fresh-port retry with a connect probe against the
same allocated port. A lingering listener accepts the probe; a released
port refuses it. Unlike a rebind, the probe does not collide with foreign
outbound sockets that transiently occupy the port on busy runners, so the
release contract stays tied to one port with no retry at all.
- chat-resize-anchor e2e: add a width-only end-pinned case proving the
virtualizer's wasAtEnd compensation keeps the transcript within 2px of
the end across 1280->1000->820->1280 resizes.
* test(ui): widen resize-anchor drift bound for Linux font metrics
The fold-spanning anchor row re-wraps by a renderer-dependent amount
(42px macOS, 63px Linux CI at 820px). 120px keeps a wide margin below
the pre-fix failure mode (anchor out of viewport, 250px+ drift).
* test(tooling): register manager-session-update-race in memory helper routing
#124024 added the memory-core test on main without updating the
cross-lane routing expectation in test-projects.test.ts, breaking the
core-tooling shard on every merge ref that includes it.
Six sites strict-compared row.key === sessionKey while the sibling locked
check already used areUiSessionKeysEquivalent (#104045 upgraded only the
model path). In alias/transition windows (pre-hello legacy 'main' vs
canonical agent:main:main) the optimistic thinking/fast-mode row patch
found no row — the click produced no immediate visible change — and the
display resolvers missed the active row, falling back to agent defaults.
All lookups now go through the equivalence helper the rest of the flow
uses.
* docs(agents): dedupe root AGENTS.md and move workflow detail into owning skills
Root AGENTS.md loads into every session; ~9.5KB of it duplicated content
already owned by skills (crabbox, openclaw-testing, release-openclaw-*,
openclaw-pr-maintainer, openclaw-changelog-update) or restated the same
invariant in multiple sections. Skills absorb the root-only operational
details first (Testbox/Crabbox mechanics, backport default target, fork-code
landing variant), then root keeps hard policy and routing only.
Also fixes skill drift found during the audit: positional testbox status id,
missing remote-unavailable local-fallback policy, delegated-flag reject list,
and aligns the early performance dispatch fail_on_regression flag with the
Full Release Validation profile gate (true stable / false beta).
* docs(agents): derive fail_on_regression from release profile in dispatch example
Broke the main gate on run 31865993941 (checks-ui-e2e 2/12):
expected 609.81 <= 609.21. The preceding scroll poll tolerates a <=1px
remainder because scrollHeight/clientHeight are integer-rounded, while
measureWithinAncestor compares fractional getBoundingClientRect values,
so the true bound is 2px, not 1.
The agents-page model catalog was cached per agent for the whole
connection: availability facts (provider keys added/removed, new models)
went stale with no refresh path except a full reconnect, while the chat
composer's sibling picker refreshes on open. Opening the overview picker
now re-reads through the same ensureModelCatalog owner with a refresh
bypass of the per-agent cache.
sessions.patch's 'model catalog unavailable' error told the operator
nothing actionable; it now states the catalog is still loading and to
retry — the failure text names the next step per doctrine.
Measured on run 31865243804: the 3-part split on
blacksmith-8vcpu-windows-2025 got exactly 2 runners; part 2 queued 211s
and only started when part 1 finished, pushing the wall to 6:45 despite
133-163s job bodies (vs 342s hosted). The runner class admits ~2
concurrent jobs, so any profile that can land on Blacksmith now uses the
single lane; the 3-part split stays for guaranteed-hosted executions
(github mode and dispatches, whose runner expression forces hosted in
every mode). A hybrid retry reruns the single lane hosted - slower but
bounded, and Windows-scoped retries are rare.
state.onModelChanged has been () => undefined since the original Control
UI refactor (65e12328aa) and was never overridden; three call sites
awaited a decoy. Effective-tools previews key their requests on the model
override, so the post-switch refresh only needs a re-render, and the
model-switch reconcile already awaits the canonical session-list refresh.
Tests that used the seam as a deferral hook now hold the list refresh
open instead — the barrier the production code actually awaits.
* fix(ui): retire the local model override once the Gateway confirms the patch
settleModelOverride re-asserted the requested model into the shared
override map after every successful patch, and the reader prefers the map
over the server row unconditionally — so any external model change
(second operator window, channel /model, fallback rotation persisted by
the Gateway) never reached this window's picker or its effective-tools
key for the rest of the connection. The refreshed row already carries the
confirmed selection; retire the entry instead of shadowing the recorded
fact. deferListRefresh callers keep the published value (their refresh
has not run), rollbacks and newer-claim transfers are untouched.
* test(ui): split model-override lifecycle coverage into a focused file (max-lines)