Commit Graph

81255 Commits

Author SHA1 Message Date
Peter Steinberger 6279636985 perf(test): remove duplicate UI place cases (#124370)
* perf(test): remove duplicate UI place cases

* test(ui): fix latent e2e timing assumptions
2026-08-15 20:21:22 -07:00
Peter Steinberger 92b6c17bb8 fix(usage): keep healthy provider usage after auth failures (#124367)
* fix(usage): isolate provider auth failures

* fix(usage): preserve direct auth failures
2026-08-15 19:59:55 -07:00
Patrick Erichsen a73249c7cd fix(ui): clarify active model context in picker (#124207)
* 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>
2026-08-15 19:57:27 -07:00
Peter Steinberger e68f3d8ad1 fix(ui): surface rejected terminal actions (#124046)
Mark stale terminal tabs exited when the Gateway rejects input or resize, so operators see an actionable outcome instead of silently losing keystrokes.
2026-08-15 19:53:47 -07:00
Peter Steinberger 4b8ca98a94 fix(gateway): omit internal class names from RPC failures (#124329)
* 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.
2026-08-15 19:47:42 -07:00
Peter Steinberger 44c5abb690 fix(gateway): report device worker capacity distinctly from disconnection (#124340)
* 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
2026-08-15 19:46:06 -07:00
Peter Steinberger b170c2c021 feat(ui): grid picker for session icons (#124335)
* 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.
2026-08-15 19:44:52 -07:00
Patrick Erichsen 1b954672b1 feat(ui): move incognito to new-session rail (#124352) 2026-08-15 19:44:08 -07:00
Peter Steinberger 6bbc57e526 refactor(types): drain chained-assertion ledger — core (#124351) 2026-08-15 19:41:31 -07:00
Vyctor H. Brzezowski aed4510bd0 improve(ui): unify chat side rails in a tabbed panel (#123874)
* 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>
2026-08-15 20:40:00 -06:00
Peter Steinberger 8d262b96cc fix(gateway): discard prepared media when a hook blocks the turn (#124353)
#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.
2026-08-15 19:39:16 -07:00
Peter Steinberger c3488150c5 refactor(computer-tool): split by responsibility (#124358) 2026-08-15 19:38:16 -07:00
Peter Steinberger 0dd8d3eafd perf(test): narrow web fetch imports (#124357) 2026-08-15 19:34:20 -07:00
Peter Steinberger 4919c7030d fix(ci): use OCM native workspace adapter (#124341)
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.
2026-08-15 19:34:14 -07:00
Peter Steinberger 289a9cabc8 docs(computer-use): state the authorization chokepoint explicitly (#124355) 2026-08-15 19:30:29 -07:00
Peter Steinberger e2e070c927 fix(ui): keep session context menus on sessions (#124344) 2026-08-15 19:24:59 -07:00
Peter Steinberger 1ab85edb0e fix(workers): bundle workspace rsync receiver (#124339) 2026-08-15 19:23:05 -07:00
Peter Steinberger ca849506f3 fix(cli): read node liveness from the recorded node.list fact and drop dead nodes-CLI guards (#124348)
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.
2026-08-15 19:19:13 -07:00
Peter Steinberger df2b83f7ae refactor(anthropic): split session catalog into concept modules (#124342)
* 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.
2026-08-15 19:18:03 -07:00
Peter Steinberger 6432ac3310 fix: agents delete leaves no stale database registry row (#124347)
* fix(agents): deregister agent databases on delete

* refactor(claws): finalize agent registry cleanup with removal state
2026-08-15 19:15:57 -07:00
Jason (Json) 5f1bbed42d fix(doctor): report missing managed local embedding setup (#123575)
* fix(gateway): expose startup blockers before cutover

* fix(gateway): include session blockers in preflight

* fix(gateway): keep preflight finding type private

* fix(gateway): preflight startup auth blockers

* fix(gateway): complete startup preflight readiness

* fix(llama-cpp): keep preflight remediation private

* fix(gateway): keep preflight passive and activation-aware

* fix(gateway): apply startup guard in preflight

* fix(gateway): align auth mode preflight

* fix(gateway): keep preflight state reads isolated

Share the read-only inspection snapshot scope across duplicated runtime chunks so blocked gateway preflight remains non-mutating when bundled provider artifacts read canonical state.

* fix(gateway): keep startup preflight passive

* fix(gateway): ignore inactive embedding owner shadows

* fix(gateway): preserve startup preflight parity

* fix(llama-cpp): keep cache inspection types private

* fix(gateway): close startup preflight parity gaps

* fix(gateway): handle uninitialized memory databases

* test(gateway): observe shell fallback portably

* fix(llama-cpp): normalize embedding model paths

* refactor(gateway): drop broad startup preflight surface

* fix(doctor): report missing managed local embedding setup

* style(memory): simplify setup enablement check

* fix(memory): keep diagnostic result type private

* fix(memory): inspect local setup with remote secret refs

* fix(memory): keep doctor index inspection immutable

* fix(memory): make readiness inspection owner-aware

* fix(doctor): mirror memory slot allowlist policy

* test(doctor): use canonical memory slot id

* fix(doctor): normalize memory provider ids

* fix(doctor): resolve external embedding readiness owner

* fix(plugins): keep embedding inspection result internal

* fix(doctor): isolate plugin state during lint

* fix(doctor): route lint metadata through snapshot

* fix(cli): keep doctor lint startup source-only

* fix(cli): keep doctor lint compile-cache free

* fix(doctor): keep local embedding readiness opt-in

* test(doctor): preserve plugin artifact roots during lint

* fix(doctor): refresh memory readiness registration

* test(doctor): type nullable provider policy mock

* fix(doctor): scope lint state snapshot to provider check

* fix(doctor): isolate selected plugin state checks

* test(doctor): restore only scoped environment

* fix(doctor): defer readiness state inspection

* fix(doctor): keep deferred config reads isolated

* fix(doctor): keep plugin state mode internal

* fix(config): preserve default plugin validation
2026-08-15 20:15:06 -06:00
Peter Steinberger 635255f4e6 perf(test): shorten Vault timeout regression (#124350) 2026-08-15 19:14:43 -07:00
Saju 300043015b fix(state): doctor --fix loops on "migration required" for older state databases (#124282)
* 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>
2026-08-15 19:08:52 -07:00
Jason (Json) 4490eaac9f feat(ui): open links in Control UI browser (#123912)
* feat(ui): open links in Control UI browser

* fix(ui): centralize Control UI link routing

* fix(ui): carry narrowed native link poster
2026-08-15 20:05:02 -06:00
Peter Steinberger 2585edba2e fix(audit): show valid values for rejected filters (#124336)
* fix(audit): validate filter flags before gateway call

* refactor(audit): keep filter constants owner-local

* fix(audit): keep cursor errors operator-facing
2026-08-15 18:53:00 -07:00
Jason (Json) 4be10d44d5 fix(ci): avoid false Crabbox auth failures during readiness (#119700)
* fix(ci): trust Crabbox doctor for broker readiness

* fix(crabbox): preserve doctor auth compatibility

* fix(crabbox): retain legacy auth validation

* fix(ci): trust structured Crabbox doctor readiness

Punchcard-Session: calm-meadow-summit-bc

* fix(ci): trust structured Crabbox doctor readiness

* fix(ci): satisfy Crabbox readiness types

* test(ci): use managed temp directory helper

* test(ci): stabilize Crabbox readiness fixtures

* test(ci): preserve temp helper baseline

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-08-15 19:47:19 -06:00
Peter Steinberger d58e378157 fix(gateway): avoid quadratic session store discovery (#124334)
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.
2026-08-15 18:45:23 -07:00
Peter Steinberger 9c6539c975 refactor(gateway): one owner for the submitted-config validation pipeline (#124331)
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.
2026-08-15 18:37:23 -07:00
Peter Steinberger 1fda883530 fix(ui): recover authorized deep links after Control UI updates (#124328)
* fix(ui): refresh stale dashboard documents

* test(ui): count recovery document requests
2026-08-15 18:32:59 -07:00
Peter Steinberger e9d70a1169 perf(test): run hidden reasoning watchdog cases concurrently (#124330) 2026-08-15 18:29:02 -07:00
Peter Steinberger 1d53211924 refactor(version): remove obsolete define injection (#124321)
* refactor(version): remove obsolete define injection

* test: drop retired version suppression expectation
2026-08-15 18:23:14 -07:00
Vito Cappello 7f93011562 fix: preserve Claude CLI prompt cache reuse across turns (#124300)
* fix: keep claude-cli prompt-cache prefix stable across turns

Claude CLI has no cache_control breakpoint, so mashed per-turn system
prompts rewrote the native prefix and burned cache hits on follow-ups.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): scope Claude prompt cache suffix

* docs: scope Claude CLI cache guidance

* test(cli): cover Claude cache prompt modes

* fix: gate Claude CLI prompt cache flag

* docs: clarify Claude CLI cache flag gate

* fix: preserve Claude CLI system prompt roles

* fix(claude-cli): close cache flag compatibility gaps

* fix(claude-cli): reject prerelease cache flag versions

---------

Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-15 18:17:17 -07:00
Peter Steinberger f870d93e2a fix(ui): no-op the second reconcile of the same sessions.changed event (#124326)
Every sessions.changed is reconciled twice — once by the capability's own
subscribeEvents handler and once by the chat page's reconcileSessionEvent —
and reconcileSessionHistory always built a fresh sessions array, so the
second, content-identical pass defeated every downstream
result === state.result publish gate and re-rendered the sidebar per event.

The merge now returns the original result identity when the merged row is
content-equal to the existing row (shallow compare with structural fallback;
rows are wire scalars/plain objects). The isOlderSessionSnapshot strict-<
guard could not catch this: the duplicate pass carries the same updatedAt.

Production +31 (the equality gate); regression test proves second-pass
identity and fails pre-fix. 9082 UI tests green.
2026-08-15 18:11:53 -07:00
Peter Steinberger a8857eec0e fix(ci): give the Vitest cache warmer the shard lanes' heap ceiling
The warmer replays the same test envelopes as the compact shard lanes
through scripts/ci-run-node-test-shard.mts, but without the lanes'
NODE_OPTIONS=--max-old-space-size=8192. Run 31874567859 lost a worker in
agents-embedded-agent-run under the default heap and the failure skipped
every cache-save post step, leaving the day's transform/compile caches
unrefreshed. Rerun 31918467031 passed, confirming load-dependence;
matching the lane environment removes the gap.
2026-08-15 18:08:23 -07:00
Peter Steinberger 60babddd62 fix(ui): stop config snapshot refreshes from eating focused in-flight edits
Main run 31914404948 (checks-ui-e2e 6/12) timed out waiting for
config.set: the test filled the Advanced settings Endpoint field ~300ms
after navigation and the captured diagnostics show the value reverted to
the loaded snapshot with no model commit. syncScalarInputIdentity resets
element.value whenever the snapshot identity changes, which also wipes a
focused field's DOM text the model has not committed yet (mid-keystroke
or mid-automation fill) when a boot-time config.get response lands.

Focused inputs with a live edit now keep their value through identity
refreshes (revalidate only); blurred fields keep the authoritative-reset
contract, pinned by the existing repeated-rows test. Regression test is
mutation-proven (fails 'local-api' != 'form-api' without the guard). The
e2e test now asserts the filled value stuck before awaiting config.set,
so a future regression fails loudly instead of a silent 30s timeout.
2026-08-15 17:54:18 -07:00
Peter Steinberger 78502eda6d feat(workers): run device sessions from Gateway bundles (#124037)
* feat(workers): run device sessions from Gateway bundles

Install the current Gateway bundle before a device environment becomes ready, verify it at attach and tunnel boundaries, launch only from the immutable namespaced bundle directory, and retire stale environments for idempotent reprovisioning. Remove the local execution mode and preserve the node-local build claim only as temporary inventory metadata for the final projection/cleanup slice.

* docs(runners): record Gateway bundle cutover

* test(ci): repair runner validation fixtures

# Conflicts:
#	src/scripts/test-projects.test.ts

* fix(workers): surface outdated node recovery

Keep legacy runner inventory diagnostic-only while exposing the update-and-reconnect action through node, environment, provider, placement, and Control UI surfaces.

* fix(workers): reject legacy inventory with recovery

* fix(workers): bundle worker deploy closure

* test(workers): close bundle cutover gates

* fix(workers): compose browser runtime at build

* fix(workers): satisfy bundle cutover gates

* fix(workers): route temp runtime through infra

* docs(workers): align bundle host guidance

* fix(ui): fence outdated session destinations
2026-08-15 17:46:44 -07:00
Peter Steinberger eb13f5719f test(plugins): deduplicate publication fixtures (#124312) 2026-08-15 17:16:07 -07:00
Vyctor H. Brzezowski 38e0ae6ec8 fix(ui): GitHub link hovercard closes before the pointer can reach it (#124274)
* fix(ui): keep the GitHub hovercard open while the pointer reaches it

Hover intent now spans the link and the portaled card: leaving one for the other keeps the card open through a bounded traversal grace, and only leaving both dismisses it. The card also receives pointer events again, so its text can be selected. Escape, click, focus, viewport, and route-removal dismissals are unchanged, and the close timer is cleared on every close and disconnect.

* fix(ui): make the GitHub hovercard title a navigable link

The preview card's title now renders as an <a> reusing the exact
activation href, opening in a new tab like other external Control UI
links (buildExternalLinkRel/EXTERNAL_LINK_TARGET). Because the card is
portaled to document.body outside the provider's delegated focus
listeners, focus landing on the title link needed its own tracking,
mirroring the existing pointerenter/pointerleave pattern; that focus
hold releases once the pointer leaves the card after a pointer-initiated
open, so clicking the title link can never leave a hover-opened card
stuck open with the mouse gone.

* test(ui): prove hovercard pointer traversal and title-link click in real Chromium

Extends the existing github-link-hovercard e2e test with two cases: one
drives real page.mouse.move coordinates from the source link, across the
gap, onto the portaled card, and confirms it stays open and only closes
once the pointer leaves both surfaces; the other clicks the card's title
link and confirms the resulting popup navigates to the item URL.

* fix(ui): make the GitHub hovercard an accessible interactive popover

The card declared role="tooltip" while owning a focusable title link. ARIA
tooltips are descriptive text: their content is flattened and unreachable, so
the link existed for the pointer only. Drop the tooltip contract instead of the
link, and give the card the popover semantics its content already needs.

The card is now a non-modal role="dialog" named by every render state, and the
trigger carries aria-haspopup/aria-expanded/aria-controls in place of the old
aria-describedby juggling. Because the card is portaled to document.body it has
no tab-sequence neighbour, so Tab from the trigger forwards into it, Tab moves
between its links natively, and Tab past either edge or Escape returns focus to
the trigger with the card closed and no reopen.

The card's other references are links too: the repo reference and the title open
the item, the author opens their profile, and a pull request's diff-size chip
deep-links to the files-changed view. The title keeps the card's only underline;
the rest stay quiet until hovered or focused. Pointer behavior is unchanged -
leaving both the link and the card still dismisses it after the traversal grace
period, with no click required.
2026-08-15 21:05:25 -03:00
Peter Steinberger 5626a79cc8 test(tooling): deduplicate updater deployment fixtures (#124304) 2026-08-15 16:49:22 -07:00
Sebastien Tardif bf4ec4b58a fix(ui): disambiguate multi-account session labels (#124228)
* fix(ui): disambiguate multi-account session labels

Parse the optional account segment on per-account-channel-peer DM and
group keys so Control UI no longer falls through to the raw routing
key. Append a non-default account as " · accountId" on label,
displayName, and fallback so two Alice rows from different Telegram
accounts stay distinguishable.

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>

* fix(ui): take the session account from the gateway row

The display resolver was reading the account out of the session key and
inferring it from label text. That missed the shipped `dm` spelling of
direct keys, invented an account-qualified group shape no key builder
emits, and treated any name ending in "(<account>)" as already
disambiguated.

The Gateway already projects the canonical `accountId` onto every session
row from the delivery route, so display consumes that and parses the key
only for panes rendered before their row arrives. Key parsing now covers
both `direct` and the pre-#11881 `dm` spelling, group keys go back to
their canonical account-less shape, and the account suffix is applied at
a single point instead of five.

`resolveChannelSessionInfo` classifies `dm` keys as channel sessions and
no longer reads a peer kind sitting in the channel slot as a channel.

* fix(ui): stop inferring session identity from rendered text

Three related text-inference defects in the same display neighborhood:

The account suffix was skipped when the resolved name already ended in
" · <account>". That reads rendered output as if it were a fact, and it
cannot tell an account apart from a user label that happens to look like
one. The suffix is now appended from the recorded account alone.

The string it was defending against came from the sidebar rename dialog,
which pre-filled with the resolved display name. Submitting persisted a
derived string as a real user label, which then outranked every later
derivation. Sidebar rows now carry the stored label alongside the
resolved one, and rename edits that, matching the Sessions page.

Channel classification accepted an account segment before any peer kind,
so `agent:<x>:<channel>:<account>:group:<id>` was filed under a channel
even though accounts qualify direct chats only and the canonical route
parser rejects the shape. The grammar now allows an account only before
`direct`/`dm`.

Resolver tests now prove the row projection is authoritative rather than
duplicating an account the key already carries: one case where only the
projection supplies it, and one where the two disagree.

* ci: retrigger after check-lint shard cancel

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>

* test(ui): prove rename opens on the stored label

The existing sidebar rename coverage uses a row that already has a stored
label, so it passes whether rename pre-fills the stored label or the
resolved display name. It could not catch the round-trip this branch
repaired.

Adds a browser case on a displayName-only row with a projected account:
the sidebar shows "Alice · cards" and the rename field opens blank. On
the parent commit the field opens holding "Alice · cards", which is the
string that used to become a real user label on submit.

* fix(ui): do not double-append a stored account suffix

A persisted label that already ends in " · accountId" must stay as-is
so "Alice · cards" does not become "Alice · cards · cards".

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>

* docs(ui): name the producer the account suffix check defends

The comment above withAccountDisambiguator claimed the rendered name is
never inspected, which the suffix check contradicts. State the real
contract instead: the account itself still comes only from the recorded
value, and the endsWith check is idempotence against the chat pane's
inline rename, which seeds its input with the rendered title and
persists a partial edit verbatim.

---------

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
Co-authored-by: vyctorbrzezowski <krzyszchweski@gmail.com>
2026-08-15 20:37:51 -03:00
Vyctor H. Brzezowski 5014839937 fix(ui): abort stale destructive session confirmations on reconnect (#124254)
* fix(ui): abort stale destructive session confirmations on reconnect

SessionDataController retires the session-mutation scope on reconnect,
capability replacement, and host disconnect, but left any open
destructive confirmation dialog in place. If the operator confirmed
after reconnect the action silently no-opped, and the dialog's global
lock stayed held until manually dismissed.

Give SessionDataController an AbortController owned by the mutation
epoch, abort it whenever the epoch advances, and expose the signal on
SidebarSessionMutationScope. Wire it into every scope-bound confirm
dialog: batch delete, single delete, cloud-worker stop, preserved-
worktree removal, and session-group delete. confirm-dialog.ts already
tears down its modal and releases the lock on an aborted signal, so a
stale dialog now dismisses itself instead of surviving the reconnect.

Fixes #124227

* fix(ui): abort chat-header confirms too and surface a retry notice

The sidebar epoch-abort wiring left the chat-header delete/fork/archive
path uncovered: it builds its own SidebarSessionMutationScope from
connectionGeneration but never handed it a signal, so a header confirm
left open across a reconnect kept the shared confirmation lock forever.
Give ChatPaneBase its own AbortController, retire it everywhere
connectionGeneration advances (reconnect and pane teardown), and wire
the signal into the header's captured scope, matching
SessionDataController's controller for the sidebar.

Separately, resolving an aborted confirm to `false` looked identical to
an ordinary cancel, so the operator's lost intent vanished silently.
Reorder every destructive confirm site to check scope staleness ahead
of the `confirmed` guard and show the existing "Gateway connection
replaced ... Try again." toast on abort, matching the pattern
deleteSessionGroup already used for the same race. This also fixes a
regression the PR had introduced in a pre-existing main test
(app-sidebar.test.ts's "keeps a reconnected group delete retryable
after its confirm opened"), which is what failed
checks-node-compact-large-22 and checks-ui on the previous head.
2026-08-15 20:18:48 -03:00
Peter Steinberger 878236cfab test(tooling): deduplicate release timeout evaluators (#124297) 2026-08-15 15:58:29 -07:00
Vyctor H. Brzezowski 94552862ad fix(ui): persist the sidebar session sort selection (#124277)
The sessions sidebar held its sort mode in component state only, so a
reload silently reverted the user's Created/Last updated/People choice
back to Created while every sibling sidebar preference persisted.

Store the mode under openclaw:sidebar:sessions:sort-mode alongside the
existing sidebar preference keys and read it back at init. Resolution of
a gateway-denied People sort now lives in one helper shared by the render
path and the setter, keeping the element net-neutral in lines.
2026-08-15 19:57:42 -03:00
Vyctor H. Brzezowski 1db8fe4d4f fix(ui): reject blank required strings in Control UI update readers (#124264)
* fix(ui): reject blank required strings in Control UI update readers

The Control UI update readers accepted `""` for fields the canonical
UpdateAvailableSchema/UpdateScheduleStateSchema declare as NonEmptyString,
so a malformed Gateway payload rendered an update banner with blank
version text.

Root cause is the reader treating "is a string" as the contract for
fields whose canonical contract is "is a non-empty string". Fixed at that
boundary with the protocol's own dependency-free `isNonEmptyProtocolString`
primitive: required NonEmptyString fields now reject the payload when
blank, and optional ones are dropped rather than discarding the whole
payload.

Deliberately does NOT re-derive the canonical schema in the browser:

- `additionalProperties: false` is a producer-side contract the Gateway
  enforces on its own outbound results (src/gateway/server-methods/update.ts).
  Rejecting unknown keys client-side would turn every additive protocol
  field into a blank overlay, since a service-worker-cached document keeps
  an older bundle across a Gateway upgrade.
- Canonical `maxLength` counts grapheme clusters (typebox 1.3.6
  guard/string.mjs), so the previous `subject.length <= 120` copy dropped
  valid emoji subjects the schema accepts. The bound is producer-side and
  is gone rather than approximated.
- typebox itself stays out of this module: it is in the Control UI startup
  graph, which has a hard gzip budget (scripts/check-control-ui-performance.mts).

Drift is now pinned in tests instead of duplicated in production: the
boundary suite runs fixtures through both the reader and Value.Check over
the canonical schemas, asserting that anything the schema accepts still
reaches the overlay.

Production LOC: net -7 against main.

* fix(ui): restore the five-entry cap on tolerant commit filtering

readUpdateAvailableValue's per-entry filter/map dropped the existing
five-commit bound while switching from all-or-nothing rejection to
tolerant filtering. The canonical schema caps commits at 5
(packages/gateway-protocol/src/schema/config.ts), and the Updates
page renders every entry this reader returns, so the cap is the
protocol's own render-side contract, not extra strictness. Slice to
MAX_COMMITS after filtering and cover an eight-entry valid payload.
2026-08-15 19:47:38 -03:00
Vyctor H. Brzezowski 5374c110fb fix(ui): make file-link label disambiguation near-linear (#124258)
* fix(ui): make file-link label disambiguation near-linear

shortestFileLabels rescanned every path list at each suffix depth to
find the shortest unambiguous label, which is quadratic in path
count. A Markdown message with thousands of distinct file references
could stall the chat renderer. Replace the repeated scan with one
reversed-segment suffix trie built per render, so each path's label
depth is found with one child lookup per depth instead of a
comparison against every other path. Output is unchanged.

Fixes #124230

* test(ui): assert file-label lookup cost stays linear

Add a deterministic performance-contract regression for
shortestFileLabels: count Map#get calls via vi.spyOn while the trie
runs at two path-count scales and assert the growth stays linear
(generous band) rather than the pre-fix O(n^2) full-list rescan's
quadratic signature. No wall-clock timing, so it can't flake under CI
load; verified locally that it fails deterministically against the
pre-fix implementation and passes clean against the fix.
2026-08-15 19:41:24 -03:00
Josh Lehman 3bb7fdd40b fix: restore frozen Codex release validation (#124204)
* oc-c45: fix frozen Codex release resolution

* fix(release): keep frozen Codex suites fail closed
2026-08-15 15:38:57 -07:00
Vyctor H. Brzezowski 8a3fff21a8 fix(ui): stop inferring session sharing identity kind from ID strings (#124257)
* fix(ui): drive session sharing icon from identity type

Remove the ID-string channel heuristic in the chat sharing menu.
Sharing identities are typed human/agent/system by the Gateway
protocol; there is no channel identity kind, so a human profile
whose ID happened to contain "channel:" was misrendered with the
channel glyph instead of the owner chip. Presentation now branches
on identity.type only.

* test(ui): describe the sharing fixture ID as opaque, not canonical

ClawSweeper flagged the comment on the channel-shaped human ID case:
it implied channel-shaped IDs are canonical for human profiles, but
durable profile IDs are generated UUIDs. The real supported path is
an opaque human identity ID (e.g. an inbound SenderId) that can
happen to contain "channel:" as a substring.
2026-08-15 19:34:15 -03:00
Vyctor H. Brzezowski 80ee4b97e5 fix(ui): use native placement for the lobster dismiss menu (#124261)
* fix(ui): use native placement for the lobster dismiss menu

The dismiss menu pre-clamped the pointer anchor against a hard-coded
264x80 guess before opening a bottom-start dropdown. Web Awesome's
dropdown enables `flip` without ever setting `flip-fallback-placements`,
so flip can never move the popup off its preferred placement; against
a bottom-start anchor pinned to the footer's bottom edge, that left
`auto-size` as the only thing that could react, shrinking the menu and
scrolling its two items whenever content grew past the guessed height.

Since the pet always sits on the footer ledge, anchor the popup with
`top-start` instead, the placement that already has room, and pass the
raw pointer position straight through. `shift` and `auto-size`, already
enabled on every wa-dropdown, then size the popup from its real content
instead of a guess that breaks under longer labels or larger type scale.

* fix(ui): correct the type-scale test and floating-ui rationale

The "enlarged type scale" e2e coverage overrode --wa-font-size-smaller/
--wa-line-height-condensed, but the dismiss menu's items render through
the app's own .session-menu__item class (layout.css), which sets an
explicit font-size from --control-ui-text-scale and wins over
wa-dropdown-item's shadow styles - the override was a no-op, which is
why the added test failed in CI. Switch it to set --control-ui-text-scale
the way bootstrap.ts does at runtime, and assert the label's computed
font-size and its own unclamped height both grow versus a baseline (the
row's 28px min-height floor otherwise hides a no-op).

Also correct the PR's floating-ui rationale: @floating-ui/core's flip
middleware does not treat an empty flip-fallback-placements as "no
fallback" - "" is JS-falsy, so it computes a real candidate list via
getExpandedPlacements. The actual reason a cramped placement can still
get shrunk instead of relocated is that wa-dropdown's auto-size (size)
middleware runs after flip and clamps whatever placement flip lands on.

* fix(ui): condense lobster dismiss-menu placement comment

ClawSweeper P3: the 12-line version-pinned comment explaining top-start
placement was the sole reason production stayed net +2. Trim it to the
repo's inline-comment doctrine (1-3 lines, why/contract/bad-outcome, no
version-pinned essay).
2026-08-15 19:28:39 -03:00
Josh Lehman 3e116952e4 fix: keep Codex plugin aligned during stable upgrades (#124209) 2026-08-15 15:27:58 -07:00
ClawSweeper 4c55629238 feat(sessions): preserve recent session history (#123987)
Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com>
2026-08-15 16:25:31 -06:00