* fix(ui): keep the GitHub mark on its label's line when chat wraps
The mark is an in-flow atomic inline, which carries a soft wrap opportunity
after it, so a chat line could end on the octocat and start the next one with
the reference it labels. Painting the mark out of flow and reserving its space
with the anchor's own padding removes that opportunity: nothing inside the
anchor precedes the label any more.
The anchor stays inline, so a long bare URL still fills the line it starts on
instead of moving down whole -- the sibling file-link chip's atomic treatment
could not be reused here.
* test(ui): route GitHub link layout test through Node
Route error-bearing fan-in abandonment through the existing failure policy so deterministic pre-adoption failures cannot exceed retry limits and starve same-lane messages.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
broadcastToConnIds sent frames without a seq, so a slow-consumer drop
between targeted sends (session.message, talk, pairing, PR-subscription
events) was invisible to the client's gap detector — the stream just got
thinner with no recovery trigger. Targeted frames now consume and carry
the same per-client sequence as fanout frames; the protocol already
treats seq as optional-but-monotonic, so older clients ignore it and
current clients gain gap-triggered recovery for targeted paths.
The agents-page fallback editor gated staging on a resolvable primary
model, but the gateway resolver honors { fallbacks } with no primary
(agent-scope.ts, including the explicit empty-array-disables-global
contract). With a fully implicit default model, typing a fallback chip
cleared the input and staged nothing — no chip, no error, no write.
Clearing the primary had the sibling bug: removeFormValue deleted the
whole model node including authored agent fallbacks.
One stageModelShape owner now writes the smallest representable shape
(bare string, { primary, fallbacks }, { fallbacks }, or removal), and
both entry points share existingModelParts instead of duplicating
existing-shape probing.
Three measured cuts toward the sub-4-minute wall:
- checks-ui-e2e hosted-planner matrix 10 -> 12 rows (11 Vitest shards +
browser-extension). Shard 1 measured 230s with ~123s test body; Vitest
hash sharding leaves count as the only balance lever.
- checks-windows hybrid attempt 1 now rides blacksmith-8vcpu-windows-2025
(retries stay hosted windows-2025). The 3-part hosted split measured a
342s pole on full-graph runs.
- build-artifacts folds the Doctor plugin-index proof (12s), singleton
smoke, and startup-memory check (7s) into one step that overlaps them
on Blacksmith and stays serial on hosted 4-core so the RSS measurement
is unperturbed; any missing startup asset rebuild completes before the
fork so no verifier reads dist mid-write.
* fix(ui,gateway): make attachment admission one funnel and clamp advertised ceiling to the WS frame budget
Three residual gaps from the #123654 size-limit fix shared one root cause:
attachment admission policy was scattered instead of owned.
- hello-ok advertised the decoded config ceiling (20MB default, higher with
mediaMaxMb) without accounting for base64 4/3 expansion against the 25MiB
WS frame cap, so a 19.6-20MB attachment passed the client guard and the
encoded chat.send frame still hard-dropped the connection (1009) for
every pane. The policy owner now clamps the advertised maxBytes to what
one frame can carry.
- Large-text paste, data-URL image paste, and browser-annotation handoff
constructed attachments without any size check, bypassing the guard that
only lived inline in appendAttachmentFiles. All intake paths now share
one admission funnel (chat-attachment-admission.ts).
- Zero-byte files rendered a normal chip, then the payload assembler
silently dropped them on send; the funnel rejects them at intake with a
named toast.
* refactor(ui): drop unused exported type from attachment admission module
Prevent a completed standalone x_search request from building or caching a result after caller cancellation. Preserve the exact abort reason and force identical retries back through the provider request path.\n\nFixes #123964
resumeStoredChatOutboxes runs on every session.message/sessions.changed
event; a never-attempted durable head parked behind an active run issued a
1000-message chat.history request per transcript event only to learn the
run was still active. The session row the wakeup event itself reconciled
already answers that question, so consult it first and keep the history
fetch for attempted rows whose delivered-detection genuinely needs it.
* fix(gateway): publish approval timeout expiry to reviewer surfaces
Timer-driven timeout expiry was the only terminal approval transition never
published: forceDeny settles the durable row and local waiter, but no
exec/plugin.approval.resolved event fires, so every reviewer surface prunes
expired approvals on its own clock. With operator clock skew the web card
stays actionable after the gateway already denied (clicking Allow then
silently vanishes via the stale-conflict path), or actionable approvals are
pruned early. Add an onExpired manager hook and route it through the
existing pending-publication queue used by run-abort/authority-close, so
timeout publishes exactly like a resolve.
* test(gateway): move timeout-expiry publication coverage to a focused file
Main's growth pushed exec-approval-manager.test.ts past the counted
max-lines cap once the onExpired test landed on top; the focused sibling
file keeps the regression without a suppression.
* fix(plugins): remove isolated npm projects on uninstall
Ordinary uninstall now removes the whole owner-managed per-plugin npm project, including packed archives and transitive dependency debris, while shared roots and retained update generations keep their existing lifecycle behavior.
* fix(plugins): preserve failed install cleanup
Allow persistence-failure compensation to remove only a planner-validated isolated npm project root, while continuing to reject shared or ambiguous cleanup targets.
* feat(cua-computer): add browser action family
* refactor(cua-computer): split browser action files
* refactor(cua-computer): move the shared act-params type to the leaf module
* fix(cua-computer): carry the contract params import with the moved type
* feat(workers): install Gateway bundles on nodes
Add a private paired-channel install command backed by a one-use Gateway HTTP capability, bounded archive validation, script-disabled dependency materialization, and atomic gateway-namespaced publication. Existing device provisioning remains on the local-build path until the next cutover slice.
* docs(runners): record node bundle installer
* test(ui): add sidebar alignment capture harness
* fix(ui): align sidebar rows to shared grid
* fix(ui): distinguish sidebar owner presence
* fix(ui): visually hide sidebar pages label
* fix(ui): collapse the hidden pages head row
* fix(ui): keep the floating pages action clickable
* fix(ui): scope facepile dedup to the rendered lead and reveal the pages editor on touch
ClawSweeper P1s on #123938: the facepile unconditionally excluded the
session creator even when the lead chip showed the archivist or nothing
at all, hiding a live viewer; renderSessionLeadingState now returns the
rendered owner identity as the single dedup source. The hover-revealed
pages editor gains the standard hoverless-pointer visibility override.
* fix(ui): re-attach chat virtualizer when a foreign host re-stamps the transcript
Switching a board session from Dashboard back to Split left the docked chat
pane blank (sizer-height empty space, no rows) until an unrelated state change
re-rendered the pane or the user scrolled.
Root cause: the transcript template is stamped by openclaw-chat-sidebar-region
(it receives the chat template as a property and renders it in its own, later
update cycle), but the TanStack virtualizer only re-resolves its scroll element
inside the pane host's update. After the face-switch re-stamp no pane update
follows, so the virtualizer stayed detached: scrollElement null, scrollRect
zeroed by the hide transition, calculateRange null, zero rows painted.
Fix, at the owner (ChatSessionVirtualizerHost): attachment now follows the DOM
identity the scroll-element ref records — an identity change queues a microtask
that re-runs the virtualizer adapter's update hook. Zero-size rects from hide
transitions (display:none sidebar panels, unmount teardown) are ignored so they
can no longer wipe measured row heights or become the virtualizer viewport.
Live-verified against a remote gateway where the broken state persisted 5+
seconds pre-fix and one attachment sync healed it. Regression tests fail
pre-fix; e2e covers the deferred-latency dashboard->split flow.
* chore(ui): raise startup JS baseline for transcript virtualizer re-attach fix
Measured 333338 B gzip on PR #123974 run 31861214469 (+345 B over the base
run 31860496921 at 332993 B; prior baseline 332040 B predates recent main
drift). Absolute 350 KiB maintainer cap unchanged. Growth is the foreign-host
attachment sync in chat-transcript-controller.ts — real fix code, not
speculative surface.
Run 31861511624 made checks-ui the UI-scoped wall pole (247s job, 177s body
on hosted 4-core even in hybrid). Attempt 1 now uses the 8 vCPU class like
the ui-e2e shards; retries stay hosted.
Tonight's UI fix stream (stale-tab retry, session attention, talk
backpressure) consumed the 1 KiB tolerance and broke build-artifacts plus
both QA smoke ui:build paths on main (run 31861213699, 333309 B vs
332040+1024). Recorded with CI bytes per the checker contract; the 350 KiB
maintainer-approved ceiling still bounds cumulative creep.
The inlined isRecord in 7e54cc9d19 tripped the coercion-helper declaration
guard on main (runs 31859544700+); the dependency-light scripts/lib/
record-shared.mjs seam is the sanctioned owner for scripts that must run
without workspace package resolution.
The manifest planner closure and the protocol coverage script import only
node builtins and relative files (verified importing the full closure with
an empty node_modules under native type stripping), so push/PR preflight
drops the pnpm store restore and install (~30s off the barrier every lane
waits behind). Manual dispatches keep the tsx path for frozen targets, and
the coverage script inlines the record guard under the documented
dependency-free exception.
The store archive accretes every prior lockfile generation through
prefix-key restores (measured 2.05 GiB, ~36s restore in every hosted job);
the warmup writer now prunes to the current lockfile closure before saving.