Commit Graph

565 Commits

Author SHA1 Message Date
Peter Steinberger 3aaf13ca84 feat(ui): show activity cards for online people (#130664)
* feat(ui): show activity cards for online people

Record live person timing independently of heartbeat freshness and expose keyboard- and touch-accessible cards with visible session links. Preserve continuous online intervals across overlapping tabs without adding persistent activity history.

Closes #130649

* build(protocol): refresh generated presence timing fields

* fix(gateway): scope presence snapshots to read-access operators

Apply one recipient projection to hello, system-presence, and events. Keep person timing and time-zone details behind read access, and filter watched references with the canonical session-list policy without leaking hidden counts. Preserve idle-person metadata for readers and prevent non-reader activity-driven frames.

* fix(plugins): preserve Gateway capability load context

Carry the owning registry's metadata generation, workspace, install records, and built-artifact preference through capability discovery. Preserve standalone source loading and existing speech eligibility while avoiding synchronous source transforms on cold Talk catalog requests.

Fixes #130777

* test: align integrated presence and capability coverage

Keep the recipient matrix in its timing-aware owner, close the suite-owned Gateway for shutdown proof, and spy on the canonical install-record reader. Remove the retired private carrier's assertion allowance.

* fix(gateway): broadcast presence when clients connect

Publish completed connection rows through the canonical scoped broadcaster so established readers see first connections and reconnects without waiting for profile edits or activity. Preserve shared online intervals and reset them only after the final socket closes.

* fix(ci): align presence fixtures and sidebar type ownership

Reuse the canonical sidebar host type to remove the type-only lazy-runtime cycle. Assert omitted scopes through hello auth and denied presence reads, and supply presence version callbacks in the shared WebSocket context fixture. Retain startup admission and drain assertions without changing deadlines.

* fix(ui): retain latest scroll commands until their destination

Do not treat the virtualizer's idle debounce or a decreasing offset as reader takeover. Settle at the actual DOM end within one pixel, retaining the separate eight-pixel UI-follow policy and explicit input cancellation. Cover retargeted idle delivery and near-end precision with the real dependency, synchronize pointer baseline capture, and type the existing composer snapshot as its textarea contract.

Fixes #130892
2026-08-27 08:07:22 -07:00
Peter Steinberger 0dbd5c81d5 feat(plugins): one consent screen for plugin capabilities, bound to the reviewed artifact (#130168)
* feat(plugins): surface plugin capability consent in Control UI and CLI

Adds plugins.inspect (declared manifest surface, operator grants, install
provenance/integrity, ClawHub trust), a Control UI consent dialog on install
and external-plugin enable, a server-side acceptance gate persisted on the
install record, artifact-anchored widen diffing, and --accept-capabilities
for non-interactive CLI use.

NOT READY TO LAND: autoreview found critical gaps (see PR notes) — the
declared surface omits 20 of 21 contract families, native plugins always
report zero hooks, several install/enable paths bypass the gate, and the
acknowledgment is not bound to the reviewed surface.

* refactor(plugins): bind capability consent to the reviewed surface

Collapses the consent error payload to the fields the client cannot fetch
(reviewToken, widened, acceptedAt) and pulls identity/declared/grants/source/
trust from plugins.inspect, shrinking the registry-free protocol reader from
395 to 91 lines and removing its divergence from the closed schema.

Acknowledgment now carries the SHA-256 reviewToken of the surface the operator
saw; the server recomputes the final staged artifact's surface and rejects any
mismatch before persisting acceptance. That closes review-then-swap, laundering
of forged acceptance through an unchanged update, and cross-artifact replay.

All 22 manifest contract families are now declared, hashed and diffed, so a
privileged family such as gatewayMethodDispatch can no longer be added without
re-consent. Consent reads the manifest runtime discovery will execute, ambiguous
install ownership fails closed, integrity resolution has one owner and no longer
labels npm SHA-1 shasums as SHA-256, and code plugins disclose that hooks
register at runtime instead of rendering an empty "no hooks" row.

* fix(gateway): register plugins.inspect in method inventories and regenerate protocol

Adds plugins.inspect to the advertised-method inventories (widening the
fixed-size slice windows so older indices stay stable), regenerates the Kotlin
protocol bindings, drops an unused exported type, and replaces two nested
conditional spreads with a plain conditional.

* refactor(plugins): split oversized consent modules and clear lint findings

Extracts the MCP controller out of the plugins page, unchanged-install
reconciliation out of update-installed, and the install lifecycle suite out of
the management-service tests, bringing all three back under the max-lines limit
without suppressions. Also renames a shadowed binding, drops an unnecessary
generic, removes a spread-to-modify in a map, and types catch callbacks as
unknown.

* chore(protocol): regenerate Kotlin bindings after rebase

* feat(plugins): let chat /plugins install review and accept capabilities

The consent gate applies to chat installs too, but the command had no way to
give consent, so external installs dead-ended on a CLI-only flag. Chat now
replies with the plugin's declared capability surface and the exact command to
rerun, and accepts a trailing --accept-capabilities mirroring the existing
--force acknowledgement. ClawHub trust acknowledgement stays CLI-only.

Staged-artifact verification is unchanged: the reviewToken is still checked
against the final artifact before acceptance is recorded.

* refactor(plugins): single-source the declared-surface groups and manifest precedence

The ordered capability group list was defined independently in the consent
engine, the protocol error reader, the CLI formatter and the Control UI, so a
new contract family had to be added in four places with nothing enforcing it.
All four now derive from one canonical list in the protocol schema with a
compile-time exhaustiveness guard.

Native-versus-bundle manifest precedence is centralized in one helper that both
discovery and staged consent call, so the two cannot drift again — that
divergence was a real bug where consent read one manifest and the runtime
executed another.

Also documents that carrying acceptance forward requires pinned artifact
integrity, so integrity-less sources such as local paths ask on every install.

* fix(plugins): enforce reviewed consent across activation flows

Route setup, repair, linked installs, updates, and chat activation through artifact-bound capability consent. Reuse canonical package discovery and recheck staged activation before config publication. Invalidate stale Control UI review requests on reconnect.

Verified focused owner and sibling tests, runtime rebuild, and real isolated CLI/Gateway install, inspect, enable, widening, and stale-token rejection flows.

* test(plugins): cover beta installs through capability consent

* test(plugins): align consent fixtures with staged artifacts

* fix(ui): review staged plugin capabilities once

* test(ui): inline the remaining plugin consent confirmation

* test(plugins): verify consent with deferred install transactions

* refactor(setup): share inference execution plan construction

* test(ui): settle applied config before deferring refresh

* fix(plugins): protect consent provenance and reuse acceptance
2026-08-27 02:58:07 -07:00
Peter Steinberger 63e6869f6d fix(cron): persist delivery suppression reason and log strict failures (#130811) 2026-08-27 01:22:40 -07:00
Peter Steinberger e4d602c6f3 fix(worker): keep background servers alive after replies (#130733)
* fix(worker): keep background servers alive between turns

Retain the supervised worker environment while background execs are live,
while recreating turn admission, tools, connections, and authorization for
each turn. Keep physical worker ownership separate from bounded turn
receipts so cancellation, restart recovery, and environment retirement
cannot discard a still-running worker.

Preserve process polling across turns, release turn callbacks, join exec
finalization before environment cleanup, and require exact worker stop
confirmation unless a dedicated provider proves the machine is gone.

Fixes #130450.

* fix(worker): fence revoked placements and verify retained lifetimes

Record provider-loss teardown intent before remote stop, prevent pending cleanup
from being recovered as an active placement, and preserve exact physical ownership
until stop is confirmed. Keep draining state when the provider becomes unavailable.

Remove the ownership/lifecycle import cycle and align workspace-retention proof
with physical teardown. Exercise missing-capability rejection and reconnect on
real Gateway/node wire, preserving scenario and cleanup failures in the harness.

* test(worker): verify durable startup revocation cleanup
2026-08-27 00:35:19 -07:00
Peter Steinberger a4c01c1a77 feat(ui): save appearance preferences per user profile (#130340)
* feat(ui): save appearance preferences per user profile

When a Control UI connection is bound to an authenticated user profile
(trusted-proxy, Tailscale, GitHub identity), theme, theme mode, and accent
become per-profile: stored in the existing user_preferences KV store via
users.prefs.set, overriding gateway-wide ui.prefs, with live cross-device
updates over a new additive users.prefs.changed event scoped to the same
merged profile. Restore default deletes only the profile key and falls back
to the gateway-wide value. talk.config projects the caller's profile accent
for native clients. Profile-less connections (token/password/none auth)
keep the existing gateway-config behavior byte-identically.

Release-note context: on multi-person gateways appearance choices are now
personal and follow you across devices; an admin restyling their own UI no
longer reskins the whole team.

* chore(protocol): allowlist users.prefs.changed for mobile clients

iOS and Android resolve the profile accent through talk.config on connect
and config refresh; live per-profile appearance push on natives is a named
follow-up.

* chore(protocol): regenerate Swift/Kotlin models for users.prefs.changed

* fix(ui): honor explicit defaults and identity switches for profile appearance

Addresses the three ClawSweeper review findings:
- resetValue for profile-bound appearance keys is the deletion fallback
  (gateway value), so an explicit product-default selection persists as a
  profile write instead of being misclassified as a reset.
- An identity switch between two known scopes forces a full appearance
  reconcile and clears appearance keys the returning identity never set,
  so a shared browser cannot keep the previous profile's look (boot keeps
  the last-seen shortcut).
- talk.config joins the profile-dependent dispatch gate so a pending
  GitHub identity sync completes before the profile accent is projected;
  token clients pass through untouched.

* chore(ui): satisfy assertion-safety ratchet and docs formatting

* refactor(ui): extract server-prefs storage primitives

server-prefs.ts crossed the 700-line cap after the review fixes; the
stateless localStorage persistence primitives move to
server-prefs-storage.ts (no behavior change). Shrink-prunes the
assertion-safety baseline for the moved casts.

* fix(ui): keep imported custom themes out of profile storage

Custom palettes are browser-local by contract, so a profile must never
carry theme=custom to a browser that cannot render it. The wire contract
drops custom from the storable theme values (readers self-heal any stored
value), a profile-bound custom selection stays retained browser-local
(including the offline-queued path), and the exhaustive theme test now
encodes the exception.
2026-08-26 16:48:26 -07:00
Peter Steinberger 395e5db41b chore(deps): refresh dependencies after seven-day cooldown (#130296)
* chore(deps): refresh cooled npm and plugin dependencies

* chore(deps): refresh cooled build and workflow tooling

* chore(deps): retain formatter compatibility

* chore(deps): retain lint compatibility
2026-08-26 16:13:18 -07:00
Vyctor H. Brzezowski f010c00ace fix(ui): distinguish inherited model defaults from session pins (#120805)
* fix(ui): distinguish inherited model defaults from session pins

The Gateway session projection collapsed inherited agent defaults and
persisted conversation pins into the same effective model fields, so the
Control UI picker marked a row Default while its footer simultaneously
called it a session override and offered a meaningless reset.

Project the authoritative persisted provenance instead: the session owner
resolves `modelOverrideSource` (normalizing entries written before source
tracking), `buildGatewaySessionRow` and the session change event carry the
additive `"user" | "auto" | null` protocol field, and the Control UI treats
only `"user"` as a conversation pin.

Closes #120752

* test(ui): scope model provenance to active pane

* fix(ui): derive the model picker's inherited sentinel from recorded provenance

Deriving "inherits the agent default" from the effective model matching the
default meant a session pinned to model X stopped reading as pinned once the
agent default moved to X. The picker then rendered the default row as already
selected, so clicking it matched its own commit value and was swallowed, leaving
the stored pin unreachable from the UI.

Project the session store's existing modelOverrideSource onto the gateway row and
let the UI resolve a closed selection source (inherited/pinned/fallback) from it,
falling back to the old equality guess only for gateways too old to send the
marker. Preserve the null tombstone in reconcile so a cleared pin stays
distinguishable from that silence.

* test(ui): pin the model picker's digit-key ownership

The numbered keycaps and the digit handler are paired by search focus, but the
guard that keeps digits in the search input had no coverage outside the browser
e2e lane. Add a unit regression for both halves and record the contract at the
numbering site, where reviewers keep reading the two gates as unrelated.

* fix(ui): keep session model selection provenance authoritative

Project saved model provenance through Gateway rows and events, retire temporary UI claims at their owner, and use canonical refresh after slash and status model changes. Preserve model intent through rollback and deletion without retaining a public cache setter.

Co-authored-by: Vyctor H. Brzezowski <krzyszchweski@gmail.com>

* refactor(ui): keep recovery with scoped session operations

Preserve recovery notifications, visible errors, and connection checks while keeping the optimistic mutation owner within its module limit.

Co-authored-by: Vyctor H. Brzezowski <krzyszchweski@gmail.com>

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-26 15:02:54 -07:00
Peter Steinberger a482a50cda perf(ui): bootstrap session rosters in one request (#130294)
* perf(ui): collapse session roster bootstrap RPCs

Amp-Thread-ID: https://ampcode.com/threads/T-01a037b3-be95-76ea-8fb3-75979929acf1

* fix(gateway): isolate and bound session roster bootstrap

Scope cached projections to the authenticated viewer, apply startup admission
consistently, and preserve the independent owner and shared preview budgets.
Regenerate the Swift client and document the bootstrap and cursor contract.
Publish Gateway test configuration atomically to remove the deferred-reader race.

Co-authored-by: Amp <amp@ampcode.com>

* test(gateway): extract session roster cache fixtures

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-08-26 14:34:48 -07:00
Peter Steinberger c3ea9775ca feat(portals): expose portals to sessions on node-backed cloud workers (#130105)
* feat(portals): expose portals to sessions on node-backed cloud workers

Worker-turn sessions placed on cloud workers (bundled Crabbox) can now open
portals. The portal tool joins the closed worker session-tool catalog and
executes on the Gateway, which mints a single-use ticket per proxied
connection; the enrolled node redeems it over a TLS-pinned WebSocket via the
new private worker.portal.stream.v1 command and splices to the worker's
loopback port. The desktop stream broker is generalized with discriminated
desktop/portal tickets, and shared node stream transport is extracted from
the desktop stream command. Portal availability is negotiated via a new
gateway hello capability plus the node's reconnect-scoped
workerHost.portalStream declaration, so old node bundles simply do not
receive the tool. Environment teardown and owner-epoch replacement close a
worker's portals; node disconnects destroy live streams while the listener
keeps serving the waiting page.

Live-proven on a Crabbox AWS worker: agent opened a portal over the worker
protocol, python3 -m http.server on the box served through the Gateway
portal URL with token auth intact.

* fix(portals): break portal proxy/service type cycle; regenerate Swift protocol models

* fix(portals): drop dead PortalTarget re-export; fix test import

* fix(portals): only tear down turn-created portals on post-open authority loss

ClawSweeper finding: a revoked turn's duplicate open could close the live
portal a still-authorized predecessor established, because service.open
returns the existing same-environment portal and the executor's error path
closed whatever id it received. portal.open now reports created-vs-reused;
the executor tears down only portals it created and closes its redundant
carrier handle whenever an existing portal is reused.

* feat(portals): fence portal reuse behind the caller's live-authority assert

Rank-up move from review: portal.open revalidates the caller inside the
serialized operation before a reuse can mutate a live portal's metadata,
mirroring close(assertCurrent). Regression test proves a lapsed caller
cannot rewrite a reused portal's title.

* test(portals): expect assertCurrent in worker portal open call

* refactor(gateway): extract worker request dispatch into its own module

worker-connection.ts crossed the 700-line ratchet after rebasing onto
current main; dispatchWorkerRequest and its rejection helper move to
worker-connection-dispatch.ts unchanged (434 + 329 lines).
2026-08-26 12:51:48 -07:00
Peter Steinberger 99a02bf115 feat(approvals): typed approval scope summaries on channel cards (#130116)
* feat(approvals): typed approval scope summaries on channel cards

Approval owners can attach a closed ApprovalScope union (message-send,
payment, external-post) describing an action's blast radius. The gateway
sanitizes it once at the producer boundary, the core view model renders a
Scope metadata row so Slack/Discord/Google Chat cards show it unchanged,
shared text builders cover Telegram/WhatsApp/Signal/iMessage/Matrix, and
the durable presentation carries it additively for operator surfaces.
Scope is display-only, never authorization; missing scope keeps today's
cards.

* fix(approvals): emit native ApprovalScope union and clamp recipient previews

Name the three scope variants as registered protocol schemas so the Swift
generator emits the ApprovalScope discriminated union the presentation
structs reference, and commit the regenerated GatewayModels.swift. Clamp
recipient previews to the declared recipientCount at the sanitize boundary
so a count of 1 with 2 previews can no longer render inconsistently.
Addresses both ClawSweeper findings on #130116.

* refactor(approvals): extract text sanitizer to break the exec-approvals import cycle

check:architecture flagged approval-scope joining the exec-approvals SCC
through exec-approval-command-display. Move the self-contained display
sanitizer into a leaf module (exec-approval-text-sanitize) with no
exec-approvals imports and migrate all sanitize importers; command-display
keeps only the payload-typed command/preview resolver.

* chore(plugin-sdk): ratchet public surface budgets down after sanitizer extraction

The approval display sanitizers left the publicly reachable SDK graph when
they moved to the exec-approval-text-sanitize leaf: exports 4343 -> 4338,
callable exports 2582 -> 2578. Shrink-only budget pin.
2026-08-26 12:13:49 -07:00
Peter Steinberger 3601020fb4 fix(gateway): prevent mismatched core command payload types (#130241)
* fix(gateway): enforce schema-typed core request handlers

* fix(gateway): preserve protocol and type safety guards

* test(gateway): await setup admission owner settlement
2026-08-26 11:08:17 -07:00
Peter Steinberger b6662bf157 fix(ui): show sessions while remote projects clone (#130192)
* fix(ui): show sessions while projects prepare

Create remote-project sessions before cloning, expose workspace preparation progress, and retain private restart-safe project intent until the workspace binds.

* fix(ui): materialize remote projects before worktree sessions

* fix(protocol): refresh session create clients
2026-08-26 10:38:29 -07:00
Josh Avant e6ed7e30cc feat(audit): record session action decisions (#129093)
* feat(audit): record session action decisions

* fix(protocol): preserve session sharing client compatibility
2026-08-26 08:21:37 -07:00
Peter Steinberger ef95d8f55e feat(secrets): agent-requested credentials the model never sees (#129670)
* feat(secrets): agent-requested credentials the model never sees

The new main-session secrets tool lets the agent request a credential by
name: the human enters the value in a masked question card (Control UI,
/ask/<id> deep link, iOS/macOS/Android), and the gateway diverts the
answer straight into the shared secret store at question.resolve. The
record, broadcast, waitAnswer, tool result, transcript, and model context
only ever carry a synthetic stored marker.

- protocol: additive secretStore binding, secretStoreExisting replacement
  metadata, and resolve-time secretStoreAllowedHosts (since 2026.8)
- gateway: store-bound question validation, admin-gated minting (blocks
  questions-scope self-answer escalation past secrets.store.set), shared
  redaction-first store write service reused by secrets.store.set
- tool: secrets request/list/delete; write-only by design, delete carries
  verified agent runtime identity; channel delivery is link-only so chat
  text is never captured as a secret
- Control UI: masked composer card with requester identity, store banner,
  editable allowed hosts, replacement warning, retry-on-validation-error,
  a standalone /ask/<id> page, and a named startup-JS baseline bump
- mobile: SecureField / password transformation for isSecret questions,
  no answer echo in terminal summaries; new native string registered in
  the locale-refresh inventory (generated artifacts stay workflow-owned)
- regression: claimed harness secret input stays out of session transcripts

Live-proven on an isolated dev gateway: real model turn, masked entry via
Playwright, value present only in secret_store_entries, absent from every
transcript, log, and the DOM.

* chore(protocol): regenerate protocol models and tool display

* fix(cli): read image string options through a typed helper

PR #129463 added four commander option narrowings in image.ts without
SAFETY coverage, leaving the assertion-safety ratchet red (21 > 17) for
every branch on current main. Replace the casts with a typeof-checked
read so the assertions are removed rather than annotated; each value is
still validated by its normalizer. SAFETY comments cannot work in this
file: the ratchet's raw scanner never rescans template tokens, so
comments after the first substitution template are unreadable to it.

* chore(protocol): refresh Swift models against current main

* chore(i18n): re-baseline the native inventory on current main

* docs(secrets): state the default-on tool policy and how to disable it

* fix(secrets): tell the model what the store actually does

The shipped tool description named the three actions and nothing else,
and no parameter carried a description. The model could not tell that
request blocks a human, that reason is shown to that human, what secret
and env select, or - the silent-failure case - that a secret stored with
no allowedHosts can never be substituted, so a successful request could
produce a permanently unusable credential. Move the description to the
presets module beside ask_user and document every parameter.

* refactor(agents): share one blocking-question lifecycle between tools

ask_user and secrets each carried their own registration, wait, and
cancel logic, and they had diverged: ask_user recovers an answer that
lands between its wait timeout and the cancel, while secrets discarded
it and reported no_answer even though the Gateway had already stored the
credential. One shared canceller and answer reader fixes that race for
both, folds the two divergent gateway-call types into one, and drops two
type assertions in favour of the canonical record guard (ask_user's
assertion baseline shrinks 11 -> 8).

Net +49 production lines: the shared module costs more than the
duplication it removes, and buys the correctness fix plus a single owner
for question lifecycle.

* fix(ui): keep the allowed-hosts field readable as an input

Main's composer restructure moved the free-text input styling into the
option-row context, so the store-request hosts field - which sits outside
a row - lost its border and read as static text. It is the one field the
operator is meant to review and edit before releasing a credential, so
give it its own border and focus ring.

* fix(secrets): close two credential-boundary holes in agent requests

Requests are now protected-secret only. list renders env values, so an
agent could request kind=env, watch a human type it into a masked box
under a no-visibility promise, then read it straight back; the tool text
even claimed values are never returned. Environment values stay operator
-set in Settings or the CLI, where they are agent-readable by design.

Store-bound questions are also bound to the run that requested them. The
resolve path authorized only the answering client, so a terminated or
replaced agent run could still have a credential written on its behalf -
the recorded runId was provenance, not closure-bound authority. Minting
now requires a runId and resolution revalidates that exact live run
immediately before the store write, with no await in between, failing
closed as QUESTION_REQUESTER_INACTIVE.

Both reported by ClawSweeper as P1 credential-boundary findings.
2026-08-26 08:10:16 -07:00
Peter Steinberger c91edb5cd5 fix(ui): restore visible gateway status in the sidebar footer and announce restarts (#130025)
* fix(ui): restore visible gateway offline status and announce restarts in the sidebar footer

The sidebar footer has been the canonical offline indicator since #112600
(which deleted the top connection banner in its favor), but #125070's footer
compaction made it sr-only + aria-hidden - invisible to everyone. Restore the
visible red offline pill (click-to-retry, queued count, redacted error
tooltip) and add a restart-aware amber state: the gateway already broadcasts
shutdown { restartExpectedMs } before restarting and refuses drain-phase work
with reason "gateway-restarting", so the Control UI now shows "Restarting..."
immediately - before the socket drops - and degrades to the offline pill when
a restart overruns max(3x restartExpectedMs, 15s).

One shared renderSidebarConnectionStatus serves the main and settings
sidebars; the dead sr-only spans and clipped live region are deleted. The
drain reason strings become shared gateway-protocol constants. The e2e mock
now passes Vite's vite-hmr client socket through to the native WebSocket so
going offline in source-served suites no longer triggers a dev-client page
reload mid-test.

* refactor(ui): delete dead sr-only footer subtitle spans and fit the startup budget

The connected-state identity subtitle spans (gateway health dot, name,
build subtitle) were the same sr-only + aria-hidden dead-markup class the
previous commit fixed for offline: invisible to sighted users and screen
readers, with the identity button's aria-label already carrying the same
information. Delete them plus their orphaned CSS; tests now assert the
aria-label contract.

The e2e mock's Vite dev-client socket passthrough keys on the vite-hmr
subprotocol. The gateway store drops validation the shared timer clamp
already owns and re-arms the restart deadline on every fresh drain
rejection (server still says restarting, so amber stays honest). The
startup-budget baseline refresh (340901 -> 341446, jitter-safe max of
three builds) records the ~530 B this feature legitimately adds over the
512 B per-change tolerance; reason stored in the baseline file.

* fix(ui): keep ordinary gateway stops on the offline pill path

ClawSweeper finding: every shutdown broadcast armed the amber Restarting
state, so a deliberate stop hid the Offline/Retry action for the 15s
deadline floor. The amber state now arms only when the broadcast carries a
numeric restartExpectedMs; ordinary stops flow through the normal offline
pill. The server omits restartExpectedMs on non-restart shutdowns instead
of broadcasting null, aligning the wire with ShutdownEventSchema's
optional integer.

Also rewires build-info-unicode.e2e to the identity aria-label (the
visible subtitle span it asserted was deleted as dead markup) and splits
the restart-state store tests into gateway-store.restart.test.ts with a
shared test-support harness to respect the max-lines cap.

* chore(ui): tighten startup gzip baseline after composer-queue rebase

The combined tree measures 340754-340770 B across builds, below the
341489 B baseline main recorded for the composer stack; pin 340810 B
(max observed + jitter headroom) so the ratchet reflects reality.

* fix(ui): unexport the test-support fake gateway client (knip dead export)
2026-08-26 07:31:30 -07:00
Ayaan Zaidi 6a8278d822 feat(cron): schedule the skill collection review as a system-owned job (#130030)
* feat(skills): expose the collection review as a single cron-driven run

The weekly Skill Workshop collection review lived in a bespoke gateway
maintenance loop with its own interval, due-state check, and gateway admission
wrapper. Replace it with `runSkillCollectionReviewForAgent`, one run that keeps
the workspace claim, identity check, and status recording and reports
`{ status, summary }` to its caller. The cron timer that will own the schedule
already runs under gateway root work admission, so the extra wrapper and its
drain test go away.

* feat(cron): schedule the skill collection review as a system-owned job

Fold the weekly Skill Workshop review into cron the same way the heartbeat
monitor is: a `skillCollectionReview` payload kind, one system-owned job per
workspace (first agent of each canonical workspace) reconciled at gateway
startup, enabled only when the workshop runs in `auto` mode, on a 7-day
`every` schedule with the heartbeat phase spread. The timer invokes the
review runner directly; cron clients cannot create, patch, or remove
system-owned payloads, and the job shows up in `openclaw cron list`.

* fix(cron): warn when skill reviews are disabled

* fix(cron): preserve skill review failure details

* fix(cron): narrow serialized payload variants

* test(cron): tighten skill review coverage

* fix(gateway): reconcile skill reviews on config reload

* fix(cron): fence revoked skill reviews

* fix(cron): satisfy system payload type coverage

* fix(ui): protect system-owned cron jobs

* test(gateway): prove review revocation fence

* fix(gateway): revoke skill reviews before config publish
2026-08-26 18:43:54 +05:30
RoboClaw 44d60a29e6 fix: preserve unread reminder for open sessions (#129386)
* fix: preserve manual unread markers in open sessions

* fix: distinguish explicit session reads

* fix(ui): gate unread contract on gateway capability

* perf(ui): keep server capabilities out of startup bundle

* test(gateway): keep agent fixtures roster-consistent

* fix(sessions): preserve legacy read compatibility

* test(gateway): type agent fixture configs

* fix(ui): remove unread gateway fallback

Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>

* test(infra): avoid fixed SSH tunnel port

* fix(ui): acknowledge unread after history commit

* docs: clarify unread upgrade boundary

* test(ui): drive mobile session menu by tap

* fix(ios): remove stale read reconciliation call

---------

Co-authored-by: roboclaw-bot <309084314+roboclaw-bot@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-08-26 05:04:39 -07:00
Peter Steinberger 093166c7d9 feat(gateway): add renewable cooperative suspension draining (#130003)
* feat(gateway): add renewable suspension draining

* fix(gateway): satisfy drain ownership checks

* fix(gateway): retain pending question drain owner
2026-08-26 03:46:16 -07:00
Peter Steinberger 4bd8859126 fix(skills): record skill usage again and retire dead curator tables (#129769)
* fix(scripts): see SAFETY comments after template substitutions

The assertion ratchet scanned each file with a raw ts.createScanner, which
reads the `}` closing a template-literal substitution as a block close. The
scanner desynced there, so every `// SAFETY:` comment after a file's first
`${...}` was invisible and its annotated assertions were counted as bare.

Track substitution brace depth and rescan the closing brace the way the parser
does. The fix uncovers already-annotated assertions in eight files, so the
baseline shrinks accordingly.

* fix(skills): record skill usage again and retire dead curator tables

Skill lifecycle curation shipped in 2026.7.1 with two producers: a trusted
`skill.used` consumer writing `skill_usage`, and a daily sweep aging skills
into `skill_lifecycle`. The weekly collection review (#121653) replaced that
policy and deleted both producers, but left every reader in place. Since then
`skill_usage` has had no writer, so curator status reported `lastUsedAtMs:
null` and `useCount: 0` forever, and `skill_lifecycle` had no writer either, so
pin/unpin/restore either threw "not found" on fresh installs or, worse,
"succeeded" on upgraded ones while gating nothing at all.
`skill_workshop_proposal_origin_runs` was never read in any revision; proposal
provenance is authoritative in `record_json`.

Restore the usage producer at its owner and retire what has no owner:

- `skill.used` events populate `skill_usage` again, registered with the
  collection-review maintenance it belongs beside. Curator status derives
  curated skills from applied Workshop create proposals and reports real usage.
- Weekly review receives bounded `useCount` / `lastUsedDaysAgo` evidence, with
  prompt text stating usage supports keeping a procedure and never alone
  justifies a drop.
- State schema 10 drops `skill_lifecycle` and
  `skill_workshop_proposal_origin_runs`. Previously archived skills return to
  the active collection, where review judges them by content; the migration
  logs how many. Reconcile now clears usage only for skills it actually drops.
- `skills.curator.pin`/`unpin`/`restore` stay registered for existing clients
  but fail with an explicit retirement message instead of silently doing
  nothing.

Retirement code moves to openclaw-state-db-table-retirements.ts to keep the
schema-repair module under max-lines; that split is a pure relocation.

Production delta is +23 raw: about -46 for the change itself, +44 for the file
split, +25 for the ratchet fix in the previous commit.
2026-08-26 01:06:06 -07:00
Josh Lehman 2dbaeef693 fix(ui): keep active commentary after session navigation (#129640)
* fix(ui): restore active commentary after navigation

* test(ui): wait for responsive activity layout
2026-08-26 00:27:41 +00:00
Peter Steinberger 473b4f19e3 feat(approvals): scoped standing grants make recurring cron automations approvable once (#129526)
* feat(approvals): mint scoped standing grants for cron allow-always

When an operator resolves allow-always for an approval raised by a cron
job's isolated run, the Gateway now mints a scoped standing grant in the
same SQLite transaction that resolves the approval, instead of writing an
unbounded command digest into the JSON allowlist. Subsequent occurrences
of that job execute the exact approved operation (command text, cwd, env
hash) without prompting while the grant revalidates against authoritative
rows: 30-day expiry, revocation, the cron job still existing with the same
config revision, and the minting approval row still holding allow-always
all fail closed back to the normal prompt. Non-cron allow-always behavior
is unchanged.

- New first-use lazy STRICT table operator_approval_standing_grants in the
  shared state DB (declared canonically, no schema-version bump; older and
  downgraded readers stay valid without it).
- The cron run owner records run -> {agent, job, config revision} in a
  process-local registry at run start; exec.approval.request stamps the
  cron source and exact operation binding onto the approval at creation,
  so nothing is ever inferred from session keys or run ids.
- The gateway exec host consults grants only when policy would prompt;
  ask=always, security=deny, mutable file operands, heredoc, strict
  inline-eval, and audit-suppression approvals keep prompting. Grant use
  updates last_used_at_ms/use_count and emits the exec approval security
  event with the grant and minting approval as lineage.
- Abort-wins guard: a run with an abort tombstone never mints.

* feat(approvals): deliver cron exec approvals to approval clients and wait inline

The standing-grant mint path was unreachable end-to-end: #128031 made
cron approval requests register with delivery fully suppressed, so the
shared owner expired them as no-approval-route within milliseconds, and
even a delivered card would have died seconds later when the isolated
run finalized on the approval-pending handoff and authority-close
cancelled the parked approval.

Cron approval requests now carry deliverToApprovalClientsOnly: the
shared delivery owner broadcasts them to connected websocket approval
clients (Control UI, TUI) but skips internal chat approval runtimes,
forwarder/iOS delivery, and turn-source routes, so the per-occurrence
chat spam #128031 removed stays removed. With no approval client
connected, the request still expires no-route into the existing
headless denial. The gateway exec host additionally waits inline for
cron-triggered approvals (the same treatment native chat channels got
in #93918), keeping the isolated run and its delegated authority alive
for the full approval window; cron jobs are single-flight, so at most
one card per job is pending at a time and allow-always ends the
recurrence by minting the standing grant.

Live-proven on a hermetic gateway: card delivered with ~30-minute
window, run waited 72s for the operator click, allow-always executed
the occurrence and minted the grant with no JSON allowlist digest,
the next occurrence ran promptless (use_count 1, no new approval row),
and editing the job failed closed back to a fresh prompt.

* chore(protocol): regenerate Swift models for deliverToApprovalClientsOnly

* fix(approvals): consume standing grants at the spawn boundary; keep node cron headless

Review findings from ClawSweeper on #129526:

- Grant authority is now recorded only at the final effect. The consult
  path validates without recording a use and returns a
  revalidateBeforeExecution closure (the mutable-file-binding seam) that
  consumes the grant immediately before runExecProcess; any invalidation
  during awaited pre-spawn work (job edit/delete, revocation, parent
  approval reversal) denies with next-step text instead of executing on
  stale authority. Regression proves consult leaves use_count at 0 and a
  reversed minting approval denies at the boundary.
- Cron approval-client delivery is scoped to host=gateway. Node-host cron
  cannot mint or consume grants yet, so it keeps the fully suppressed
  headless policy from #128031 instead of raising cards whose allow-always
  could not stick; node-host grant support stays a named follow-up.

* test(agents): complete plugin-metadata-snapshot mock factories

Five explicit vi.mock factories for current-plugin-metadata-snapshot.js
exported only getCurrentPluginMetadataSnapshot. Under isolate:false shard
composition the incomplete mock can bleed into siblings that import the
real module — model-resolution-consistency.test.ts failed on CI with
'No withPluginMetadataSnapshotScope export is defined on the mock'.
Spread importOriginal so every binding prod touches stays exported, per
the repo mock-factory rule; only the snapshot getter stays overridden.

* chore: drop accidentally committed pinned swiftlint binary; ignore .build/

scripts/install-swift-tools.sh installs pinned Swift tools into
.build/swift-tools per the lint-swift.sh remediation hint; the 36MB
binary must never ride a commit. Remove it and ignore the directory.

* test(agents): shield model-resolution-consistency from leaked snapshot mocks

The agents-embedded shard still failed after completing five factories:
~20 more test files across the repo mock current-plugin-metadata-snapshot
with incomplete explicit factories, and under isolate:false composition
any of them can strip withPluginMetadataSnapshotScope from this file's
imports. Give the victim a file-local identity mock (importOriginal
spread) that always wins, and revert the static-catalog factory edit that
tipped that grandfathered file over the max-lines cap — the repo-wide
factory completion belongs to a dedicated sweep.
2026-08-25 16:17:58 -07:00
Sasan e9620fba9d fix(gateway): usage.status no longer waits on provider HTTP (#121799)
* fix(gateway): refresh provider usage asynchronously

* fix(ui): report a stalled provider-usage refresh on Model Providers

The page observed the incomplete-usage marker but discarded the exhausted
outcome, so once the retry budget was spent it rendered ordinary provider
cards with no usage and no explanation — indistinguishable from providers
that report no usage at all. Keep the outcome and render the warning the
Usage page already owns, reusing usage.providerUsage.stalled rather than
minting a Model Providers key so no locale baseline churns.

A user-initiated refresh now restarts the retry budget. The notice tells the
operator to refresh, so the button has to hand back attempts to spend; only
the forced path resets it, or the budget could never exhaust.

Also fixes tsgo:core:test on the current head: createStore's inferred literal
had no usageStats, so the run-bookkeeping case could not stamp it, and
view.test.ts needed the new prop.

Closes the ClawSweeper P2 at model-providers-page.ts:169-175.

* fix(ui): keep the stalled usage notice when usage.status starts rejecting

loadModelProvidersData turned a rejected usage.status into providerUsage:
null, which the page read as a completed load. observe(false) then reset the
retry budget and cleared the stalled callout, so a permanently broken usage
endpoint rendered as ordinary cards with no usage and no explanation — the
same silent failure the callout was added to prevent. The reset also fired
mid-cycle: one incomplete response followed by one rejection restarted the
budget, so the notice could be deferred indefinitely.

Record the failure at its producer instead of inferring it downstream. A null
providerUsage also means "not loaded yet", and no caller can tell the two
apart, so load.ts now reports providerUsageFailed explicitly and the page
treats a failed read as unresolved rather than resolved-empty.

Found by a Codex review of 417d43b65d.

* revert(gateway): drop the opportunistic model-catalog fast path

It broke two chat.history tests on main — both assert the cold catalog loader
runs exactly once, and reading the prepared snapshot first means it never does.
checks-node-compact-small-10 was red for that reason.

The change was a separate-surface latency fix that this PR picked up in passing,
and the body already offered to split it. Dropping it is the honest resolution:
rewriting main's assertions to accommodate a drive-by optimization would trade
one concern's proof for another's convenience. optional-model-catalog.ts,
server-model-catalog-auth.ts and their test return to the merge-base.

This PR is now only the usage.status non-blocking contract and its clients.

* fix(usage): preserve incomplete retry state

* perf(ui): keep usage capability startup-neutral

* fix(ui): restore provider usage retry convergence

* fix(usage): restore retry and cache invariants

* fix(usage): stabilize provider convergence

* test(ui): exercise provider recovery path

* test(ui): remove stale usage route fixture field

* fix(macos): show provider usage errors

* fix(macos): bound usage retries per menu open

* fix(macos): end usage retries on menu close

---------

Co-authored-by: Josh Lehman <550978+jalehman@users.noreply.github.com>
2026-08-25 13:20:10 -07:00
Peter Steinberger 4dc7bb7411 chore(deps): refresh dependencies after seven-day cooldown (#129187)
* chore(deps): refresh dependencies after cooldown

* fix(gateway): emit append-only Responses content events

* chore(deps): retain unverified Sherpa runtime
2026-08-25 05:00:46 -07:00
Peter Steinberger 8a1ff3966b feat(workers): run OpenClaw and Codex on the same cloud profile (#127752)
* feat(workers): run both harnesses on one cloud node profile

* docs(workers): explain shared OpenClaw and Codex cloud profiles

* fix(workers): preserve trusted Codex cloud enrollment

* docs(codex): clarify bundled cloud plugin eligibility

* fix(workers): honor Crabbox bootstrap retries

* fix(workers): allow cloud profile setup to finish

* fix(workers): reject unsupported cloud execution modes

* refactor(workers): split cloud placement modules

* fix(workers): preserve completed container results
2026-08-25 00:13:29 -07:00
Josh Avant 2e9c8f8d3f Audit exact-bound owner-native lifecycle receipts (#126082)
* feat(audit): add owner-native lifecycle receipts

* fix(audit): store lifecycle bindings additively

* test(cron): isolate execution binding fixtures

* fix(audit): reject stale lifecycle owners

* fix(acp): gate parent flow binding on retained task

* fix(audit): reject pruned owner cursors
2026-08-24 17:49:25 -07:00
Peter Steinberger 058a72fe66 fix(update): bind managed handoffs to exact targets (#128868)
* fix(update): bind managed handoffs to exact targets

* fix(update): preserve campaigns on target mismatch

* fix(update): fence active campaign updates
2026-08-24 17:01:11 -07:00
ClawSweeper a5b5920444 feat(ui): configure capabilities before session start [AI-assisted] (#128081)
* feat(ui): configure capabilities before session start

Reuse the active-chat Plus menu on new sessions, move Draft into it, and persist admin-scoped tool overrides before the initial turn. Closes #128079.

* test(ui): follow new-session Draft menu

* refactor(ui): reconcile new-session capability ownership

* fix(ui): gate terminal launch on capability overrides

Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>

---------

Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-08-24 11:47:12 -07:00
Peter Steinberger 053b89d80f improve(ui): open short session links without extra lookup (#128778)
* perf(ui): remove short session route waterfall

* docs: clarify short link gateway requirement
2026-08-24 09:33:20 -07:00
Peter Steinberger b145e25fea fix(ui): resume session starts after Gateway reconnects (#128661)
* fix(ui): resume session creation after reconnects

* improve(ui): remove redundant appearance reset buttons

* fix(ui): align session protocol and appearance validation

* fix(ui): retain promoted placement session ownership

* fix(gateway): isolate session creation capacity by owner

* test(ui): stabilize hovercard bridge pointer movement
2026-08-24 06:36:51 -07:00
Peter Steinberger 234df15a6d chore: refresh dependencies after seven-day cooldown (#128414)
* build(deps): refresh dependencies after cooldown

Apply dependency, toolchain, action, image, and exact tool updates released by the inclusive 2026-08-16 seven-day cutoff. Adapt owner boundaries for the resulting CUA, logging, Teams, Markdown, native, and test-harness contract changes while retaining versions blocked by upstream compatibility constraints.

* fix(ui): align markdown renderer env typing

* fix(deps): align postcss and mistral peer contracts

* fix(deps): repair refreshed dependency contracts

* fix(deps): retain tslog startup budget

* fix(ci): verify Android tools with SHA-256

* fix(ci): fence Android SDK cache version
2026-08-24 03:01:54 -07:00
Peter Steinberger 53dcaaedec feat(gateway): generic operator roles for non-maintainer access (#128548)
* feat(gateway): generic operator roles for non-maintainer access

Adds gateway.roles: named role bundles over a closed capability set —
sessions.others (none/view/suggest/write), an agents allowlist, and an
operator-scope ceiling. Roles are person-level (additive user_profiles.role
column, SQLite stays at v9); users.setRole (admin-only) assigns them. With
no gateway.roles config, behavior is unchanged for solo deployments.

Enforcement is deny-by-default from a host-minted actor identity
(system vs operator+profileId on server-only client.internal, never
accepted from the wire) and covers every entry point: WS RPCs, OpenAI-compat
and Responses HTTP, tools invocation, cron, questions, usage, task
suggestions, session catalog/sharing/reads. The agents allowlist gates both
session creation and run-start on existing sessions. Subagent completion
announce and descendant wake mint explicit system authority so role
boundaries never silently drop parent notifications.

The enforcement surface is expressed through a narrow policy vocabulary
(operatorSessionCap, hasOperatorBoundary, authorizeSessionSharing) rather
than per-handler policy internals.

* fix(gateway): heal PR CI after rebase onto main

- Break import cycles: extract GatewayOperatorRoleActor leaf contract; merge
  session-group-mutation-targets into session-sharing-target-input.
- Split sessions-suggestions.test.ts (max-lines) into a visibility suite.
- Add users.setRole to the 2026.8 train registry test and regenerate the
  Kotlin protocol client.
- Startup UNAVAILABLE gating now precedes session authorization: session
  stores are not loaded during startup, so authorization reads would deny
  with a misleading non-retryable error.
- sessions.assignOwner keeps its documented visibility-authorized contract
  when no operator role caps the caller; view/suggest-capped roles still
  cannot reassign foreign session ownership.
- Test stubs updated for main's socket readyState guard (#128144) and the
  system-authority arg on channel-native resets.

* test(gateway): chat.send pending-profile dispatch carries its required session target

chat.send requires a non-empty sessionKey at the protocol level; the mutation
pipeline now rejects targetless frames before profile-dependent dispatch, so
the pending-profile test must send a realistic frame.
2026-08-24 01:20:03 -07:00
Peter Steinberger 6178409d9a fix(dashboard): preserve widget content ownership (#128489) 2026-08-23 21:53:39 -07:00
Peter Steinberger 0978d55b05 feat(nodes): automatic device placement for sessions.dispatch (#128421)
* feat(nodes): automatic device placement for sessions.dispatch

sessions.dispatch gains autoDevice: true — the gateway selects the eligible
session-host node with the most available worker slots (deterministic
tie-break), retries up to three candidates when a node churns at the
pre-provisioning eligibility fence, and reports the chosen device in the
placement runner projection. Control UI offers Any available node with
actionable disabled reasons. No-eligible-host failures state why.

* fix(ui): break draft-place-state/draft-session-placement import cycle

resolveDraftSessionPlacement only needs four scalar fields; a structural
param type replaces the Pick<DraftPlaceState,...> import that created the
madge cycle.

* fix(ui): keep the devices section hidden when no devices are paired

The Any available node row lives inside the Your devices section; rendering
it with zero paired devices resurrected the section on gateway-only setups.
Gate it on device presence — Connect a machine remains the discoverability
path — and cover both the empty and non-hostable cases.

* fix(gateway): project dispatch runner state through the canonical reader

The dispatch reply no longer synthesizes an available device runner; it uses
the fenced workerPlacementRunnerAvailabilityReader (and disk-space reader)
exactly like session reads, so a node lost after durable provisioning
projects offline consistently. Documents placement.runner.deviceId in the
protocol reference.
2026-08-23 21:08:33 -07:00
Peter Steinberger 9284e23cdd feat(control-ui): show client IP and time zone on the activity identity card (#128438)
* feat(control-ui): show client IP and time zone on the activity identity card

The Activity identity card showed only host and platform, so an operator
looking at a teammate could not tell where that person was connecting from.
Presence already carried a best-effort `ip`, and it was simply not rendered.

Add the client's self-reported IANA time zone to the connect handshake and
presence entry, and render both `ip` and `timeZone` on the device row. The
time zone matters because the connecting address is frequently unusable for
location: connect handling omits `ip` for loopback clients, and tunneled or
Tailscale clients land in private/CGNAT ranges. A browser knows its own zone
regardless of how it reached the gateway.

Both protocol additions are optional fields, so no version bump is needed.

* build(protocol): regenerate Swift models for presence timeZone
2026-08-23 20:07:06 -07:00
Vyctor H. Brzezowski 6669872a95 feat(ui): let admins dismiss the sidebar update indicator (#128365)
* feat(ui): let admins dismiss the sidebar update indicator

Refs #128232

* refactor(ui): clean up update dismissal flow

* fix(ui): align update action state

* fix(ui): keep update reconciliation non-actionable

* fix(ui): preserve update reconciliation visibility

* fix: validate update dismissal facts

* fix(ui): resurface active updates

* fix(ui): show applying update campaigns

* fix(gateway): preserve boot identity

* perf(ui): split sidebar update styles

* fix(test): provide captured plugin runtime
2026-08-23 19:21:10 -03:00
ClawSweeper a77fbdef33 fix(ui): recover queued follow-ups from settled runs (#126180)
* fix(ui): recover queued follow-ups from settled runs

Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>

* fix(ui): preserve terminal client run ownership

Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>

* fix(sessions): persist recovered terminal client runs

Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>

* test(gateway): publish configured reply runtime

Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>

* fix(protocol): refresh Swift session row

Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>

---------

Co-authored-by: Ian Moog <ianmoog42@gmail.com>
Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>
Co-authored-by: Roboclaw <roboclaw-bot@users.noreply.github.com>
2026-08-23 06:55:21 -07:00
Ayaan Zaidi 500c9b4ea6 fix(outbound): prevent remote gateway duplicate sends (#128202)
When the agent runtime talks to a remote Gateway (gateway.mode "remote" or a
gatewayUrl/gatewayToken override), the message tool withholds the runtime
identity, so a proven-not-sent outbound failure kept its durable retry row
and also surfaced as an error the model would answer by resending — a
duplicate once the queue replayed the row.

- deliver-queue-execute: mark the thrown error recoveryOwnedRetry when the
  proven-not-sent row stays replay-eligible (one retry owner per row)
- message.action: additive UNAVAILABLE detail code OUTBOUND_DELIVERY_QUEUED
- message tool: project that error into a non-throwing delivery_queued
  result ("queued, will retry automatically, do not resend") and keep the
  autogenerated idempotency-key mapping so an identical resend collapses

Validated with unit tests and a real-Telegram remote-gateway E2E
(affected on main: duplicate; fixed: single send, delivery_queued).

Closes #124279

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-08-23 17:44:48 +05:30
Vincent Koc 9046ecea73 fix(talk): isolate cancellation ownership contract (#127186)
* fix(talk): isolate cancellation ownership contract

Co-authored-by: Zhilong Zheng <zhengzhilong1115@gmail.com>

* fix(talk): frame realtime relay output audio

Co-authored-by: Zhilong Zheng <zhengzhilong1115@gmail.com>

* fix(talk): confirm turn-bound provider cancellation

Co-authored-by: Zhilong Zheng <zhengzhilong1115@gmail.com>

* fix(talk): bind legacy iOS output cancellation

Co-authored-by: Zhilong Zheng <zhengzhilong1115@gmail.com>

* fix(talk): close cancellation ownership gaps

Drain dynamically arriving forced-consult results before terminal completion and validate legacy iOS cancellation responses against the active lifecycle.

Co-authored-by: Zhilong Zheng <zhengzhilong1115@gmail.com>

* fix(talk): require Android output identity for cancellation

Co-authored-by: Zhilong Zheng <zhengzhilong1115@gmail.com>

* fix(talk): bind Android cancellation to action turn

Co-authored-by: Zhilong Zheng <zhengzhilong1115@gmail.com>

* test(talk): align relay checks with current main

Co-authored-by: Zhilong Zheng <zhengzhilong1115@gmail.com>

* fix(talk): fence stale iOS output clear

Co-authored-by: Zhilong Zheng <zhengzhilong1115@gmail.com>

---------

Co-authored-by: Zhilong Zheng <zhengzhilong1115@gmail.com>
2026-08-23 03:58:00 -07:00
Peter Steinberger 0b20779024 perf(ui): stop running full setup detection on every Control UI connect (#128135)
The first-run gate was paying 5.5s warm / 16.8s cold and retaining roughly 216MB to compute setupComplete. Record the default agent's model-configured fact in the hello snapshot and make the UI redirect decision synchronously from that fact.
2026-08-23 00:34:34 -07:00
ClawSweeper 7d95cff39d fix: macOS onboarding waits for Gateway restart (#127713)
* fix(onboarding): wait for inference gateway restart

Co-authored-by: hannesrudolph <49103247+hannesrudolph@users.noreply.github.com>

* fix(onboarding): preserve custodian handoff after restart

Co-authored-by: hannesrudolph <49103247+hannesrudolph@users.noreply.github.com>

* refactor(macos): share activation restart finalization

Co-authored-by: hannesrudolph <49103247+hannesrudolph@users.noreply.github.com>

* style(macos): format restart reconciliation

Co-authored-by: hannesrudolph <49103247+hannesrudolph@users.noreply.github.com>

* fix(macos): compile restart finalization

Co-authored-by: hannesrudolph <49103247+hannesrudolph@users.noreply.github.com>

* test(macos): sequence onboarding restart proof

Co-authored-by: hannesrudolph <49103247+hannesrudolph@users.noreply.github.com>

* test(macos): finish onboarding after activation

Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>

Co-authored-by: hannesrudolph <49103247+hannesrudolph@users.noreply.github.com>

* test(macos): reuse managed restart proof

Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>

Co-authored-by: Hannes Rudolph <49103247+hannesrudolph@users.noreply.github.com>

* test(macos): assert receipt before handoff cleanup

Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>

Co-authored-by: Hannes Rudolph <49103247+hannesrudolph@users.noreply.github.com>

* fix(onboarding): keep restart verification bounded

Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>

Co-authored-by: Hannes Rudolph <49103247+hannesrudolph@users.noreply.github.com>

---------

Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com>
Co-authored-by: hannesrudolph <49103247+hannesrudolph@users.noreply.github.com>
2026-08-22 15:51:04 -07:00
Peter Steinberger d80a66cb11 fix(protocol): export chat history cursor types (#128018)
Co-authored-by: Amp <amp@ampcode.com>
2026-08-22 12:43:05 -07:00
Ayaan Zaidi 041938bc2f feat(ui): add a Claude CLI 200K/1M context-window switch to the model picker (#127951)
Adds a generic plugin-declared selectable-context-window surface mirroring thinkingLevels: ModelCatalogEntry.contextWindows + contextWindowDefault through catalog normalization and the gateway protocol, session validation on sessions.create/patch, and a 200K/1M switch inside the Control UI model picker for Claude CLI 5-series models. The Anthropic plugin owns the option mapping: explicit 1m → `[1m]` argv suffix, 200k → bare id + CLAUDE_CODE_DISABLE_1M_CONTEXT=1, omitted → bare id (shipped default argv). Run budgets follow the selection on both CLI and native paths, so a 200K session gets a matching auto-compact window instead of a silent 1M budget.

Review fixes landed in this PR: run-owner prepared-fact plumbing so ordinary replies honor the selection; atomic catalog overlay merge and normalization for the options/default tuple; one-owner tuple reads in the picker; sessions.create key-presence patch semantics; native-run budget capping.

Feature direction and in-picker switch shape by @obviyus (maintainer review).
2026-08-22 23:25:23 +05:30
Yuval Dinodia ff6db34233 fix(skills): preserve local changes during updates (#118190)
Prevent ClawHub skill updates from silently replacing locally modified files. Verify recorded install digests at the rollback boundary, restore on mismatch, and require explicit --force for unverifiable legacy installs.

Co-authored-by: yetval <yetvald@gmail.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-08-22 12:32:04 +05:30
Peter Steinberger 1353ce0995 feat: run Codex sessions on approved paired devices (#127202)
* feat(codex): execute paired-device sessions over node carrier

* fix(node-host): preserve approved invocation session identity

* fix(codex): observe paired-node execution leases before handshake

* fix: fence paired-device placement and Codex execution owners

* fix: satisfy paired-device placement CI ownership guards

* fix(codex): reject credentialed paired-node URL parameters

* fix(codex): fence nested remote HTTP session credentials

* fix(codex): scrub node process URLs and preserve plaintext HTTP

* test(codex): republish node inventory after capability approval
2026-08-21 12:50:26 -07:00
Josh Avant ccbfa6c3a3 feat(ui): explain decision receipts in Activity (#126007)
* fix(audit): project safe decision receipt displays

* docs(agents): preserve audit display privacy

* fix(ci): satisfy audit receipt guardrails
2026-08-21 11:33:32 -07:00
Peter Steinberger e7cfff2167 feat(control-ui): stream live draft previews in the typing indicator (#126994)
* feat(control-ui): stream live draft previews in the typing indicator

Multi-identity sessions now show what a teammate is typing, not just that
they are typing: the composer's per-keystroke session.typing sends carry a
bounded tail of the draft (optional preview field, 400 code points max),
the gateway throttle re-emits on changed payloads at 250ms (boolean-only
stays at 1s, trailing edge keeps the latest draft), and the transcript
renders a per-actor bubble with the live text plus a blinking caret.
Actors without preview data keep the three-dot bubble.

Previews are ephemeral presence: never persisted, never part of the
session transcript or model context, excluded from aria-live regions, and
gated by the existing >=2-live-viewers, sharing-role, and incognito
checks. No new config surface.

* chore(protocol): regenerate Swift gateway models for typing preview

* fix(gateway): aggregate typing previews across same-actor connections

A boolean-only session.typing update from a second connection of the same
actor (another tab or device) erased their live draft preview, because
typing liveness aggregated per actor while the broadcast preview came only
from the latest request. Preview aggregation now lives with the connection
aggregation owner: updateTypingConnections tracks per-connection previews
and returns the newest non-empty preview among live connections, so the
broadcast keeps the active draft until its connection stops or expires.

Regression fails pre-fix (event lost its preview field).
2026-08-20 22:30:41 -07:00
Peter Steinberger e22774127a test: remove core test scaffolding (#126926) 2026-08-20 18:34:15 -07:00
Peter Steinberger bbbd70542b feat(sessions): recover offline device placements (#126284)
* feat(sessions): recover offline device placements

* chore(protocol): refresh session placement models

* perf(ui): lazy-load session placement recovery

* test(ui): remove dropdown timing assertion

* test(ui): await committed cloud recovery route

* fix(sessions): complete explicit abandonment locally

* fix(sessions): fence lists by runner availability

* fix(ui): preserve canonical session freshness

* fix(sessions): preserve recovery contracts after rebase

* test(ui): await durable cloud recovery entry

* test(gateway): complete current runner fixtures

* fix(gateway): publish runner availability edges

* fix(ui): preserve shared session freshness

* fix(ui): preserve canonical sidebar session state

* fix(sessions): preserve abandoned partials and run-owned replies

* fix(sessions): resume durable abandonment retries

* test(gateway): compose provisioning replay with runner availability

* fix(sessions): publish recovered move transitions
2026-08-20 09:59:59 -07:00
ClawSweeper 267ffc4754 feat(ui): manage automation condition triggers (#126534)
* feat(ui): manage cron condition triggers

* test(models): isolate ambient CLI availability

Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>

---------

Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-08-20 07:20:01 -07:00
Peter Steinberger 1d2986b06f fix: prevent incompatible cloud runtime dispatches (#126585)
* fix(ui): gate cloud profiles by runtime mode

* test(gateway): expect cloud placement mode

* test(gateway): keep route suite within lint budget
2026-08-20 01:31:27 -07:00