524 Commits

Author SHA1 Message Date
Peter Steinberger d6e1f019f0 fix(gateway/ui): record model auth unavailability reasons; gate composers only on actionable auth failures (#131697)
* fix(gateway): preserve model auth unavailability reasons

Record missing credentials, unusable auth, and cooldown separately from unknown availability. Carry native runtime auth facts and retry times through the additive models.list projection without changing protocol versions.

* fix(ui): gate composers only on actionable auth failures

Read producer availability reasons in chat and new-session gates. Preserve Model Setup for missing credentials, keep accurate rejection copy, and allow sending during cooldown or unknown availability. Keep unavailable picker options disabled.

* fix(gateway): distinguish permanent auth rejection from cooldown

Keep permanently rejected profiles and inline credentials actionable without a retry timestamp. Use only viable transient profiles for the earliest recovery time, preserving explicit profile retry behavior.

* test: isolate submit gates and native runtime availability fixtures

Extract the existing submit-gate suite and shared draft fixture to stay within the file-size limit. Supply registered CLI metadata through the existing test boundary so availability checks do not cold-load executable plugin setup code.

* chore(protocol): regenerate Swift gateway models for availability reasons

* test(gateway): expect availability reasons in models.list projections
2026-08-28 03:18:43 -07:00
Peter Steinberger de12e0a06c feat(approvals): standing-grant ledger, revocation, and configurable terms (#131602)
* feat(approvals): standing-grant ledger, revocation, and configurable terms

Completes the operator half of cron standing grants (#129526):

- Grants are listable and revocable. New gateway methods
  exec.approval.grants.list/revoke (operator.approvals scope), CLI
  'openclaw approvals grants list|revoke', and a Standing grants section
  on the Control UI approvals page with per-row revoke. Revocation is
  idempotent, records the acting client, and takes effect at the next
  occurrence's spawn boundary.
- Grant terms are frozen at mint and configurable. Default stays
  until-revoked (matching what shipped Always allow has always meant);
  tools.exec.grantExpiryDays sets a day-bounded default for future
  grants (enterprise fleet policy), and resolve surfaces may override
  per grant via grantExpiresInDays / --expires-in-days. Existing grants
  keep their minted terms; config changes are never retroactive.
- expires_at_ms goes nullable (NULL = until revoked). The mandatory-
  expiry shape never reached a release tag, so the lazy ensure rebuilds
  it in place; grants are re-derivable authority, so the fail-closed
  cost is one re-prompt.
- Approval cards say what Always allow does. Automation approvals carry
  a standing-grant ApprovalScope (new union member) built server-side
  from the cron run registry (now carrying the job display name); the
  Control UI inbox card and modal render it via i18n, e.g. 'Always
  allow runs this exact command for "grant-demo" without asking, for
  10 days (revocable)'.
- Aux method registries stay in lockstep: new parity test fails when a
  GATEWAY_AUX_METHODS entry lacks a lazy handler (this gap produced
  live unknown-method errors during bring-up).

Live-proven on a hermetic gateway: until-revoked grant ran 62 promptless
occurrences; UI and CLI revocation each returned the prompt on the next
occurrence; config default stamped exactly 10.0 days via a UI resolve;
--expires-in-days 99 override stamped 99.0 days; ledger and card
screenshots captured from the running Control UI.

* chore(ui): annotate the scope narrowing assertion for the safety ratchet

* test(gateway): move aux-method parity check to its own suite

* fix(ci): boundary-parse UI approval scope; register grants schemas for native codegen

- The Swift/Kotlin generators discover named schemas through the
  protocol schema fragments; the standing-grant scope member and the
  grants list/revoke shapes were missing there, which made the Swift
  generator silently drop the ApprovalScope union and broke ios-build.
  Registered, regenerated, union and new struct emitted again.
- The chained-assertion lane rejects 'as unknown as' in the UI scope
  parser; replaced with a real per-kind boundary parser, which is what
  the rule asks for.
- The sqlite-allow-raw marker must share the line of a mid-statement
  prepare() call for the Kysely guardrail; split the pragma read so the
  SAFETY comment and the allow marker each sit on their own guarded
  line.

* chore(ui): record startup-budget growth for the approvals scope line

The standing-grant scope renderer and grants-ledger strings ride the
boot path (~700 B gzip). Recorded via the check's --update-baseline flow
with the reason inline; well under the committed 350 KiB hard cap.

* fix(gateway): restore the channel-restart helper export dropped on main

#118157 made startGatewayChannelFromActiveRegistry module-local while
its colocated test still imports it, breaking check-test-types and the
compact shard on every merge ref. Smallest correct main heal per the
broken-CI doctrine; the test passes again.

* fix(gateway): drive channel-restart reload coverage through its exported owner

Replaces the prior export restore: #118157 made the restart helper
module-local (the dead-export scan is right that tests are not
production consumers), but left its colocated test importing it, so
main failed check-test-types either way. The test now exercises the
same preserveManualStop semantics through rollbackStoppedGatewayChannels,
the exported reload owner, and the helper stays private.

* test(ui): answer the grants ledger request in approvals-page history tests

The approvals page now loads the standing-grant ledger next to history;
a method-aware stub answers it out of band so the ordered history mocks
keep their call counts. Adds coverage for the ledger render and the
revoke round-trip.

* chore(ui): record CI-observed startup-JS baseline for the grants surface

The e2e-real-gateway lane builds the same tree ~600 B larger than checks-ui
and local builds, past the 64 B variance allowance; record the CI-observed
value so the enforcement limit stops straddling cross-lane build variance.

* fix(cli): escape standing-grant ledger fields visibly; register grants methods in inventory tests

The grant ledger's revokedBy (client display name), automation label, and
agent-authored command now go through escapeApprovalTextForTerminal like the
pending-approvals table, so hostile control characters render as visible
\u{...} escapes instead of being silently stripped by the table renderer.
Regression test fails pre-fix. Also lists exec.approval.grants.list/revoke
in the 2026.8 release-train roster, classifies the approvals grants command
group for JSON output, and fixes the stubGrants mock typing.
2026-08-28 02:56:20 -07:00
Peter Steinberger 54a5358356 fix(ui): open worktree sessions before setup finishes (#131559)
* fix(ui): open worktree sessions before setup finishes

Admit the first turn before naming, checkout, and repository setup; show
startup progress and keep failures retryable in the same session. Reuse the
accepted-run workspace owner, fence cancellation and setup authority, and
block sibling work until the workspace is ready.

Fixes #124966 (immediate worktree sessions and setup progress).

* test(ui): retain project picker capability in startup fixture

* fix(worktrees): fence checkout retries after cancellation

* test: align native phases and title fallback with admitted startup
2026-08-27 23:13:38 -07:00
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 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
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 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
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
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 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
Peter Steinberger 1be9b56d57 refactor(protocol): rely on synthesized Swift coding keys (#128334) 2026-08-23 13:35:51 -07: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
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
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
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 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 c6ecfd26e5 fix(outbound): preserve first-reply behavior through durable delivery (#126205)
* fix(outbound): preserve reply facts through durable delivery

* fix(protocol): regenerate message action models

* fix(outbound): complete reply fact custody
2026-08-19 17:52:12 -07:00
Peter Steinberger 0a86702241 feat(github): authorize agent identities from Settings (#126474)
* feat(github): add device authorization lifecycle

* fix(github): harden device authorization lifecycle

* fix(github): refresh native tool display snapshot

* fix(github): refresh config and UI baselines

* refactor(github): break OAuth identity import cycle

* test: make gateway retry deadline assertion scheduler-safe
2026-08-19 17:41:58 -07:00
Peter Steinberger 0606e31d0e feat(gateway): broker GitHub publication (#126306)
* feat(gateway): broker GitHub publication

* refactor(gateway): split publication owners

* fix(gateway): enforce publication branch authority

* fix(gateway): bind publication to remote identity

* fix(gateway): bind publication recovery to remote state

* fix(gateway): preserve publication git state

* fix(gateway): retain publication recovery authority

* fix(gateway): commit publication index atomically

* fix(gateway): narrow publication index errors

* refactor(gateway): keep publication CAS errors private

* fix(gateway): recover publication index transactions

* fix(agents): describe GitHub publication tool

* fix(gateway): harden publication base fetch

* fix(gateway): reject publication filter semantics

* fix(gateway): verify publication creation base

* refactor(agents): align publication tool options

* fix(gateway): isolate publication object lineage

* refactor(gateway): use shared table probe

* test(gateway): keep publication helpers in routed suite

* perf(ui): lazy-load GitHub publication request

* fix(gateway): preserve publication support contracts

* fix(gateway): recover publication before authority checks

* fix(gateway): fence local publication snapshots

* fix(android): format generated protocol models

* fix(gateway): harden publication recovery

* fix(gateway): fence publication recovery

* fix(ui): reset completed publication cycles
2026-08-19 11:05:12 -07:00
Peter Steinberger 51599041bc fix: explain preserved session worktrees accurately (#126347)
* fix(sessions): report worktree preservation reasons

* fix(sessions): align preservation checks with current main

* perf(ui): keep preservation copy within startup budget

* perf(ui): reuse localized preservation copy

* test(ui): match preserved worktree confirmation copy

* test(ui): await committed raw config state
2026-08-19 10:59:42 -07:00
Peter Steinberger 3e0c980aaf fix(models): honor per-agent model metadata (#126194)
* fix(models): honor per-agent model metadata

Resolve per-agent aliases, bare providers, fallbacks, and catalog tags consistently across runtime, CLI, Gateway, sessions, and the Control UI.

* fix(models): preserve projection ownership

* fix(models): carry agent scope through fallbacks

* test(models): complete compaction fallback mock

* test(models): complete startup fallback mock

* test(agents): isolate recovery id expectations

* refactor(protocol): split public schema barrel

* test(cron): await child readiness events

* fix(models): scope native catalogs to session agent

* perf(ui): tighten agent model option projection

* perf(ui): reduce agent model projection overhead
2026-08-19 06:04:24 -07:00
Peter Steinberger 94eb34fa78 fix(skills): require re-review when proposals change (#126156)
* fix(skills): bind workshop decisions to reviewed revisions

* chore(i18n): refresh native source inventory

* test(skills): align revision proof with inspect projection

* test: align skill workshop regression fixtures

* fix(ui): align workshop revision admission proof

* fix(ui): keep revision errors out of startup
2026-08-19 01:52:11 -07:00
Peter Steinberger 2bcc06cc22 fix(cron): required delivery failures no longer report success (#126164)
* fix(cron): preserve required delivery completion

Record durable completion independently from payload execution so required delivery failure cannot delete one-shots or report successful waits.\n\nCloses #126163

* fix(cron): keep completion contracts acyclic

* fix(cron): keep delivery predicate private
2026-08-18 22:44:33 -07:00
Vyctor H. Brzezowski 2a469c1cb2 fix(ui): dismiss completed progress cards (#126102)
* test(ui): cover completed progress card dismissal

* test(ui): capture both completed card themes

* fix(ui): dismiss completed progress cards

* style: format progress card dismissal

* test(ui): assert visible dismissal lifecycle

* test(ui): wait for reloaded composer proof

* refactor(ui): reuse progress card action styles

* chore(protocol): refresh Swift progress card params

* style(ui): format progress card test

* fix(ui): hide progress dismissal from viewers

* test(ui): use Vitest viewer assertion
2026-08-18 23:39:31 -03:00
Peter Steinberger ef22410985 refactor(protocol): remove beta-only expectedRunId from chat.send (#125921)
The Gateway owns start-or-steer at admission (6515f6a255) and no
client produces expectedRunId anymore (d84a910fc8). The field shipped
only in v2026.8.1-beta.2 - never a stable tag - so it is removed rather
than deprecated. Steer sends resolve the selected session's current
operation; the exact-match branch, the operation|run target identity
discriminator, run_mismatch rejection, and the suggestion producers'
active-run-id selection (with its ambiguity failure) are deleted.
Provider-native turn fencing (Codex expectedTurnId) is unchanged:
the backend-captured runId on the injection target remains.
2026-08-18 11:28:36 -07:00
Peter Steinberger 2bed8caf9e feat(ui): channel conversation avatars in the sidebar (Discord + Slack) (#125668)
* feat(gateway): proxy channel conversation avatars

* feat(discord): capture conversation avatars

* feat(slack): capture DM sender avatars

* test(discord): bind guild avatar mock

* feat(ui): render channel conversation avatars

* fix(ui): align sidebar owner fixtures

* fix(gateway): version channel-avatar routes by media revision

A stable per-session URL let AuthenticatedAvatarRouteLoader's blob and
sticky-404 caches pin a mounted row to a stale or blank avatar after the
backing media changed. Append an opaque digest of the media reference so
replacement and 404-recovery change the route identity.

* test(ui): align sidebar owner facet

* fix(ui): keep owner chip until channel avatar loads

A session with a channelAvatarUrl suppressed its owner chip even while the
blob was loading, auth was not ready, or the route 404ed, leaving an empty
lead slot. The chip now rides as fallback content inside the avatar element
and yields only to a usable image. Covers 404 and auth-pending states;
avatar rows keep renderedOwnerId unset so an owner-viewer stays visible in
the facepile.

* perf(ui): keep channel avatar fallback within budget

* perf(ui): lazy-load the channel avatar element

The avatar element and its authenticated blob loader rode the startup
bundle through session-leading-indicator, pushing startup JS 51 B over the
CI gzip budget. Channel avatars are not startup-critical: register the
element on the first avatar row; the owner-chip fallback covers the
one-time upgrade window. Startup JS returns ~1 KiB under the ceiling.

* build(ui): raise startup baseline for channel avatars

CI-measured startup JS is 344379 B against a 343289 B baseline (+1090 B).
The avatar element and blob loader are code-split out of startup (previous
commit); the residual is the sidebar lead-slot render branch and row
plumbing, which cannot be deferred. Baseline updated via
check-control-ui-performance --update-baseline with CI bytes per the
script's contract; well inside the 4096 B ratchet step and 358400 B
ceiling.
2026-08-18 09:55:50 -07:00
Peter Steinberger 5f65ef8138 feat: add A2UI widgets to session boards (#125803)
* feat: add A2UI board widgets

* fix: harden A2UI board widget integration

* fix: repair A2UI CI contracts

* fix(canvas): keep native A2UI pushes on v0.8

* fix(canvas): support board-only widgets without file host
2026-08-18 09:08:45 -07:00
Peter Steinberger bdbb0444ee fix: share Control UI terminals with chat agents (#125758)
* fix: share Control UI terminals with chat agents

* fix: terminate canceled shared terminal opens

* perf(ui): keep terminal binding behind lazy boundary

* perf(ui): reuse terminal session scope for bottom dock

* perf(ui): reuse session route decision

* fix: keep shared terminal cleanup server-owned

* fix: preserve shared terminal session identity
2026-08-18 07:51:48 -07:00
Peter Steinberger efaa867d93 feat(nodes): publish exact runner slots (#125708)
* feat(nodes): publish exact runner slots

* docs(runners): link slot inventory PR

* fix(ui): keep session menus clickable under previews

* perf(protocol): keep runner slot schema off broad barrel

* test(codex): tolerate elapsed media startup budget

* test(codex): type media client factory mocks

* fix(protocol): reject impossible runner slot counts

* test(ci): stabilize loaded lifecycle checks

* test(ui): allow loaded bulk-delete scheduling

* test(ui): stabilize loaded interaction checks

* test(ui): isolate loaded update notice checks

* test(ui): colocate bulk-delete lifecycle coverage

* test(gateway): publish session config before async setup
2026-08-18 07:08:06 -07:00