* fix(computer-use): converge provider ref lifecycles
* test(computer-use): expect stale lifecycle code
* fix(computer-use): key native window refs on stable window identity
Peekaboo matched an existing window ref on the whole WindowMutationIdentity,
which embeds capturedBounds and isMinimized. Any move, resize, or minimize
therefore minted a new ref for the same live window, never evicted the old
one, and let the old ref keep resolving to a stale ServiceWindowInfo whose
identity and bounds were then used as the per-action expectations, so the
operator saw a downstream mismatch instead of COMPUTER_STALE_OBSERVATION.
Window refs now key on stable identity only (WindowServer id plus the owner
process generation that guards pid reuse) and refresh the stored target in
place on every discovery, so one live window keeps one ref for the whole
lifecycle generation and later checks compare against current data. The
one-instantiation generic reference store is folded back into the service as
plain state plus small concrete helpers.
The shared case table gains window_moved, which both providers must satisfy,
and the Swift side now drives the real ComputerActionServiceV2 with real
Peekaboo values instead of a toy store with an equality matcher. Its
in-flight generation case runs a real perform() whose lifecycle is revoked
mid-action rather than throwing the error it asserted.
* test(cua-computer): drive the real in-flight generation-rotation path
Global-scope composer fallback adoption coalesces sibling candidates
(bare-global, default-main, qualified-main) into one winner and deletes
the losers from chatComposerFallbackByScope — but never released their
attachment payload-store entries, unlike every clearChatComposerMemory-
Fallback caller. The dropped data URLs and object URLs leaked for the
pane's lifetime.
Release dropped candidates' payloads at the adoption site, retaining ids
still referenced by the live composer or any surviving fallback (the
same retention rule the pane-handoff owner uses).
resolveHeartbeatRunPrompt can no longer return prompt: null — the only
null producer (inferred commitments follow-ups) was deleted in
4b0151682e — so the two not-due short-circuits in
heartbeat-runner-execution.ts were dead branches. Narrow the resolution
type to string and delete both consumers.
HEARTBEAT_SKIP_LANES_BUSY had no remaining producer and
HeartbeatDeps.getCommandLaneSnapshots no prod reader (the per-lane busy
check was consolidated into requests-in-flight admission). Delete the
constant, the dep, and the tests that only exercised the dead seam;
retarget the retry-simulation tests at live retryable reasons and drop
the stale lanes-busy mention from the troubleshooting doc.
* fix(ui): reset logs when source changes
Reload the active tail without a cursor when logs.tail switches files so the Control UI never mixes sources or skips a new file prefix.
* fix(ui): reset log source after reconnect
* test(ui): harden logs lifecycle proof
* test(ui): route Logs lifecycle through real Gateway lane
consumeAskFallback gated only on decision-less resolved records, so
run-aborted cancellations and dispatch-fencing revocations — which also
end decision-less — satisfied the timed-out predicate and could be
replayed as ask-fallback. Require the expired/no-route terminal so only
genuinely unanswered approvals re-admit.
Dispatch fencing now records its closure honestly: worker-dispatch
revocation routes through forceDenyDetailed(run-aborted, cancelled)
instead of expire(), which persisted a false 'timeout' terminal.
Rider cleanup: delete the write-only PendingEntry.reject/rejectPromise
plumbing and the lookupPendingId alias (single test caller moved to
lookupApprovalId).
* fix(ui): keep chat file links whole when a line wraps
A file reference in chat prose could end a line on its file-type glyph and strand its label on the next line, splitting one reference into two fragments. The chip is now an atomic inline, so it moves to the next line whole, and it is capped at the column width so an unshortened label wraps inside the chip instead of overflowing the bubble.
Closes#123309
* fix(ui): keep the file-link glyph attached to its label on wrap
The prior fix (display: inline-block + max-width: 100% on the anchor)
stopped the whole chip from splitting across the parent's lines, but
left the glyph itself splittable from its label: the ::before glyph
was display: inline-block, an atomic inline with its own contingent
line-break opportunity, so when a long/unbreakable label couldn't fit
next to it, the UA pushed the whole label onto the next line instead
of letting overflow-wrap continue the run — stranding the glyph alone
above it. Switching the glyph to display: inline (sized via
padding-inline-start + an explicit mask size instead of width/height +
contain) removes that break opportunity; the glyph and the label's
first character now share one unbreakable run, while the rest of a
long label still wraps normally via the inherited
overflow-wrap: anywhere.
The existing wrap test couldn't have caught this: a.markdown-file-link
is inline-block, so its own getClientRects() always reports exactly
one rect regardless of how the content wraps inside it. Added a
table-driven probe (short basename, an unbroken long basename with no
natural break points, and a path with a :line suffix) that compares
the chip's own top against its first character's top via a Range,
which does see the internal split. It fails on the pre-fix CSS and
passes after.
* fix(ui): let the wrap probe infer its page.evaluate types
A single explicit type argument on page.evaluate binds Arg to void, so the
sweep's case ids arrived untyped and the shard failed to compile. Inference
reads both the argument and the result from the call.
Also condenses the layout-invariant comments on the file-link rules and the
probe to three lines each.
* test(ui): sweep the glyph probe over enormous and non-ASCII labels
A 240-character basename with no separator and a CJK label reached through a
host-local href both wrap several times inside the chip, so the first cut is
where a glyph would strand. Both fail on the pre-fix rule.
* fix(ui): give the file-link chip its own wrap policy
The atomic chip capped its box with max-width but relied on .chat-text for
overflow-wrap: anywhere. .sidebar-markdown declares no wrapping policy, so in
the Chat Detail Panel an unbreakable label painted past the capped box and the
panel's hidden horizontal overflow clipped it.
Declare the wrap policy on the shared file-link owner and sweep both renderers
in the browser probe: the separation cases now assert the painted label never
runs past the column's content edge.
* docs(plan): mark wave-5 rows landed and record Linux as partial
* docs(plan): record trust-model, tool-split, ref-convergence, and rig root cause
* docs(plan): carry wave-4 rows forward, define partial status, order rows by wave
* fix(ui): show active model context budget
* fix(ui): simplify compact model label
* fix(ui): preserve provider when matching context budget
* fix(ui): hide stale context during model switches
---------
Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com>
Mark stale terminal tabs exited when the Gateway rejects input or resize, so operators see an actionable outcome instead of silently losing keystrokes.
* fix(gateway): drop internal error class names from operator output
* test(cli): drop stale Error: prefixes from capability expectations
* test(cli): align invalid-port output with canonical formatting
* fix(errors): preserve primary structured codes
* style(cli): format invalid-port expectation
* test(cli): align shared error rendering expectations
* fix(gateway): scope error code rendering to agent failures
* test(errors): redact opt-in structured codes
* refactor(errors): keep canonical formatter callback-safe
* test(audit): drop stale Error: prefixes after formatter cleanup
PR #124336 landed audit's gateway-error rendering while this branch was
in flight, so its two new assertions were written against the prefixed
output. The canonical formatter no longer emits the generic Error:
prefix, so the expected strings are updated to match.
* fix(gateway): report device worker capacity distinctly from disconnection
This separates paired, disconnected, and connected-but-saturated device worker failures so capacity exhaustion no longer looks like disconnection. Saturated hosts now tell operators to retry after a running turn completes, and table-driven coverage asserts all three messages. AI-assisted.
* test(gateway): expect unpaired reason in startup availability probe
* feat(ui): add session icon grid picker
* fix(ui): use radio semantics for icon grid
* fix(ui): use pressed-state action-grid semantics for the icon picker
ClawSweeper: menuitemradio/radio semantics promise arrow-key selection, but arrows here only move focus (selection persists server-side, so select-on-focus would patch per keypress). Plain buttons with aria-pressed match the click/Enter-only contract.
* Control UI: unify chat rails in a tabbed panel
* fix(ui): compact chat side panel navigation
* Polish rail separators around the active tab
* Soften rail tab separators
* Inset the rail tab strip from the resize handle
* Remove the rail tab strip bottom rule
* Add compact close controls to rail tabs
* Redistribute rail tabs after closing a surface
* Restore bottom docking from the rail terminal
* Fade clipped rail tab labels at the edge
* Add drag reordering to rail tabs
* Polish terminal tab chrome
* Polish rail tab close controls
* Regularize rail tab separators
* Space rail tabs evenly
* Add feedback to inactive rail tabs
* Smooth rail tab selection
* Hide unavailable rail menu items
* Preserve browser multi-tab entry
* Support bottom-docked rail tabs
* Polish rail tab sizing
* Refine side panel actions
* Neutralize rail empty-state icons
* Separate dark rail surfaces
* Fade only clipped rail tab labels
* Compact rail header controls
* Refine the surface picker
* Raise and tighten the surface empty state
* Keep Browser and Tasks in the chat topbar
* Unify surface list cards
* Keep embedded rails inside their dock
* Reveal rail tab close actions on demand
* Show full rail labels only when clipped
* Let rail tabs use available space
* Align chat topbar surface controls
* Unify rail surface empty states
* Center rail tab glyphs
* Fade clipped rail labels smoothly
* Keep rail tab labels visible
* Tighten rail tab separator and label fade
* Keep the new-tab control beside the rail tabs
* Keep rail tab separators from reflowing on activation
* Anchor the new-tab control outside the rail tab scroll
* Scroll rail tabs at the floor instead of clipping them
* Tighten spacing between rail tabs
* Report an unusable terminal open response as a readable failure
* Keep the terminal session menu inside the panel
* Share one dock destination cluster across rails, terminal and browser
* Align the terminal mini rail with its management icons
* Tidy panel pickers, browser chrome and task detail retry
* Type the sidebar callbacks object so tsgo:ui passes
* Space out the panel picker rows
* Measure tab strip edges by rect and cancel superseded installs
* Validate every terminal session field and release unusable sessions
* Fold the terminal rail stretch rules into one
* test(ui): match rail tab checks to the tabbed panel header
Anchoring the new-tab control outside the scroll area wrapped the strip in
.side-panel__header-tabs, and every query written as a direct-child chain
started matching nothing. The assertions did not fail loudly: they compared
an empty label list, so three side-panel journeys and the background-tasks
rail went red on a selector detail rather than on behaviour.
Give the label query one named owner scoped to the panel's own header, so
Terminal and Browser strips in the panel body stay excluded while header
layout can keep moving. Freeing that width also means two short tabs now
fit where they used to be squeezed, so the closing step asserts the honest
invariant - a strip that fits again releases the fade - while the overflow
and tooltip regime stays proven earlier in the same journey.
Settle the shared side-panel helper on whichever surface renders: an empty
panel offers its type list and a populated one offers the header menu, and
probing before either existed left it waiting for a control that never
comes. Also drop a shadowed seed binding and an unused locator that oxlint
flagged in the same file.
* fix(ui): adapt session discussion slot to the unified panel layout
* test(ui): capture the bottom dock evidence with a resting divider
* fix(ui): drop the dead previousDock toggle path
* test(ui): align sibling suites with the tabbed panel decisions
* fix(ui): keep narrow-pane hides above the icon-button styling cascade
* fix(ui): create tooltip descriptions via ownerDocument
* fix(lint): merge duplicate terminal header styles and drop dead branches
* refactor(ui): extract embedded panel templates from the pane render
* refactor(ui): delete rail-era dead exports
* test(ui): repair type drift after the rebase
* fix(ui): keep the workspace toggle module-local and settle test caps
* fix(ui): give side-panel tabs their runtime and their own header actions
The tabbed side panel dropped two things the old rails owned. The discussion
tab lost its only runtime registration, so `openclaw-session-discussion` was
never defined and the tab rendered as a permanently blank box; it now loads
through the per-slot lazy runtime map like the other panel types.
Panels also have no header of their own anymore, so any action on the active
panel's content had to reach the shared header. The header now takes panel
contributed actions instead of a discussion-only open-url special case, which
brings back the side-chat "Clear thread" overflow menu — its gateway reset had
no reachable entry point at all in the embedded rail.
* test(ui): anchor the sidebar scroll proof to the panel host
The bounding contract moved from `.sidebar-column__panel` to
`.side-panel__panel`; mounting the detail panel under the removed class let it
grow instead of scroll, so the test failed for its harness rather than for the
behavior it guards.
* refactor(ui): move side-panel wiring out of the chat render monolith
`chat-pane-render.ts` crossed its line budget. The region callbacks are layout
policy, so they resolve in the layout module with the pane injecting only what
it owns, and the two panel-action producers collapse into the single header
contribution the region consumes.
* fix(ui): keep one terminal intent queue per document
A session route mounts the side-panel terminal beside the shell instance kept
for the bottom dock. Each owned a private action array over one sessionStorage
key, so their whole-array writes erased each other's intents and a freshly
mounted panel drained through a reconnect fence it never saw.
The queue, its persisted record, and the fence now belong to the document;
panels bind as executors while connected. The chat pane records a toggle intent
where it observes it, instead of parking the raw event in memory and handing it
to a panel that may not be mounted yet — a service-worker reload in that window
dropped the request, thread id and all, with nothing explaining why. That makes
the deferred-restore handshake dead weight, so it goes.
* fix(ui): let the panel that starts a terminal intent finish it
Opening a catalog terminal from a chat route mounts the side-panel terminal
right after the request, which swapped the queue's executor mid-flight and
stranded the in-flight action; the panel's own mount restore then opened a
plain session instead. The executor that began an action stays responsible for
it as long as it can still run.
* test(ui): cover closed legacy side docks
* refactor(ui): split side panel controllers
* fix(ui): stabilize embedded panel handoffs
* style(ui): format browser panel refresh
* test(ui): settle catalog prepend paint incrementally
* docs(ui): describe the unified chat side panel
* test(ui): settle catalog clock transitions
---------
Co-authored-by: Jason (Json) <263060202+fuller-stack-dev@users.noreply.github.com>
#124100's disarm-at-ACK custody rests on dispatch persisting the
referencing transcript on every post-ACK path. The blocked-turn path breaks
that: a before_agent_run block persists only the redacted block reason —
no media markers, no media facts — so the offloaded inbound refs stay
unreferenced forever (the inbound sweep is off unless attachments.ttlHours
is set). Dispatch settle now discards the prepared refs when the recorder
ended blocked, applying the same custody rule as the pre-ACK owner.
Regression test drives a blocked dispatch with an offloaded attachment and
asserts the inbound dir returns to baseline; fails pre-fix.
Pin OCM v0.2.32 so Performance validation uses upstream native .mts discovery and transitive workspace dependency handling instead of the local Bash/tsx trampoline.
Four nodes-CLI repairs at their owners:
- nodes status --last-connected joined a second pairing-scoped RPC
(node.pair.list) to reconstruct last-connection times the gateway already
records on every node.list row (lastConnectedAtMs, max of stored pairing
history and live connection, since 2dcd47d4f4). The client-side join also
preferred stored history over a live connection instead of taking the max,
and made the command fail for callers whose auth grants operator.read but
not operator.pairing. The filter now reads the recorded fact;
connectedAtMs covers gateways predating it.
- nodes list --connected blanked pending rows and then printed "Pending: 0"
while requests waited — asserting a fact it never checked. Pending rows
carry no connection state to filter on; they now always show.
- nodes remove/rename carried unreachable !nodeId guards (resolveCliNodeId
throws "node required" on blank input and every parse layer drops id-less
rows) whose dead text pointed at the wrong command (nodes pending targets
pairing requests, not paired nodes). Guards deleted; the reachable empty
--name branch now hints at nodes list.
- status.node-mode derived node-service liveness from the formatted
runtimeShort display string — parsing a fact back out of its own
projection, dead in all first-party call paths (both callers pass the
runtime object it is derived from). Field and branch deleted.
e2e test updated to pin the single-RPC shape (asserts node.pair.list is NOT
called); node-mode fixtures now use the production shape.
* refactor(anthropic): split session-catalog into concept modules
Follow the #124178 sibling seam pattern while preserving behavior. Remove the max-lines suppression and baseline entry after the split.
* refactor(anthropic): own record caches in session-catalog-discovery
The record-shaped caches (catalogDiscoveryCache, claudeSessionScanCache,
their entry types, bounds, and TTLs) are consumed only by discovery; moving
them out of scan removes the scan->discovery type-import edge flagged by the
madge topology gate and leaves scan as record-agnostic fs primitives.
* fix(state): doctor --fix loops on "migration required" for older state databases
Databases at user_version < 3 cannot complete the STRICT table rebuild when
they predate a first-use additive column such as
device_bootstrap_tokens.setup_id. The rebuild validates each table against its
full canonical column set, so it fails with:
SQLite table device_bootstrap_tokens does not match its canonical columns
(missing setup_id)
The repair runs in a single transaction, so this rolls back the audit-events-v2
work that already succeeded. The next open re-detects the legacy audit ledger
and reports that migration instead, which names a table unrelated to the real
failure and leaves doctor --fix repeating the same message. It also early
returns out of runLegacyStateMigrations, skipping the config machine-state
migration, so the gateway keeps failing config validation.
STATE_PERSISTENT_SCHEMA_COMPATIBILITY.allowedMissingColumns already treats
these columns as legitimately absent, so the persistent schema contract and the
STRICT rebuild disagreed about the same database.
Ensure the first-use additive columns immediately before the STRICT rebuild in
both paths that run it, gated on previousVersion < the STRICT schema version.
This mirrors repairLegacyGatewayRestartHandoffsForStrictMigration directly
above it. It is not a semantic change: the rebuild recreates each table from
canonical SQL that already declares these columns, so the resulting shape is
identical either way, and scoping it to pre-STRICT databases keeps the
absent-until-first-use behaviour for every normal open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(state): cover the cold-open route and session_groups first-use columns
Adds independent regression cases for the two routes the shared helper covers
but the original fixture did not exercise directly:
- session_groups.cwd / .worktree, the other first-use additive columns, so the
coverage is not specific to device_bootstrap_tokens.setup_id
- the writable cold-open path, which upgrades the same database during gateway
startup without doctor running first
Both fail on an unpatched tree with the canonical column mismatch and pass with
the fix, alongside the existing doctor-repair case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(state): enforce that first-use additive columns stay bare nullable
Materializing these columns before the STRICT rebuild is only safe while every
one of them is bare and nullable: the rebuild adds them to tables that already
hold rows, so a NOT NULL, defaulted, constrained, or key-bearing column would
fail the migration or silently rewrite existing data.
That property was documented as a convention on the definition list. Assert it
against the canonical schema instead, one case per first-use column, so adding
a non-nullable column to the list fails CI rather than pre-STRICT upgrades.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Configured agents now use their canonical direct store paths while retired and manual owners retain broad discovery. This prevents Gateway startup prewarm from rescanning every agent root once per configured agent.
config.set and config.patch each hand-rolled the same normalize ->
raw-validate -> plugin-validate -> respond-invalid pipeline (including the
duplicated errorShape/details blocks the round-5 error-text fix touched in
both places). validateSubmittedConfigOrRespond now owns it; both handlers
consume { validationCandidate, config }.
Behavior-neutral: same validators in the same order, same error shapes
(pinned by the existing 26 config tests including the round-5 regressions).
Net -11 production LOC and one place to evolve validation error content.