Commit Graph

80123 Commits

Author SHA1 Message Date
Peter Steinberger 2b2c11e748 fix(install): keep stable channel when npm install retries (#124778)
* fix(installer): keep npm channel target immutable

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* fix(install): fail after repeated CLI package install errors

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* fix(installer): verify npm package publication

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* fix(install): satisfy shellcheck for entry validation

* fix(installer): link the packaged OpenClaw launcher

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* test(installer): keep retry fixture version-valid

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-08-17 00:17:26 -07:00
Peter Steinberger 1027837d96 fix(msteams): avoid ambiguous delivery replay (#125127) 2026-08-17 00:13:59 -07:00
Peter Steinberger 0e3a3aa203 refactor: finish canonicalization stragglers (#125056)
* refactor: finish canonicalization stragglers

* chore: remove release-owned changelog entry

* refactor: narrow SecretRef SDK import
2026-08-17 00:09:29 -07:00
Peter Steinberger 60f238c5af fix(ci): scope the Node toolchain cache to runners that need it
GitHub-hosted images carry Node 24.19.0 in /opt/hostedtoolcache, so hosted jobs
resolved from there and never populated the cached root. They still ran a
restore that could only miss and then a save whose path did not exist, logging
"Path Validation Error: Path(s) specified in the action for caching do(es) not
exist" on every hosted job. No entry was ever written, so nothing was poisoned,
but the warning is noise and the steps are pure waste on ~30 jobs per run.

Gate both steps on runner.environment != 'github-hosted', the signal this
workflow already uses for Blacksmith-only behavior, and gate the save on the
setup step reporting that a download actually populated the root. That second
guard also covers a future self-hosted image whose toolcache clears the floor.

Verified on Blacksmith: a satisfying image toolcache leaves the root absent so
the save is skipped, and a download populates it so the save runs.
2026-08-17 00:04:53 -07:00
Peter Steinberger 53cede420f perf(test): complete managed handoff explicitly (#125129) 2026-08-16 23:56:29 -07:00
ClawSweeper 84853266e7 fix(ui): widen the review sidebar by default (#125116)
* fix(ui): widen code review panel by default

* test(ui): align sidebar layout default

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-16 23:55:49 -07:00
Peter Steinberger 2e1a9a1a3d perf(gateway): reduce chat history read-path work (#125123) 2026-08-16 23:54:20 -07:00
Peter Steinberger 83d279a4c1 fix(cli): honor channel resolve agent ownership (#125109)
* fix(cli): honor channel resolve agent ownership

Forward explicit channel owners through command-time secret collection and later workspace/channel resolution, preventing AGENT_SELECTION_REQUIRED in multi-agent configurations.

* fix(cli): narrow explicit agent option value

Use runtime string narrowing for the Commander option so agent-owner forwarding remains assertion-free and satisfies the assertion-safety ratchet.

* docs(cli): document channel resolve agent owner
2026-08-16 23:50:52 -07:00
Peter Steinberger 7b18510e6c fix(config): materialize fresh-install defaults for missing config (#125110)
* fix(config): materialize fresh-install defaults for missing config

A missing config file (fresh install) skipped runtime-defaults
materialization on both loadConfig and readConfigFileSnapshot, while an
existing empty {} config got the full defaults (compaction safeguard,
session/cron/model defaults). Out-of-box behavior silently diverged from
the documented defaults until the first config write created the file.
The drift dates to bc75968074, which dropped the missing-branch
materializeRuntimeConfig call during an import-trim.

Route the missing-file branches through the same load/snapshot
materialization as existing configs, and delete the now-unreferenced
per-mode defaults profile table in materialize.ts — load and snapshot
must materialize identically anyway (prepared-runtime exact-config
resolution depends on it), so the profile indirection only invited
drift.

* chore(ratchet): shrink io.load.ts assertion baseline to 2

The missing-config fix removed an 'as OpenClawConfig' cast; the
assertion-safety ratchet requires the baseline to shrink with it.
2026-08-16 23:48:46 -07:00
Peter Steinberger b1e76c4640 refactor(ui): define review default document (#125105) 2026-08-16 23:48:28 -07:00
Peter Steinberger 29b105beda docs(gateway): record why Control UI admission exemptions exist (#125108)
* docs(gateway): record why Control UI admission exemptions exist

* docs(gateway): state the configured-root exemption reason precisely
2026-08-16 23:45:54 -07:00
ClawSweeper 1d65e7b449 test: default trusted checks to local execution (#125120)
Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com>
2026-08-16 23:45:50 -07:00
Peter Steinberger 4e1f26dc18 refactor(gateway,ui): one bounded display projection; delete marker strip sites (#124997)
* refactor(gateway,ui): one bounded display projection; delete marker strip sites

Persisted transcripts are marker-free since the write-boundary projection
(#124793), the historical migration (#124888), and TTS facts (#124913), so
display surfaces stop compensating. sessions.list.lastMessagePreview and its
siblings (sessions.preview/describe, TUI picker, sessions_list tool, MCP) now
share one bounded role-aware projection (240 chars, tool/system/thinking and
suppressed control replies excluded, directive-only rows fall through). The
web reply chip reads the typed openclawDelivery fact instead of parsing text;
chat.history preserves the field to the UI. Post-hoc display strips are
deleted across web/TUI/MCP/sessions-list; live streaming cleaners stay.
Stale gateway-protocol preview comments corrected; no schema change.
Assertion-safety baseline pruned for shrunk files (sanctioned direction).

Production net -173, tests net -137. Fixes the sidebar [[reply_to_current]]
preview leak and the empty-code-pill overstrip of quoted markers.

* fix(agents): preserve restart recovery transcript reads

* refactor(gateway): remove obsolete transcript exports

* fix(gateway): normalize injected delivery directives

* fix(ci): scope projection and recovery checks

* chore(ci): shrink plugin SDK surface budgets

* test: deflake loaded side question and worker checks

* test: align display projection CI fixtures

* style: format display projection fixture
2026-08-16 23:44:29 -07:00
Peter Steinberger 59c661bf66 fix(gateway): prevent large history from blocking chat startup (#124994)
* fix(gateway): bound startup history materialization

Apply SQLite byte limits before loading history payloads and share a yielded, fingerprinted Claude CLI snapshot across concurrent startup clients. Preserve marker, cursor, redaction, and external identity semantics, and clarify that shrink-only ratchet updates need no separate approval.

* test(browser): await all lazy command groups

* fix(gateway): bound history snapshot state

* fix(gateway): preserve oversized history responsiveness
2026-08-16 23:43:40 -07:00
Peter Steinberger 73ab74eccc fix(discord): preserve routed agent for realtime voice (#125111) 2026-08-16 23:41:38 -07:00
ClawSweeper 6b86199892 fix(ui): keep session status icons visible (#125096)
Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com>
2026-08-16 23:39:21 -07:00
Peter Steinberger 3010318ba3 fix(android): isolate debug app installs (#125099) 2026-08-16 23:36:57 -07:00
Peter Steinberger ac5c8be001 fix(ui): keep agent terminals backgrounded (#125101) 2026-08-16 23:32:06 -07:00
Peter Steinberger 1204225303 chore(scripts): enforce erasable TypeScript syntax (#125104) 2026-08-16 23:31:27 -07:00
Peter Steinberger c26eda0620 feat(approvals): add request kind discriminant (#125084)
* feat(approvals): add request kind discriminant

* test(approvals): expect normalized legacy request
2026-08-16 23:30:39 -07:00
Peter Steinberger 76b26c31dc fix: keep device identity reads lock-free (#124745)
* fix: keep device identity reads lock-free

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* feat(lint): enforce import ordering and deduplication (#124730)

* refactor(imports): dedupe and hoist imports

* feat(lint): enforce import/no-duplicates and import/first

* feat(voice-call): sessionScope "main" routes calls into the agent main session (#124708)

* feat(voice-call): add sessionScope "main" for main-session call routing

Inbound and outbound calls can now share the configured agent's main
session instead of a dedicated voice session. The new scope resolves
through the existing explicit-key canonicalization path, honoring core
session.mainKey and global-scope aliasing. The inbound webhook path now
forwards coreSession like every other resolver call site.

* style: format events.test.ts

* test(voice-call): split events.test.ts under the max-lines cap

* fix(cli): forward agent name through setup (#124740)

Amp-Thread-ID: https://ampcode.com/threads/T-01a00b6b-e4e9-74af-bb31-30363fae6c89

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

* refactor(mcp): centralize tool filter policy (#124739)

* fix(agents): preserve restart-unsafe delivery evidence (#124737)

* fix(ci): snapshot synced Testbox candidates (#124743)

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

* fix(ci): keep QA evidence output repo-relative (#124760)

* fix(cli): allow hook toggles to select an agent (#124761)

* test(upgrade): drop retired agent context assertion (#124765)

Amp-Thread-ID: https://ampcode.com/threads/T-01a00a6a-b64e-74a5-8b15-2d3b966a468d

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

* fix(skills): report missing ClawHub skills as not found (#124764)

* feat(codex): show sessions from additional homes (#124660)

* feat(codex): register additional session homes

* fix(codex): preserve automatic session homes

* docs(codex): explain automatic home discovery around sessionCatalog.homes

---------

Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>

* fix(qa): preserve effective fast mode (#124762)

Amp-Thread-ID: https://ampcode.com/threads/T-01a00b6b-e4e9-74af-bb31-30363fae6c89

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

* fix(ui): keep Control UI device identity working on plain-HTTP origins (#124724)

* fix(ui): keep Control UI device identity working on plain-HTTP origins

@noble/ed25519 defaults its SHA-512 provider to crypto.subtle, which
browsers gate to secure contexts, so device identity silently vanished
on http:// LAN dashboards and connects fell back to shared-credential
auth with no pairing. Wire a lazy pure-JS @noble/hashes fallback for
SHA-512 and the fingerprint SHA-256, and drop the isSecureContext gate
in the connect path. Secure contexts keep the platform digests and pay
no startup bytes: the fallback loads as its own lazy chunk, kept out of
the gateway-runtime startup chunk on purpose.

* test(ui): cover device identity minting and signing without crypto.subtle

New jsdom regression suite fails on pre-fix code (subtle-less crypto stub
with getRandomValues, which real insecure contexts keep). Rewrites the
gateway connect tests that previously asserted the device-less insecure
fallback: an insecure context now attaches a device identity.

* docs(web): plain-HTTP dashboards now pair with a device identity

The signing key never crosses the wire, so HTTP+pairing is strictly
stronger than the old HTTP token-only fallback; HTTPS (Tailscale Serve)
stays the recommendation for transport privacy.

* fix(ui): drop unnecessary boolean literal compare in secure-context timing meta

* test(ui): declare device.id on the connect-frame test shape

* test(ui): split the subtle-less scope-upgrade e2e into the two real invariants

Without crypto.subtle the browser can now sign, so the banner offers the
explicit admin upgrade; manual-only guidance is reserved for browsers that
cannot mint an identity at all (no WebCrypto RNG). Also corrects the
connect-path comment: blocked storage yields an ephemeral identity, only a
failed mint degrades device-less.

* fix(ui): address review findings on the HTTP device-identity path

- Storage-blocked pages keep one stable in-memory identity per page
  lifetime instead of minting a fresh unpaired key on every reconnect,
  and a write-rejecting store no longer fails the mint (regression tests
  bite pre-fix).
- Connect timing now reports the real browser secure-context fact via a
  shared browserSecureContext() helper instead of inferring it from
  device-identity presence.
- Docs state the accepted trusted-proxy contract: browsers attach a
  device identity on every origin, so first connects follow the standard
  pairing flow (deviceAutoApprove or a one-time approval); device-less
  admission remains only for browsers that cannot mint an identity.

* refactor(ui): trim the connect-path additions under the max-lines cap

* fix(line): retry lost pushes without duplicating an accepted send (#124464)

A LINE push made exactly one attempt, so a transient provider or transport
failure dropped the reply even though retrying was safe to do. Retrying alone
would have duplicated a send LINE already accepted, so every push now carries an
X-Line-Retry-Key and reuses it across attempts: LINE answers a replayed key with
409 and the accepted request's sent messages, which resolves to the original
delivery instead of a second message.

Retries follow LINE's documented policy - server errors and transport failures
only, never 2xx, 409 or any 4xx - and run through the shared channel API retry
runner in strict mode. Replies stay single-attempt because LINE offers no retry
key for them.

* fix(outbound): record queue-write loss and fail every payload on prep abort (#124756)

* fix(outbound): record queue-write loss and fail every payload on prep abort

Two silent-observer gaps in the outbound queue admission path:

1. Best-effort staging/queue-write failures were swallowed with a bare
   'return null'. The send proceeded live-only, so a crash mid-send lost
   the message with zero forensic trace of why the write-ahead row was
   missing. Every sibling degradation on this boundary logs
   (persistQueuedPreSendState, the dispatch-refresh fallback); this one
   now warns with channel/target and the error.

2. Batch preparation failure emitted message_sent for only the first
   payload while audit emitted per-payload terminals, so plugins with
   message_sent hooks saw 1 failure for an N-payload batch. The failure
   path now emits one hook failure per logical payload, matching the
   audit terminals and the recovery sibling's
   queuedTerminalFailureEvents. The unused error.payload pick and its
   OutboundPayloadPreparationError import went with it.

Regressions: queue-write warn asserted in the existing best-effort
fallback test; new multi-payload prep-failure test asserts 2 hook
failures. Both fail pre-fix.

* chore(outbound): keep OutboundPayloadPreparationError module-local

Last external consumer went away with the per-payload emit; the
deadcode:exports gate rejects unused exports.

* refactor(gateway): drop dead session-subscriber registry surface (#124754)

* refactor(gateway): drop dead session-subscriber registry surface

getForConnection and both registry clear() methods had no production
callers — only server-chat-state.test.ts consumed them (verified by
repo-wide grep incl. plugin-sdk). The recency-sorted connToSessionKeys
index existed solely so getForConnection returned keys in subscription
order, an ordering nothing read, rebuilt via toSorted() on every
subscribe/unsubscribe/settle — including the provisional-replay settle
path.

unsubscribeAll now reads the reverse index directly from
connToSessionRecency's keys (same contents, no ordering), removing the
per-call O(k log k) rebuild and the duplicate map. Tests assert through
get()/getApprovals(), the production read paths, per test-audit norms.

Behavior-neutral: subscription state transitions and event-visible
delivery unchanged; chat-state, broadcast, node-subscription,
instance-runtime, agent-events, session-events suites pass.

* test(gateway): fix ETXTBSY flake in workspace-sync candidate bounding

The test rewrote its mock git script between two createGitTransferList
calls. On Linux CI the second spawn can race a lingering fork of the
first child still holding the script's write fd, failing exec with
ETXTBSY. Write the script once; vary the entry count through a data file
the script reads instead.

* fix(config): preserve migration notice paths (#124719)

* fix(runners): preserve device sessions through lifecycle faults (#124744)

* test(runners): cover paired-node lifecycle wire

* fix(runners): preserve retryable device lifecycle

* test(runners): keep wire fixture internals local

* fix(runners): stop fallback on device capacity

* fix(doctor): keep identity probe within typed storage APIs

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* fix(openai): keep setup auth choices consistent (#124729)

* fix(openai): centralize setup auth metadata

* refactor(openai): overlay auth hooks in place

* fix(tasks): cancel subagents through gateway (#124752)

* fix: honor context caps on Codex-routed models (#124735)

* fix(codex): forward authored context caps

* test(codex): keep binding fixtures uncapped

* fix(agent-harness): separate authored context cap

* fix(config): render actionable validation errors (#124763)

* fix(config): render actionable validation errors

* fix(config): cover service and gateway validation

* docs: remove agent-hierarchy guardrail from VISION roadmap

Agent-created agents (durable roster entries with provenance) are now
planned work, so the blanket VISION.md guardrail against agent-hierarchy
frameworks no longer reflects direction. The adjacent guardrail against
heavy duplicative orchestration layers stays.

* fix(gateway): preserve worker lifecycle failure reasons (#124774)

* fix(cli): explain empty directory lookups (#124753)

* fix(cli): explain empty directory lookups instead of printing nothing

* fix(cli): distinguish unsupported self lookups

* fix(codex): honor session node exec host (#124777)

* refactor(line): replace nine-marker prompt DSL with typed rich messages (#124755)

* refactor(line): replace nine-marker prompt DSL with typed rich messages

Delete the LINE plugin's double-bracket marker language (quick_replies,
location, confirm, buttons, media_player, event, agenda, device,
appletv_remote) and its parser. Portable interactions now flow through the
existing presentation-block seam (renderPresentation, matching Discord and
Feishu); LINE-specific cards ride closed channelData.line schemas mapped to
the existing Flex renderers. Prompt section shrinks to four capability
lines and explicitly de-fangs marker text. Removes the stale
assertion-safety baseline entry for the deleted parser.

Production LOC net -69, tests net -433. Suite: 510/510 green.

* fix(line): declare rich message schema dependency

* fix(line): satisfy rich message type checks

* docs(line): mark card fragments as partial

* feat(ui): surface approvals passively and redesign the approval card (#124767)

* feat(ui): surface approvals passively and redesign the approval card

Approvals no longer auto-open the centered modal: the queue is reachable
only through the sidebar attention chip, while the owning session shows
the inline card and other sessions surface via the session-row shield
icon, agent badges, and the chip. Deletes the inline-vs-modal exclusion
machinery (modalApprovalQueue, inlineApprovalId, forceShowAll) and makes
modal dismissal close the view instead of denying the active request.

Card: severity now drives the accent color instead of a table row,
plugin/agent render as header chips, the session key moves behind a
collapsed Details disclosure (modal only), and low-value exec rows
(Resolved/Security/Ask) collapse into the same disclosure. The codex
app-server bridge stops duplicating the session key into description
text; the envelope already carries it.

* fix(ui): gate settings Escape on the approval dialog's recorded open state

ClawSweeper caught that shouldIgnoreSettingsEscape still inferred an open
approval dialog from queue non-emptiness; with passive approvals a pending
queue no longer implies a visible dialog, so settings would swallow Escape.
The exec-approval element now records dialogOpen as a fact and the guard
reads it.

* refactor(channels): carry typed approval-prompt bindings through signal/imessage send seams (#124742)

The Signal and iMessage channel plugins inferred approval prompts by
regex-sniffing rendered message text (header matching like 'Exec approval
required', /approve command parsing), violating the transport-only channel
doctrine: approval actions must stay typed until channel encoding.

The typed envelope already existed (channelData.execApproval with
approvalId/approvalKind/allowedDecisions) and every payload-level delivery
path consumed it; the regex paths were redundant re-derivation at the raw
send seam plus restart recovery for in-memory iMessage poll state.

- Signal: delete send-level prompt sniffing entirely; all approval sends
  already flow through typed structured-payload or native-handler paths.
- iMessage: sendMessageIMessage takes a typed approvalPrompt binding
  (id/kind/decisions) from the native approval handler instead of an
  approvalKind flag plus text re-parsing.
- iMessage poller: persist pending poll targets in the plugin keyed store
  so restart recovery no longer regex-scans chat history; typed recent-chat
  discovery for handle-only DM targets stays. Split poll-target ownership
  into approval-reaction-poll-targets.ts (max-lines).
- Plugin SDK: remove extractApprovalReactionPromptBinding — beta-only
  surface, never in a stable release, so no deprecation window applies;
  AGENTS.md now records that rule.

Accepted tradeoff: approval prompts delivered by a pre-upgrade process are
not rediscovered from chat text after restart (<=24h transient state;
persisted reaction bindings and event-driven tapbacks still work).

* fix(ci): select aggregate QA evidence by manifest (#124802)

* docs: correct code mode runtime contract (#124792)

Amp-Thread-ID: https://ampcode.com/threads/T-01a00b7c-b9f0-73d5-8fb7-e619e8e458e1

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

* fix(ci): invalidate extension boundary cache for attempt types (#124800)

* refactor(plugin-sdk): consolidate session catalog families (#124801)

* fix(memory): fail corrupted index commands (#124770)

* fix(memory): fail corrupted index commands

* fix(memory): preserve promotion output limit

* fix(memory): keep ranked promotion output

* feat(codex): name additional session-catalog homes (#124807)

* fix(state): copy-safe agent database registry via state-relative paths (schema v9) (#124728)

* fix(state): store in-root agent database registry paths relative to the state dir (schema v9)

Copied state directories retained stale absolute agent database registry rows. The combined gateway store then merged old and new copies, causing every sessions.list request to fail with SessionCanonicalKeyMigrationRequiredError.

Schema v9 stores in-root registry paths relative to the state directory; migration rewrites eligible rows, deletes stale duplicates, and preserves external paths.

* fix(voice-call): describe state-relative registry migration

* fix(state): preserve registry locator traversal

* test(macos): move PortGuardian schema-version boundary to v9

The store opens any state database up to maximumSupportedSchemaVersion,
which the schema v9 bump moved from 8 to 9. Shift the supported/newer
partition in the boundary test accordingly.

* fix(state): re-anchor copied default-layout registry rows instead of deleting them

Resolve the ClawSweeper P1 by preserving genuine external default-layout registrations, re-anchoring copied rows when an in-root counterpart exists, and deleting only dual default-layout conflicts.

* fix(upgrade): retain owners in legacy cron stores (#124809)

Amp-Thread-ID: https://ampcode.com/threads/T-01a00a6a-b64e-74a5-8b15-2d3b966a468d

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

* feat(tools): prompt proactive widget use (#124810)

* fix(ui): stop stale clients from acting connected (#124772)

* fix(ui): stop stale clients from acting connected

Carry reload metadata on terminal Control UI build rejections, render an explicit refresh-required recovery state, fence reconnect-only actions, and surface disconnected approval failures with accessible modal controls.

* fix(ui): preserve reconnecting session drafts

* fix(ui): preserve offline preference intent

* fix(ui): return owned fallback digest bytes

* test(ui): follow passive approval presentation

* fix(ui): distinguish stale builds from protocol mismatches

* style(ui): format gateway recovery phase

* refactor(validation): type producers and drop internal re-validation — pilot lane C (#124808)

* fix(cli): restart disabled installed services (#124786)

* refactor(agents): persist directive facts at the assistant write boundary (#124793)

* refactor(agents): persist directive facts at the assistant write boundary

Assistant final text is now stripped of inline delivery directives once, at
the SessionManager append boundary, with the parsed facts persisted as a
typed openclawDelivery field on the assistant message (openclawDeliveryMirror
precedent, inside event_json — no schema change). Live session state is
replaced with the persisted canonical bytes so same-run continuations and
next-turn store rebuilds never diverge (prompt-cache invariant).

Delivery and restart recovery consume the stored facts instead of re-parsing
persisted text; pre-upgrade in-flight messages without facts intentionally get
no text-parse fallback (commented tradeoff at the read site). Directive
parsing is now code-region aware, so markers quoted in inline code or fences
neither parse nor strip — fixes the empty-code-pill display bug.

The undocumented reaction marker DSL ([[react:]], [[react_to_current:]]) is
deleted; structured message-tool reactions remain the canonical path. Stale
assertion-safety baseline entry removed with the parser.

Production net -10, tests net +38. Focused suites: 9,068 passed; pre-fix
regressions fail for the intended reasons. Codex autoreview clean.

* fix(agents): satisfy directive persistence gates

* fix(config): drop unused directive export

* fix(agents): preserve indented directive examples

* perf(test): remove ClickClack scheduler waits (#124819)

* refactor: split max-lines pilot files and retire suppressions (#124813)

* fix(runners): align reclaim results with runtime (#124791)

* refactor(runners): prepare placement move targets

* fix(runners): preserve normalized dispatch targets

* style(protocol): format reclaim result type

* fix(ui): make light-theme diff syntax readable (#124814)

Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com>

* fix(ci): render only aggregate QA evidence (#124823)

* fix(ui): show session changes in review panel (#124824)

* fix(gateway): stop closed clients from restoring session subscriptions (#124771)

* fix(gateway): reject closed session subscribers

* test(gateway): type subscription race params

* test(gateway): use public subscription assertion

* fix(ci): invalidate SDK declarations on attempt types

* chore(ci): align boundary cache comment with main

* fix(gateway): surface failed node event delivery (#124798)

* fix(gateway): surface failed node event delivery

* fix(plugin-sdk): preserve authored context cap

* fix(mcp): make Codex approval dead ends actionable (#124766)

* fix(mcp): make Codex approval dead ends actionable

* fix(mcp): preserve native approval fallback

* fix(mcp): configure saved approval modes

* fix(mcp): preserve saved Codex metadata

* style(mcp): simplify saved metadata spread

* improve(ui): one primary composer button, Enter-steer, Esc-stop, quiet mic, no transcript focus ring (#124799)

* fix(ui): drop the transcript keyboard-focus ring

The .chat-thread:focus-visible 2px inset outline (added in #120693) boxed
the entire main content whenever the transcript took keyboard focus
(Shift+Tab, closing transcript search) and read as a broken border that
stopped above the composer. The transcript stays keyboard-focusable for
scroll keys; it just paints no focus box. Accepted tradeoff: no visible
focus indicator on the scroll container.

* feat(ui): single primary composer button with Enter-steer and Esc-stop

The composer now renders exactly one primary action, always rightmost:
- draft or attachments present: one send-shaped button whose label follows
  the session follow-up mode (Send / Queue / Steer); the stop button no
  longer renders beside it
- empty while a run is active: Stop (tooltip "Stop (Esc)"); Escape in the
  composer aborts the run, deferring to reply-clear and menu precedence
- empty while idle: disabled send labeled "Write a message to send."

Enter on an empty composer while a run is active steers the oldest
steerable queued message — Enter queues, Enter again steers — using the
same eligibility predicate as the queue chip's Steer action (shared
steerableQueuedMessage helper). The mic/dictation control is now always a
compact secondary control left of the primary instead of morphing into
the primary when the composer is empty. Dead ChatRunControlsProps fields
(hasMessages, onExport, onNewSession, showPrimary, showSecondary) and the
composer exportMarkdown wrapper are removed; the assertion-safety
baseline entry for chat-composer.ts shrinks 8 -> 7.

* refactor(ui): quiet the idle composer mic

The idle mic was an accent-tinted two-segment pill (mic + permanently
visible device-picker chevron) that outweighed the primary action. It now
matches the attach button's ghost weight: transparent idle, subtle hover,
and the accent treatment is reserved for the recording and talk-active
states so the idle -> recording escalation reads stronger. The device
picker chevron collapses at idle and reveals on hover/focus-within (kept
always visible on coarse pointers, where dictation matters most; reveal
transition disabled under prefers-reduced-motion). The idle tooltip now
teaches the gesture split — "Tap to talk · Hold to dictate" — while the
aria-label stays "Start voice input".

* refactor(ui): split composer keydown and oversized composer test files

chat-composer.ts crossed the 700-line oxlint budget after the keyboard
additions; the keydown handling (menu routing, history keys, Escape-stop,
Enter steer/send) moves to chat-composer-keydown.ts behind a typed deps
factory. The composer unit tests split their primary-action/keyboard
coverage into chat-composer-actions.test.ts over a shared
chat-composer.test-support.ts, and the catalog/model-discovery e2e cases
move to chat-composer-catalog.e2e.test.ts. Pure moves, no behavior
change; no max-lines suppressions or baseline entries added.

* chore(ui): keep composer test-support ComposerProps internal

The split exported a type nothing imports; the dead-export scan is a
merge gate.

* fix(ui): narrow composer keydown target and adapt keyboard tab-order proof

The keydown split moved two bare textarea assertions into a zero-baseline
file; a single instanceof narrowing at the handler entry replaces both
(the handler only binds to the composer textarea). chat-composer.ts's own
assertion baseline ratchets 7 -> 5. The pointer-activation e2e now walks
the real post-redesign tab order (mic, focus-revealed picker trigger,
then primary send) instead of assuming one Tab.

* fix(ui): gate Enter-steer on the queue chip's visible availability

ClawSweeper P1: the Enter path only required an abortable run, so
offline Enter swallowed the keypress and invoked a steer lifecycle that
returns at !connected with no visible outcome, while the queue chip's
Steer button was correctly hidden behind connected && canCompose. The
keydown path now mirrors the chip's exact boundary (connected, canSend,
abortable, visible queue surface including sessionKey-less pending
rows).

* refactor(ui): align approval page with redesigned approval card (#124816)

* refactor(ui): align approval page with redesigned approval card

Follow up #124767 by carrying severity accents and identity chips onto the standalone approval page.

Remove the unused execApproval.labels severity, plugin, and agent keys.

* refactor(ui): keep approval-page helpers in repo idiom

Reverts drive-by rewrites from the build pass: restore the exhaustive
decisionLabel switch (the ternary lost the never check), the early-return
appliedDecisionMatches, explicit gatewayCode comparisons, and align the
severity mapping shape with the sibling exec-approval-card.ts.

* test(ui): type approval severity fixtures

* test(scripts): wait for a parseable descendant pid in Bun smoke cleanup

The readiness gate only required the descendant pid file to exist, but
existsSync can observe it between open() and write(); a loaded runner
then reads an empty file and the integer assertion flakes (seen on PR
#124816 CI run 31971191374). Readiness now requires parseable content.

* perf(webui): reuse the chat metadata catalog across pages (#124794)

* refactor(webui): centralize chat metadata caching

* test(webui): cover shared chat metadata store

* refactor(webui): move ChatMetadataResult type to its store owner

* fix(webui): let the metadata store own revalidation lifetime

* fix(webui): satisfy no-promise-executor-return in retry delay

* fix(browser): stop local actions when agent turns are cancelled (#124795)

* fix(browser): cancel local actions with agent signal

Amp-Thread-ID: https://ampcode.com/threads/T-01a00b6b-e4e9-74af-bb31-30363fae6c89

* fix(agent-harness): expose authored context cap

Amp-Thread-ID: https://ampcode.com/threads/T-01a00b6b-e4e9-74af-bb31-30363fae6c89

* chore(agent-harness): drop redundant context-cap override

Amp-Thread-ID: https://ampcode.com/threads/T-01a00b6b-e4e9-74af-bb31-30363fae6c89

---------

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

* improve(ui): make chat disclosures compact (#124832)

Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com>

* fix(gateway): share generated session titles with worktree names and start title work at dispatch (#124787)

Worktree sessions now derive their worktree/branch name from the same
concise generated title the sidebar shows (bounded 8s wait, raw-prompt
slug then crustacean fallback), persisted once as displayName so the
chat-send pass never duplicates the model call. Dashboard title
scheduling moves from the dispatch chain's finally (after the whole
first turn) back to dispatch start, so long or interrupted first turns
no longer leave sessions silently untitled (regression from #122471).
slugifyWorktreeTitle truncates at word boundaries; dead
generatedDisplayName plumbing removed.

* fix(browser): support Chrome Web Store native bootstrap (#124775)

* fix(browser): support Chrome Web Store native bootstrap

* chore: keep browser release note in PR body

* docs(browser): document Store identity trust boundary

* docs(browser): correct Store recovery guidance

* fix(media): bound image-optimize fetch headroom to the image cap (#124838)

When a caller passed an explicit maxBytes with optimizeImages on (the
default), the source-read bound was inflated to max(maxBytes, 100MB
document cap). A channel with a 5MB attachment cap therefore let a
hostile or oversized URL buffer up to 100MB before the final size check
— 20x resource amplification, and the inflation applied before content
classification, so it was not limited to images.

The inflation exists so oversized-but-compressible originals can be
fetched and compressed under the delivery cap. Size that headroom off
the thing that justifies it: 4x the 6MB image cap (24MB), enough for
large phone photos, instead of the document cap.

Accepted tradeoff (named at the constant): originals above 24MB that
would have compressed under the cap now fail; the fetch error names the
bound. Callers without an explicit cap keep per-kind defaults.

Live proof on a real localhost HTTP server (no mocked fetch): an 8.1MB
PNG under a 5MB cap compresses to 3.75MB and delivers; a 30MB-declared
image is rejected at the Content-Length precheck in 7ms with 'content
length 31457280 exceeds maxBytes 25165824'.

Regression: headroom-bound test fails pre-fix (30MB passes the old
100MB bound); companion test proves compression headroom still works.

* fix(channels): route direct DM replies to sender (#124837)

Default direct-message reply context to the authenticated sender address so targetless Reef and Nostr replies do not resolve to the local recipient.

* feat(auto-reply): record message-tool mute outcomes (#124830)

* feat(auto-reply): record message-tool mute outcomes

* fix(auto-reply): require source delivery evidence

* test(infra): preserve historical agent schema fixtures

* fix(agent): preserve implicit roster in configless exec (#124805)

Amp-Thread-ID: https://ampcode.com/threads/T-01a00b7c-b9f0-73d5-8fb7-e619e8e458e1

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

* fix(delivery): unify terminal settlement ownership (#124825)

* fix(delivery): unify terminal settlement ownership

Treat identityless adapter returns as potentially visible across channel, queue, and cron paths. Let recovery own terminal completion so ambiguity persists as notice debt instead of being double-settled or silently suppressed.

Co-authored-by: ruel225 <ruel225@users.noreply.github.com>

* refactor(delivery): narrow terminal internals

Remove now-unused internal exports after terminal-settlement ownership was consolidated.

* test(tts): preserve message runtime exports

Import and spread the actual message runtime so the focused mock retains every runtime binding while overriding only the durable send core.

---------

Co-authored-by: ruel225 <ruel225@users.noreply.github.com>

* fix(ui): give queued-outbox badges their own outbox icon (#124842)

The sidebar queued-message badge and the composer queue reused the clock
glyph that also marks attached automations, so a session with both showed
two identical clocks distinguishable only by tooltip. Add an outbox tray
icon (Lucide inbox with the roof swapped for a rising arrow) and use it
for queued-to-send surfaces; the clock stays automation/cron-only. The
mock dev fixture now flags one session with hasAutomation so the badge
renders in mocked-dashboard proofs.

* refactor(plugin-sdk): extract stream and SecretRef primitives (#124835)

* refactor(ui): derive cron types from gateway protocol (#124827)

* refactor(ui): derive cron types from gateway protocol

* fix(protocol): preserve cron session target type

* fix(ui): keep sent images visible through history handoff (#124839)

* fix(memory): report indexed SQLite sessions (#124834)

* fix(memory): report indexed SQLite sessions

* refactor(memory): remove unused state path export

* refactor(validation): consolidate boundary guards into schemas (#124820)

* refactor(validation): consolidate boundary guards into schemas — pilot lane B

* fix(meetings): reject non-object browser status

* perf(test): remove Gmail watcher shutdown wait (#124851)

* refactor(types): discriminated unions for internal contracts — pilot lane A (#124845)

* refactor(slack): mark approval headers with typed block ids (#124841)

* feat(ui): add steer-now composer shortcut (#124826)

* fix(slack): drop the tool-call receipt from finished progress cards (#123851)

The Block Kit session card terminalized into a '🛠️ N tool calls · ⏱️ Ns'
footer, leaving a per-turn receipt in the Slack transcript on the default
progress path. PR #122976 deleted the same receipt from the native progress
surface but left this sibling rendering it.

Finished cards now keep only the durable diff stat and the session link;
tool-call and elapsed counters stay live working state. Also corrects the
documented channels.slack.streaming.mode default, stale since #122552.

* fix(agents): keep queued followups from missing post-compaction context (#124850)

* fix(auto-reply): order post-compaction context before followups

Amp-Thread-ID: https://ampcode.com/threads/T-01a00b6b-e4e9-74af-bb31-30363fae6c89

* test(auto-reply): clean compaction workspace fixture

Amp-Thread-ID: https://ampcode.com/threads/T-01a00b6b-e4e9-74af-bb31-30363fae6c89

---------

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

* fix(test): propagate parent heavy-check ownership (#124859)

Amp-Thread-ID: https://ampcode.com/threads/T-01a00b7c-b9f0-73d5-8fb7-e619e8e458e1

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

* fix(cli): emit one JSON failure contract for --json invocations (#124849)

* fix(cli): unify JSON failure output

* test(cli): update skills verify failure envelope

* fix(gateway): preserve prepared plugin metadata under load (#124844)

* fix(gateway): preserve prepared plugin metadata under load

* fix(gateway): project metadata before catalog preparation

* test(agents): assert projected startup metadata

* test(gateway): bind load probes to each finalization wave

* docs(ci): record artifact checkout experiment

* perf(test): reuse postpublish file-cap fixture (#124871)

* fix(protocol): model cron run delivery trace (#124856)

* fix(protocol): model cron run delivery trace

* chore(protocol): refresh generated cron run model

* test: replace assertion chains with typed fixture builders — wave 2 (#124865)

* refactor(security): consolidate path containment onto canonical fs-safe guard (#124870)

* refactor(security): expose canonical path containment modes

* refactor(agents): use canonical path containment guard

* refactor(security): consolidate core path containment sites

* refactor(plugins): consolidate path containment sites

* test(security): cover canonical path containment behavior

* fix(gateway): persist agent turns before acknowledgement (#124857)

* fix(gateway): persist agent turns before acknowledgement

* fix(gateway): revalidate exec approval handoffs

* docs(gateway): clarify durable session ownership

* fix(gateway): reject stale transcript admission targets

* fix(ui): show recovery when Control UI does not render (#124861)

* fix(ui): retire bootstrap fallback after render

* test(ui): avoid returning from Promise executor

* fix(qa): recover repeated gateway restarts through channel ingress (#124746)

* fix(qa): repair repeated gateway restart recovery

* test(qa): satisfy restart recovery checks

* fix(audit): rekey repeated recovery identity

* fix(plugin-sdk): expose authored context cap

* test(android): synchronize process tree readiness

* fix(ui): restore task count badge (#124840)

* fix(ui): align update status card spacing (#124878)

* fix(browser): cancel node discovery with tool runs (#124880)

Amp-Thread-ID: https://ampcode.com/threads/T-01a00b6b-e4e9-74af-bb31-30363fae6c89

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

* perf(webui): render cached model catalog while the chat pane refreshes (#124868)

* perf(webui): render cached model catalog while the chat pane refreshes

* test(webui): fix seed-test types and shrink assertion baseline

* fix(state): report v9 registry migration row decisions (#124862)

* fix(state): report v9 registry migration row decisions in doctor and logs

* fix(state): consume registry migration summary type

* fix(state): avoid registry path observer import cycle

* fix(workboard): include actual length in bounded-string error

The shared normalizeBoundedString helper in extensions/workboard/src
threw an opaque error like 'comment body must be 2000 characters or
fewer.' whenever an oversize string hit any bounded field. Agents that
posted a long run summary or oversized link URL had no way to tell
how far over the limit they were, so they could not retry with a
trimmed payload.

Include the actual length in the error so the message tells the agent
exactly what to do next:

  comment body must be 2000 characters or fewer (got 3502).

The format applies uniformly to every bounded field routed through
this helper (labels, link URL, link title, link target, comment
body, title, notes, worker log message, session key, run id,
protocol violation detail, etc.) since the helper is the single
canonical owner of the message.

Cover the new format with a focused unit test that pins the regex
for the comment-body repro from #118845 and exercises the six other
bounded fields listed in the issue. Update the existing
gateway.test.ts assertion to allow both the old and new format so
the format change is a forward-compatible improvement.

* test(workboard): align matrix with real normalizeBoundedString consumers

ClawSweeper flagged that the prior matrix at line 31-48 tested
labels/title/notes by manually invoking normalizeBoundedString, even
though those fields use the independent normalizeTitle, normalizeNotes,
and normalizeLabels functions (each with its own generic message).
A regression that drops the (got N) suffix from normalizeBoundedString
would not be caught for those three surfaces — operators would still
see the old opaque error.

This commit narrows the matrix to only the consumers of the shared
helper (comment body, link title, link target, link URL, card id) and
restores the exact labels gateway assertion that should match the
unchanged normalizeLabels error.

Verification: 9/9 inline Node 18 AST harness (node /tmp/verify-pr-118888-v77.mjs);
parent-commit proof-gate (pre-PR helper without the got suffix) fails
4/9 of the post-fix expected messages.

Closes ClawSweeper review on PR #118888 (P2 + P3).

* test: cover Workboard length errors at gateway boundary

* docs: note length-aware Workboard errors (#118888)

* perf(ci): split Windows CI two ways instead of one serial lane

`checks-windows-node-test` ran the whole 222-266s Windows body in a single
Blacksmith lane, so every run that scheduled it finished at 287-334s against a
~180s plateau. Measured across 45 canonical main runs it was the wall in all
seven runs that triggered it (324-519s vs a 210s median elsewhere).

Blacksmith's Windows class admits exactly 2 concurrent jobs (run 31865243804),
which is why the earlier 3-part split failed: its tail queued 211s behind a
finished part. Pin the split width to that measured capacity instead. The two
parts are balanced by measured per-project wall time -- part 1 keeps the old
list 1 plus list 2's non-infra projects (~108s), part 2 takes list 2's infra
project plus the old list 3 (~112s) -- so both land near 113s and the lane
stops setting the wall.

One canonical partition now serves every backend; the github-mode/dispatch
3-part variant and its separate file inventory are gone, along with the
single-lane `task: test` case and the per-row `matrix.runner` the collapsed
lane needed. The complete 65-file Windows inventory is preserved exactly, and
the guards pin the width on both sides (mutation-checked: restoring 3 parts
fails the matrix guard).

* fix: stop forked commands after Gateway timeouts (#124081)

* fix: stop service child trees before timeout completion

* fix: preserve service child root results during cleanup

* test: isolate relay selection from linux spawn wrapping

* refactor: keep service anchor payload internal

* test: validate lifecycle probe pid output

* fix: close service child adversarial races

* fix: preserve service startup failures

* fix(process): flush service output before root result

* fix(process): bound cleanup on held output

* chore(process): satisfy promise executor lint

* fix(process): finish cleanup after lineage loss

* fix(process): retain relay output before subscription

* fix(ci): register service child runtime entries

* fix(ci): expose relay safety invariants

* fix(process): preserve service cancellation grace

* test(process): prove service grace on macOS

* fix: prevent clipped session selections and Code Mode timer crashes (#124879)

* fix(agents): support timers in code mode

* fix(ui): keep selected sessions clear of scrollbars

* fix(agents): propagate timer cancellation after resume

---------

Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com>

* fix(ui): reveal clipped session history (#124866)

* fix(ui): reveal clipped session history

Show a persistent earlier-history action for paginated sessions and reveal loaded content without chaining observer fetches.

Related: #110771

* refactor(ui): split chat history support

* fix(cli): keep internal error causes out of operator output (#124887)

* fix(skills): expand explicit references on agent turns (#124784)

* fix(skills): expand explicit references on agent turns

Route generic Gateway, CLI, webhook, and local agent turns through the same explicit skill-reference renderer as channel auto-replies. Keep original transcript text, preserve unknown slash behavior, and fail visibly for allowlist-hidden skills.

Maintainer review: scoped Option 1 — generic agent turns expand both $skill-name and leading /skill-name args through shared skill rendering; they do not run the channel command dispatcher, and all other slash commands retain their existing behavior.

* fix(skills): bound explicit reference prompts

* fix(skills): prefer allowed reference collisions

* fix(skills): preserve command invocation boundaries

* fix(skills): reject hidden channel slash commands

* perf(skills): skip literal dollar discovery

* fix(ui): replace the broken terminal tab underline (#124875)

* fix(ui): redesign dock panel tabs

* fix(ui): remove stale terminal tab overrides

---------

Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com>

* fix(tui): close selection overlays and report rejected handlers (#124876)

openSelector's onSelect fired the async handler with void and only
closed the overlay after a successful await. A rejecting handler (e.g.
setAgent -> setSession against a failing gateway) left the selector
stranded open with an unhandled rejection: the TUI froze on the picker
with no visible cause — a silent dead-end.

Root cause: failure path missing from the overlay lifecycle. The
handler now catches, surfaces the cause via chatLog, and always closes
the overlay.

Regression: /agent selection with a rejecting setSession asserts the
overlay closes and the cause reaches the chat log — fails pre-fix.

* fix(ui): make terminal open failures retryable (#124890)

* fix(sandbox): surface list probe failures instead of empty results (#124881)

sandboxListCommand and fetchAndFilterContainers swallowed backend and
registry probe failures with catch(() => []). A broken Docker daemon or
corrupt registry rendered as 'No sandbox runtimes found.' — and in
recreate, filtered an entire fleet down to nothing — indistinguishable
from a clean empty state. --json emitted a success envelope for a
failed probe.

Root cause: failure collapsed into the empty success shape at the
consumer. The listers now propagate; the sandbox CLI runner already
owns the error path (message + exit 1), and the new --json failure
contract (6c66f48a7c) emits the JSON error envelope.

Regression: replaced the test that enshrined the masking ('handle
errors gracefully' asserting the empty-state message) with the
propagation contract — fails pre-fix.

* fix(tui): report the first last-session write failure (#124886)

* fix(tui): report the first last-session write failure

rememberCurrentSessionKey swallowed writeTuiLastSessionKey failures
with catch(() => undefined). A corrupt or locked state DB silently
disabled session restore: the operator only discovers it on next
launch, landing in the wrong session with no clue why.

Root cause: fire-and-forget with no recorded failure. The write wrapper
moved to its owner (tui-last-session.ts) as
createRememberSessionKeyWriter, which surfaces the first failure via
the chat log ('session memory write failed: ...') and stays silent on
repeats — one line per TUI run, not one per session switch. Empty and
'unknown' keys still skip without touching the writer.

Regression: colocated tests prove first-failure-once reporting and the
skip path — fail pre-fix (factory did not exist; failure was
swallowed at the call site).

* chore(tui): make the last-session write dependency explicit

deadcode:exports rejects writeTuiLastSessionKey as unused when only the
factory default referenced it; tui.ts now passes it explicitly.

* fix: remove delivery markers from historical sessions (#124888)

* fix(sessions): migrate historical transcript directives

* fix(infra): complete directive migration on pre-archives-schema databases

session_transcript_archives is a lazy additive v17 surface; valid databases
may omit it. The archives phase now treats the missing table as empty and
completes the cursor instead of warning-skipping the whole database forever.
Verified against a real long-lived agent DB: previously stuck cursor
completes with zero warnings in ~100ms.

* chore(ci): allow directive migration SQLite access

* fix(sessions): report self-archive before stopping the turn (#124896)

* feat(agents): record agent creation provenance and add roster tree listing (#124828)

Adds an additive agent_provenance table (shared state DB, schema v8) owned
by src/state/agent-provenance.ts. createAgent() records operator/agent
provenance after commit, the system-agent create-agent operation passes its
own id as creator, and Claw installs record created-via claw at their roster
commit point. Agent deletion removes the agent's own row inside the deletion
journal transaction; children keep dangling creator ids as historical fact.
openclaw agents list gains --tree (provenance hierarchy) and JSON provenance
fields.

* fix(ui): keep message presentation stable when expanded (#124898)

* fix(gateway): stop a plugin dangerous flag from revoking desktop computer.act (#124863)

* fix(gateway): stop a plugin dangerous flag from revoking desktop computer.act

`resolveNodeCommandAllowlistInternal` subtracted every plugin-declared
dangerous command from the composed allowlist, including core's own
`PLATFORM_DEFAULTS` entries and including the pairing allowlist, where core's
dangerous defaults are exempted. `computer.act` is both a desktop platform
default (grant = node-local enablement + pairing approval) and a command that
`registerComputerUseProvider` marks dangerous. Since `cua-computer` became
enabled-by-default on darwin, every macOS-hosted Gateway stripped `computer.act`
from both allowlists, so `normalizeDeclaredNodeCommands` dropped it from the
node's declaration with no pairing upgrade, no prompt, and no record. The
`computer` capability survived because caps were never allowlist-filtered, and
the `computerUse` descriptor was then dropped for the missing command.

Scope the plugin-dangerous subtraction to commands outside the platform-default
base: the flag still keeps a plugin's own surface behind an explicit allow and
still forces a registered invoke policy, but it no longer revokes a command core
declares itself.

Also make cap-without-command unrepresentable. `retainFulfilledNodeCapabilities`
drops a capability when policy withheld commands from its family and admitted
none, and the reconciler records the withheld commands so a refused declaration
is never silent.

* test(gateway): type the computer-use fixture against its contract

* fix(doctor): partition retired session stores (#124846)

Recover owner-qualified rows from the retired top-level session store into each configured agent database without guessing ambiguous ownership. Relocate stale transcript paths, tolerate historical Unicode IDs when an explicit transcript exists, and avoid double-counting physical SQLite databases in migration totals.


Amp-Thread-ID: https://ampcode.com/threads/T-01a00a6a-b64e-74a5-8b15-2d3b966a468d

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Josh Lehman <josh@martian.engineering>

* perf(ci): price agents-core-models from its measured hybrid wall

`agentic-agents-core-models` carried the 36s scaled estimate while measuring
56.3s (n=6, p90 58.6s) across 260 compact jobs on 2026-08-16. Packed beside
`agentic-agents-core-runtime-hosted-1` (60.4s) it built the only bin running
>=1.25x its prediction: compact-large-19 ran 122s of work priced at 88s, which
made it the most frequent slowest job in the workflow.

Add the measured value to the hybrid hint map, which already exists for exactly
these Blacksmith-specific observations. The packer then separates the two, and
the tallest actual compact bin drops 122s -> 109s with no bin above 1.25x and
no change to row counts (48 push / 56 pull-request), so this costs no extra
runner registrations.

Two things deliberately left alone. The `agentic-gateway-core-3` 140s pin looks
like a 2x over-prediction against its 68.5s median, but run 31938297538 really
did take 138.0s (109.03s + 28.98s across its two configs) on a run whose fleet
slowdown factor was 0.98 -- a genuine tail on a healthy run, so unpinning it
would rebuild a >200s bin whenever the tail lands. Every other bin already sits
within 1.24x of its prediction.

The replaced guard pinned one bin arrangement (`runtime-hosted-1` not sharing a
job with `agents-core-tools`) with no stated failure mode, so any honest refit
broke it. It now asserts the property that comment was reaching for -- both
weight sources survive rebalancing under the body ceiling -- plus the specific
regression this fixes. Mutation-checked: dropping the hint fails the new guard.

Measurement note for the next refit: sum a shard's per-config Duration lines
before taking a median. Pooling them reads as a large over-prediction that is
not there.

* fix(codex): prevent app-server leaks after desktop fallback (#124885)

* fix(codex): reuse app server after managed fallback

* test(codex): opt relay fixtures into loop detection

* fix(ui): add spacing between suggested task actions (#124900)

* fix(ui): separate suggested task card actions

* test(ui): keep task suggestion spacing check lint-safe

* fix(ui): drop duplicate plan card summary (#124895)

* fix(backup): give backup create actionable output errors (#124894)

* fix(backup): give backup create actionable output errors

* fix(backup): preserve output errors for debug

* fix(test): create sessions in default Control UI mock (#124912)

Amp-Thread-ID: https://ampcode.com/threads/T-01a00b6b-e4e9-74af-bb31-30363fae6c89

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

* fix(plugins): adopt root context engines on caller-owned handles (#122457)

Keep caller-owned agent runtime registries in discovery mode so full-only
plugins cannot replace process-global sandbox backends. Copy runtime
context-engine factories from the composition-root registry instead.

Co-authored-by: Josh Lehman <550978+jalehman@users.noreply.github.com>

* docs(codex): clarify loop relay behavior (#124918)

* fix(gateway): terminalize stale worker dispatches (#124920)

* refactor(ui): simplify chat history pagination (#124921)

* test(gateway): make archive lifecycle waits deterministic (#124923)

* fix(android): remove completed plan strikethrough (#124916)

* fix(plugins): restore caller-owned workspace plugin-root projection

#124844 made resolvePluginMetadataSnapshot synthesize workspacePluginRootPresent
itself with a process-memoized fs.existsSync probe whenever a caller did not
supply it. That fact is owned by the prepared-model-runtime lease, which resolves
it only for real agent runs (resolveWorkspacePluginRootPresence) and passes it
explicitly, so the probe silently turned an opt-in fast path into the default for
unrelated control-plane callers.

Gateway startup config validation is one of them. It never asserts the fact, so it
began projecting the published lifecycle graph instead of loading a fresh one, and
the projection derives configFingerprint from that graph rather than a real load.
Startup convergence rewrites the persisted plugin index between the two reads that
form the migration checkpoint identity, so the pre-convergence read projected while
the post-convergence read loaded. The two pluginMigrationFingerprint values differed
and the gateway refused readiness with "OpenClaw plugin migration inputs changed
during startup convergence", failing every config-patch restart.

Deleting the probe restores one owner for the fact and a net-negative production
diff. The agent-side hunks of #124844 are untouched: they still pass the fact
explicitly from the lease.

Root cause: admission fact manufactured outside its lifecycle owner.
Owner boundary: src/agents/prepared-model-runtime-lease.ts owns workspace
plugin-root presence; src/plugins/plugin-metadata-snapshot.ts only consumes it.
Production LOC: -18.

Fixes red main ci-gate: QA Smoke memory-dreaming-sweep, matrix-restart-resume,
matrix-post-restart-room-continue.

* fix(doctor): preserve migrated session owner (#124928)

Amp-Thread-ID: https://ampcode.com/threads/T-01a00a6a-b64e-74a5-8b15-2d3b966a468d

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

* refactor(webui): read slash commands through the chat metadata store (#124931)

* fix(doctor): stop reporting phantom shared auth migrations (#124929)

* fix(cron): stop advertising inactive JSON defaults (#124903)

* fix(cron): make --json help match behavior

* fix(cron): preserve scratch write JSON output

* test(cli): classify cron JSON result commands

* fix(ui): remove dashboard card text underlines (#124939)

* fix(agents): refuse add when prompts cannot run (#124940)

* test(ui): assert steer order by rendered position (#124905)

* fix(plugins): keep migration metadata authoritative (#124924)

Restrict exact config identity trust to prepared runtime generations and force startup convergence to verify against a fresh authoritative plugin snapshot.

* test(mistral): wait on delivered transcripts after reconnect (#124935)

* test(mistral): wait on delivered transcripts after reconnect

* test(nvidia): assert the remaining catalog fetch budget as a bound

* fix(cli): show actionable guidance for mistyped commands (#124892)

* fix(cli): render unknown commands as guidance

* fix(cli): keep parse JSON terminal-neutral

* refactor(sdk): source wire models from gateway-protocol schemas (#124943)

* test(ui): split chat follow-up E2E suite (#124938)

* fix: honor provider timeouts during stuck-session recovery (#123877)

* fix(diagnostics): respect active model request timeouts

* chore(diagnostics): keep recovery activity internal

* test(diagnostics): await stream wrapper results

* fix(diagnostics): harden provider timeout recovery

* fix(diagnostics): track compaction model calls

* test: prove provider timeout recovery boundary

* fix(config): reject scalar config roots instead of loading defaults (#124945)

A config file whose JSON5 root parsed to a scalar (null, a number, a
bare string — the classic truncated/clobbered file) hit a special case
in loadConfigFromContext that returned {} with a snapshot marked
valid: true. Three failures in one: the process silently ran with
defaults while the operator's real config (channels, auth, allowlists)
disappeared; the valid snapshot could promote the corrupt file's
fingerprint as lastKnownGood, poisoning the clobber-recovery machinery
that exists to catch exactly this; and the load path contradicted the
snapshot path, which correctly reported the same bytes invalid.

Root cause: an early-return that collapsed invalid input into the
empty-config success shape. Deleted; a scalar root now flows into
validateConfigObjectWithPlugins, fails schema validation, records a
valid:false snapshot, and throws INVALID_CONFIG — same as an array
root, same as the snapshot path, handled by doctor's invalid-config
recovery.

Regression: new io.scalar-root.test.ts (null/number/string roots all
throw INVALID_CONFIG; snapshot agrees invalid) — all 3 fail pre-fix.
Updated the compat warning-fingerprint test that relied on null-root
loading as defaults.

* fix(security): prevent approved scripts from changing before execution (#124858)

* fix(security): bind exec approvals to mutable operands

* test: align approval fixtures with operand binding

* fix(security): close durable operand binding gaps

* fix(security): preserve independent exec policy grants

* improve(ui): simplify widget preview chrome (#124883)

Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com>

* fix: deliver requested image artifacts across agent runtimes (#124910)

* fix: distinguish image inspection from delivery

Clarify that image-reading tools only load private model context, prefer native Codex image inspection when available, and preserve explicit outbound tool media across Claude CLI loopback runs.

Co-authored-by: pash <pash@openai.com>

* fix: propagate CLI vision capability

* chore: refresh CI after main fix

---------

Co-authored-by: pash <pash@openai.com>

* fix(ci): derive plugin SDK boundary cache inputs (#124897)

* feat: add cloud worker profiles and machine selection (#124864)

* feat(workers): add per-dispatch machine classes

* feat(ui): add cloud worker machine picker

* feat(tts): add structured reply speech fields (#124913)

* feat(tts): add structured reply speech fields

* fix(tts): keep transcript directive parser acyclic

* fix(tts): drop unused directive fact export

* fix(outbound): preserve reply-payload metadata through response prefixes

The response-prefix rebuild spread the payload without its metadata carrier,
dropping structured speech facts (and any other payload metadata) whenever a
channel responsePrefix applied. Rebuild via copyReplyPayloadMetadata.

* fix(tts): keep voice-only sends visible when the speech runtime is cold

A structured voiceText-only send with no visible text returned unchanged from
the cold-runtime guard, so delivery normalized the empty payload to null and
the send silently vanished. The cold exit now applies the same visible
fallback as synthesis failure: explicit spoken text materializes as payload
text when no other content exists.

* fix(qa): isolate capture store startup (#124949)

* refactor(channels): align approval reaction bindings (#124942)

Split Signal approval routing into its concept-owned module and call the shared SDK binding helpers directly.

Reject persisted reaction targets when any allowed decision is invalid or duplicated, rather than retaining a valid subset from a corrupt transient record.

* docs(agents): harden live-test and capture-inspection requirements

Live verification of user-facing behavior is the default before landing,
with skips requiring a stated infeasibility in the PR; captured
screenshots/videos count as evidence only after the agent inspects them
in frame.

* refactor(ui): define sidebar panel contract (#124950)

* improve(ui): move session status to second row (#124960)

* improve(ui): move session status to second row

* test(ui): align session row layout expectations

---------

Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com>

* docs: clarify model switching guidance (#124889)

* docs: clarify model switching guidance

* docs: warn that reasoning changes invalidate cache reuse

---------

Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com>

* fix(ui): stop duplicate dashboard session queries (#124968)

* fix(doctor): surface legacy-config copy failures (#124948)

* fix(doctor): surface legacy-config copy failures

maybeMigrateLegacyConfig swallowed every copyFile error with a bare
catch whose comment claimed EEXIST-only. EACCES, ENOSPC, and
cross-device failures were silently ignored: the operator's
~/.clawdbot/clawdbot.json exists, migration was attempted and failed,
and doctor proceeded as a clean fresh install with no config, no change
note, and no warning.

Root cause: failure collapsed into the skip-silently success shape. The
catch now rethrows anything other than EEXIST with the source/target
paths in the message; EEXIST (config already at the target) keeps its
skip semantics.

Regression: read-only target dir makes the preflight reject with
'Failed to migrate legacy config' — fails pre-fix (resolved as a clean
run); companion test proves the successful copy path still migrates.

* chore(doctor): annotate the errno assertion for the safety ratchet

* refactor(doctor): narrow the copy error without a type assertion

The assertion-safety ratchet flagged the errno cast; property narrowing
removes the assertion instead of annotating it.

* fix(android): show plan status and explanation (#124958)

* fix: scale Gateway RSS diagnostics to runtime limits (#124914)

* fix(logging): scale RSS pressure to runtime limits

Derive Gateway RSS warning and critical thresholds from the smaller of the effective V8 heap and OS process limit. This preserves heap and rapid-growth pressure detection while avoiding routine false alerts on large heaps.

* test(logging): narrow optional heap fixture

* fix(logging): bound process memory sentinels

* test(logging): pin Bun physical memory fixture

* refactor(channels): delete the per-turn progress receipt line (#124972)

The finished-turn digest ('🧠 N thoughts · 💬 N notes · 🛠️ N tool calls ·
⏱️ Ns') was synthesized onto three surfaces: Slack's Block Kit card footer
(removed in #123851), Discord's final-answer subtext, and a duplicate Telegram
implementation added to match Discord.

Nothing gated it — no config key for the receipt has ever existed on any
channel — so PR #122976, which deleted it from Slack's native progress surface,
could only ever be a partial removal. Finish it: live progress surfaces keep
their in-flight indicators, and a finished turn leaves the answer with nothing
synthesized appended.

Telegram's window collapsed *into* the digest, so it now retires like every
other surface; Discord already discarded its draft after the final landed. The
core tracker keeps only the live counters Slack's working footer reads.

* feat(agents): preserve create request provenance (#124963)

* refactor(types): discriminated unions for internal contracts — wave 2 (#124959)

* fix(gateway): surface blocking startup progress (#124309)

Keep Gateway admission sidecar-gated while surfacing retryable startup progress in TUI and Control UI.

Unify deferred startup ownership and shutdown draining so late workers, plugin services, and sidecars cannot outlive transport teardown.

Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>

* test(node-host): own proxy tunnel lifecycle

* fix(codex): preserve resumed session usage (#124969)

* fix(doctor): report malformed final JSONL records (#124977)

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

* feat(agents): show creation hierarchy in Control UI (#124967)

* feat(agents): show creation hierarchy in Control UI

* chore(protocol): regenerate Swift agent model

* refactor: consolidate record guards (#124970)

* fix: plugin tools disappear from Codex and restricted profiles (#124947)

* fix(memory): make standing intents reachable

* fix(agents): dedupe macOS read path variants

* docs(plugins): document profile tool metadata

* test(codex): complete prepared runtime fixtures

* fix(agents): preserve prepared tool boundaries

* fix(cli): prevent node/device RPCs from corrupting shared state (#123674)

* fix(cli): prevent node/device RPCs from corrupting shared state

* fix(cli): keep gateway RPC transport state read-only

* fix(cli): preserve doctor lint config isolation

* fix(cli): keep gateway RPC transport state read-only

* fix(gateway): keep auth preflight state read-only

* fix(cli): preserve SQLite artifacts in read-only RPCs

* fix(infra): remove stale identity store import

---------

Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>

* fix(codex): reject malformed native tool arguments (#124649)

* fix(codex): reject invalid native tool arguments

* fix(codex): preserve raw argument preparation

Signed-off-by: sallyom <somalley@redhat.com>

---------

Signed-off-by: sallyom <somalley@redhat.com>
Co-authored-by: sallyom <somalley@redhat.com>

* feat: main-session agent-wide visibility + session.groupScope routing (#124965)

* feat: add main session group routing

* docs: explain main session routing scopes

* fix: align memory session visibility

* test(qa): cover main-scoped group bindings

* fix(sessions): preserve binding-scoped outbound routes

* fix(routing): preserve explicit outbound owners

* fix(sessions): recognize global main visibility

* chore(ci): prune assertion safety baseline

* fix(installer): finalize deferred PATH installs successfully (#124678)

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

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

* fix(cli): align agent exec with system owner (#124697)

* fix(onboard): run channel hooks after config commit (#124736)

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

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

* fix(ui): hide mobile transcript metadata until tap (#124936)

* fix(ui): reveal mobile message metadata on tap

* fix(ui): centralize mobile transcript disclosure

Co-authored-by: Vyctor H. Brzezowski <51521767+vyctorbrzezowski@users.noreply.github.com>

---------

Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com>

* fix(openrouter): keep OAuth loopback open after invalid callbacks (#124922)

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

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

* refactor(validation): consolidate boundary guards into schemas — wave 2 (#124961)

* docs(cron): recommend lighter models for routine automation jobs (#125000)

The cron model picker help and the automations docs now say what
operators otherwise learn by surprise on the bill: summaries, triage,
and classification jobs run well on a lighter model, and --fallbacks
covers escalation. Copy-only change; pnpm ui:i18n:verify green, no
baseline drift.

* fix(release): allow beta Docker backfills (#124999)

* perf(ci): cache the Node toolchain instead of refetching it every job

Blacksmith's image tracks an older runner-images snapshot. Measured on a leased
box 2026-08-16, its toolcache holds Node 20.20.0, 22.22.0 and 24.13.0 while this
repo's engines floor is >=22.22.3 and >=24.15.0 -- short by three and two
patches. Every candidate is rejected, so all 306 of 306 sampled jobs fell
through to a nodejs.org download. GitHub-hosted runners carry 24.19.0 and
resolve from the toolcache in about a second, which is why only Blacksmith pays.

Normally that download is 2.6s (p99 3.3s), but ~46 jobs fetch the same 50 MB
simultaneously and the mirror throttles: three of 53 sampled runs had setup-node
medians of 44-93s with maxes to 139s, and because every job pays at once it
lands whole on the wall -- those runs went ~210s to 325s.

Keep the payload in the Actions cache, which Blacksmith serves from its
colocated backend. Measured on Blacksmith: cold 1605ms, warm 77ms.

Restores are prefix-keyed and the save carries the resolved patch. An exact key
would be worse than nothing: cache entries are immutable and an exact hit
suppresses the post-job save, so a floating `24.x` key would pin the first Node
it ever saw and, once the floor advanced past it, every job would restore the
rejected payload and re-download forever. Keying the save on the installed
version lets a newer resolve publish a new entry that later prefix restores
pick up.

A rejected payload is pruned before the replacement installs, because the entry
is saved wholesale and a leftover would ride along in every future save.
Windows keeps its existing path. Proven on Blacksmith across cold, warm, stale
and truncated-binary cases; both guards are mutation-checked.

This stays useful even if Blacksmith refreshes their image: the floor moves
independently of the snapshot, so the gap recurs. The image refresh is still
the better fix and is worth asking them for.

* improve(android): drop the role title on every chat message (#124985)

* fix(android): let chat bubbles carry message roles

* test(android): prove chat speaker accessibility

* chore(android): refresh native i18n inventory

* ci: shrink assertion safety baseline

* fix(android): satisfy Compose modifier lint

* docs: cover groupScope in faq and security guide (#125006)

* fix(agents): preserve resolved agentId in model workspace resolution (#124954)

* fix(agents): preserve resolved agentId in model workspace resolution

resolveModelWorkspaceDir re-resolved a default agent instead of reusing
the agentId the caller already authorized, so any plugin call to
runtime.llm.complete with an explicit agentId threw
AgentSelectionRequiredError on multi-agent configs. Thread the known
agentId through resolveModel/resolveModelAsync into workspace
derivation so it is only re-derived when the caller has none.

Fixes #124822

* fix(agents): forward agentId through route-materialization re-resolution

The route-materialization resolver call in prepareSimpleCompletionModel
re-resolved the model without the caller's agentId, so a multi-agent
plugin completion that switches OpenAI routes still fell back to
resolveDefaultAgentId and threw. Forward params.agentId on this call
too, matching the first resolution.

* fix(gateway): bind worker authority to exact lifecycle claims

* refactor(gateway): carry exact worker claims

* test(gateway): model exact worker claim lifecycle

* fix(codex): report dynamic tool audit starts once (#124976)

* fix(codex): dedupe dynamic tool audit starts

* fix(codex): preserve eager dynamic tool audit starts

* test(codex): make audit timing proof nonblocking

* test(codex): consolidate dynamic audit lifecycle proof

* test(codex): declare side-question tool arguments

* fix(ui): stop duplicate Sessions page queries (#125018)

Make the Sessions route and page share the managed session-list owner so startup hydration, reconnects, filters, and mutations cannot schedule duplicate raw roster requests. Preserve last-good rows and retire stale query epochs.

* fix(ui): inset limited access badge from chat edge (#125012)

* fix(gateway): prevent control-plane polling stalls (#124891)

* fix(gateway): avoid repeated control-plane scans

* fix(tooling): allow concurrent worktree validation

* fix(ci): refresh protocol and runner inputs

* perf(ui): defer hidden session refreshes

* fix(ui): resolve session refresh lint failure

* fix(update): preserve pre-cache update channel

* fix(update): normalize cached update channel

* fix(gateway): lifecycle-cache update install identity

* fix(ui): preserve manual history retry after layout scroll

* test(codex): repair side-question tool schema fixture

* fix(ui): show Review immediately when opening files (#124990)

* fix(ui): show review before file content loads

* fix(ui): preserve review while disconnected

* fix(gateway): reject unattributable loopback proxy traffic [AI] (#119950)

* fix(gateway): bind auth limits to ingress attribution

* fix(gateway): close remaining ingress auth gaps

* fix(gateway): carry attribution into new ingress paths

* fix(gateway): close ingress ownership gaps

* fix(gateway): complete proxy ingress hardening

* fix(gateway): stabilize managed Tailscale ingress

* fix(gateway): make Tailscale cleanup ownership-safe

Refuse reset-on-exit publication until Tailscale exposes an atomic owner-bound cleanup operation, and migrate legacy configs with Doctor.

* fix(gateway): finish ingress ownership repair

* fix(gateway): own managed Tailscale route lifetime

Run managed Serve and Funnel routes as foreground claims tied to the Gateway lifecycle. Retire named Service config through Doctor because Tailscale Services cannot run in foreground mode.

Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>

* fix(gateway): align Tailscale consumers and build guards

Remove the retired named-service config from Telegram Mini App URL resolution and register the lifecycle worker as an explicit production entry.

Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>

* fix(gateway): preserve retired Tailscale inputs

Keep Funnel enabled when removing an ignored named-Service setting and accept the legacy positive reset flag as a no-op now that managed routes always follow Gateway lifetime.

Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>

* fix(gateway): preserve Tailscale route diagnostics

Prefer the actionable foreground CLI failure captured during timeout cleanup, and cover the original delayed-failure ordering.

Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>

* fix(gateway): reconcile Tailscale ingress with main

Preserve current ingress ownership contracts after the rebase, retire the obsolete device-auth migration check, validate route-owner IPC, and move Tailscale auth coverage onto the managed listener.

Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>

* fix(gateway): finish ingress rebase coverage

Unify the rebased net imports and let module-reset WebSocket tests prepare attribution through the same fresh module instance as the handler.

Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>

* test(gateway): align run-loop server fixture

---------

Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>

* fix(ui): preserve steer order and simplify composer ownership (#124915)

* refactor(ui): simplify composer steer ownership

* fix(ui): preserve target order for landed steers

* docs: align classic onboarding setup flow (#124712)

* docs(onboarding): align classic setup flow

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* docs(onboard): clarify import reset incompatibility

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

---------

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

* fix(install): reject invalid PowerShell installer options (#124882)

* fix(install): reject invalid PowerShell installer options

Bind installer parameters strictly so unknown switches and positional arguments cannot enter the mutating installer body. Validate environment options before initialization, add explicit help, and gate website publication on Windows PowerShell 5.1 and pwsh probes.

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* test(installer): validate strict PowerShell options

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

---------

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

* test(gateway): bind node admission to worker claim

* feat(audit): explain outbound message delivery (#123709)

* feat(audit): explain outbound message delivery

* fix(audit): record early message policy denials

* refactor(audit): split message delivery readers

* test(outbound): cover delivery audit lifecycle

* fix(audit): preserve message progress across downgrade

* fix(audit): keep progress out of activity protocol

* docs(audit): keep activity outcomes terminal-only

* fix(audit): bound merged delivery paging

* fix(audit): trust decision channel references

* fix(audit): keep retention constant private

* fix(audit): record broadcast target denials

* fix(audit): distinguish broadcast denial receipts

* test(qa): enforce message delivery receipt proof

* fix(audit): integrate C04 with schema v9 owners

* chore(audit): satisfy assertion safety gate

* refactor(agents): split explicit message target guard

* test(audit): materialize pinned reader in shallow CI

* test(qa): poll terminal audit persistence

* fix(delivery): defer audit terminal to recovery

* test(qa): isolate message delivery restart proof

* test(qa): keep gateway restart config canonical

* test(qa): exclude unrelated restart plugins

* test(qa): remove unused restart plugins

* test(qa): disable automatic memory plugin

* test(qa): converge replacement gateway startup

* fix(plugins): stabilize registry fingerprints

* fix(plugins): use public normalization export

* test(plugins): keep fingerprint fixture immutable

* fix(audit): bind delivery receipts to exact executions

* chore(audit): keep terminal binding helper private

* test(reply): cover unbound admission callbacks

* docs(agents): preserve durable delivery ownership

* refactor(android): collapse the chat's second color palette into ClawTheme (#125020)

* refactor(android): consolidate chat palette

* refactor(android): fold the font family into ClawTheme

* fix(agents): flush memory after CLI and gateway turns (#124964)

* fix(agents): run memory flush for agent commands

* test(agents): isolate memory flush in model switch tests

* fix(agents): require persisted CLI transcript before flush

* fix(agents): fence memory maintenance authority

* fix(agents): compact post-flush session successor

* test(agents): consolidate memory-flush rotation coverage

* fix(agents): preserve memory flush auth profile

* fix(agents): fence memory flush startup

* test(gateway): split node provisioning coverage

* improve: detect Claude CLI prompt cache regressions (#124988)

* test(gateway): cover Claude CLI prompt cache reuse

* test(gateway): validate Claude CLI cache across processes

* test(gateway): perturb Claude native cache context

* test(gateway): calibrate Claude CLI cache threshold

* docs(testing): align Claude cache threshold

* test(gateway): preserve config literal types

* test(gateway): split node admission coverage

* test: replace assertion chains with typed fixture builders — wave 3 (#124986)

* test: replace assertion chains with typed fixture builders — wave 3

* test: keep policy fixture type local

* test: type gateway context fixture

* test: type systemd stream fixture

* test: type chat pane fixture overrides

* fix(matrix): preserve thread binding activity on shutdown (#125039)

* fix(matrix): await binding flush on shutdown

Amp-Thread-ID: https://ampcode.com/threads/T-01a00b6b-e4e9-74af-bb31-30363fae6c89

* test(gateway): split provisioning intent coverage

Amp-Thread-ID: https://ampcode.com/threads/T-01a00b6b-e4e9-74af-bb31-30363fae6c89

* chore: drop superseded CI unblock

Amp-Thread-ID: https://ampcode.com/threads/T-01a00b6b-e4e9-74af-bb31-30363fae6c89

---------

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

* test(gateway): split provider-provisioning replay coverage (#125047)

Direct commit 4d64889290 pushed provider-provisioning.test.ts to 1091
lines, past the 1000-line max-lines cap, breaking check-lint-core-2 on
main. Move the replay/restart-adoption/serialization tests into
provider-provisioning.replay.test.ts (same suite setup via
service.test-support), matching the repo's split-not-suppress policy.

* perf(test): consolidate Control UI package fixtures (#125051)

* fix(test): ignore stale extension directories in labeler coverage (#125022)

* fix(gateway): deduplicate Claude CLI replies after reload (#125030)

* fix: preserve Unicode sessions during SQLite upgrade (#124951)

* fix(sessions): preserve unicode IDs during migration

Amp-Thread-ID: https://ampcode.com/threads/T-01a00a6a-b64e-74a5-8b15-2d3b966a468d

* fix(sessions): bound topic transcript filenames

Amp-Thread-ID: https://ampcode.com/threads/T-01a00a6a-b64e-74a5-8b15-2d3b966a468d

* fix(sessions): canonicalize Unicode IDs

---------

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

* fix: keep device identity reads lock-free

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* fix(doctor): keep identity probe within typed storage APIs

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* fix(sqlite): preserve empty read snapshots

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* test(infra): fix device identity SQLite mock typing

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

---------

Signed-off-by: sallyom <somalley@redhat.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: WhatsSkiLL <whatsskilll@gmail.com>
Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com>
Co-authored-by: Eden <146086744+edenfunf@users.noreply.github.com>
Co-authored-by: ClawSweeper <steipete+clawsweeper@gmail.com>
Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com>
Co-authored-by: ruel225 <ruel225@users.noreply.github.com>
Co-authored-by: Vyctor H. Brzezowski <krzyszchweski@gmail.com>
Co-authored-by: Kailigithub <12250313+Kailigithub@users.noreply.github.com>
Co-authored-by: Shakker <shakkerdroid@gmail.com>
Co-authored-by: Josh Avant <830519+joshavant@users.noreply.github.com>
Co-authored-by: Josh Lehman <josh@martian.engineering>
Co-authored-by: felirami <feliramii@gmail.com>
Co-authored-by: Josh Lehman <550978+jalehman@users.noreply.github.com>
Co-authored-by: pash <pash@openai.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Co-authored-by: Vito Cappello <hixvac@gmail.com>
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
Co-authored-by: PollyBot13 <pollybot13@gmail.com>
Co-authored-by: sallyom <somalley@redhat.com>
Co-authored-by: Leon-SK668 <0668001470@xydigit.com>
Co-authored-by: chelsealong <chelsealong@126.com>
Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>
2026-08-16 23:30:27 -07:00
Peter Steinberger a3eb8787ff fix(cli): keep offline gateway RPCs read-only (#125102)
Gateway health, suspend, and resume now preserve shared SQLite state when the target is unreachable.

Related: #101290

Follow-up to #123674
2026-08-16 23:29:30 -07:00
Peter Steinberger f77d2ec3a8 fix: stop repeated static catalog manifest scans (#125090) 2026-08-16 23:26:11 -07:00
Peter Steinberger 35695cb3bc fix(test): intersect scoped CLI filters with each lane's own scope (#125004)
narrowIncludePatterns returned the caller's CLI pattern whenever it overlapped
a lane's include list, instead of intersecting the two. A directory argument
therefore replaced the lane's curated scope with a broad glob:

  pnpm test src/plugins
  -> unit-fast include became ["src/plugins/**/*.test.*"] instead of its
     60 curated files
  -> contracts-plugin include became ["src/plugins/**/*.test.*"] instead of
     ["src/plugins/contracts/**/*.test.ts"]

Both lanes run isolate: false, so every re-admitted file shared a worker with
unrelated files. unit-fast re-admitted exactly the files it excludes for being
stateful (module mocking, dynamic import, filesystem state), and contracts-plugin
pulled in every sibling test outside contracts/. That produced nondeterministic
cross-file pollution: failures that moved between files and lanes run to run,
reproduced in neither isolation nor CI.

Keep the lane's own pattern when it is rooted deeper than the CLI selection,
otherwise keep the CLI pattern. Equal-depth selections are unchanged, so the
existing scoped-config expectations still hold.

Coverage is unchanged; only duplicate execution is removed. `pnpm test src/plugins`
went from 1171 file-executions (3.4x duplication over 346 files) to 344 — the two
remaining files are .e2e.test.ts, excluded by the shared config by design.
2026-08-16 23:24:11 -07:00
Peter Steinberger 2915e563d6 feat(workboard): show full boards on session dashboards (#125094)
* feat(workboard): full-board dashboard widget

Adds a third trusted plugin widget kind, workboard:board, rendering the
complete Kanban board (all columns, live plugin.workboard.changed
refresh) inside a session dashboard. Props {boardId} scope it to one
board; without it the widget shows every board, matching workboard:mini
semantics. Card rendering reuses the workboard page renderColumn with an
explicit surface option so page and widget cannot drift; the shared
board filter moved to ui/src/lib/workboard/board-filter.ts. Mutations
follow the connection's canMutate; read-only connections get a
view-only board.

Proof: pnpm test ui/src/lib/board ui/src/pages/workboard
ui/src/lib/workboard extensions/workboard (9,494 passed), i18n baseline
green, autoreview clean (codex/gpt-5.6-sol). tsgo core/ui/extensions
green locally; remote check:changed blocked by Daytona capacity and the
tsgo:core Daytona exit reproduced locally as a heavy-check lock race,
passing on retry.

* test(ui): seed workboard fixtures in the mocked dev server

* fix(ci): register workboard proof capture script
2026-08-16 23:19:30 -07:00
Peter Steinberger a3578c7790 test(qa): cover Discord progress draft lifecycle (#125089)
* test(qa): cover Discord progress draft lifecycle

* test(qa): allow live Discord draft proof

* ci(qa): select Discord provider mode
2026-08-16 23:17:53 -07:00
Peter Steinberger 7b4981bfd2 fix(tui): clear stale overlays after session restore (#125086)
Centralize TUI session identity in state so remembered-session restores use the same invalidation path as interactive switches. Fixes #125013.
2026-08-16 23:14:48 -07:00
Peter Steinberger 0af3b865d7 fix(gateway): fence legacy pending-result recovery (#125083) 2026-08-16 23:11:35 -07:00
Shubhankar Tripathy 2e52aa436e fix(agents): scope embedded prompt media roots to the owning agent (#124957)
* fix(agents): scope embedded prompt media roots to the owning agent (#123273)

* test(agents): exercise session-key-first media owner resolution per review

* fix(agents): hydrate embedded images from agent workspaces

Image attachments sent to a named (non-default) agent never reached the
model. The embedded prompt path hydrated images without an agent-scoped
media-root allowlist, so it fell back to the default roots, which grant
<stateDir>/workspace but not a named agent's workspace-<id>.

Both call sites now carry the owner already resolved for that path instead of
re-parsing session identity during prompt hydration. The plugin-harness path
passes workspace.sessionAgentId, and the settled embedded path passes
setup.sessionAgentId. The former threw on failure; the latter returned early
and silently dropped the image.

Roots are scoped only when workspaceOnly is off, because images.ts falls back
to [workspaceDir] when localRoots is absent. Passing agent-scoped roots
unconditionally would widen workspaceOnly rather than narrow it. Sibling
workspaces remain blocked because the scoped roots add only the owning agent's
workspace.

Related: #123273

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: sallyom <somalley@redhat.com>

* fix(agents): preserve scoped media on history replay

Signed-off-by: sallyom <somalley@redhat.com>

* fix(agents): scope native video replay media

Signed-off-by: sallyom <somalley@redhat.com>

* fix(media): preserve scoped workspace isolation

Signed-off-by: sallyom <somalley@redhat.com>

---------

Signed-off-by: sallyom <somalley@redhat.com>
Co-authored-by: Sai Sashankh D <530713+sashankh@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: sallyom <somalley@redhat.com>
2026-08-17 02:09:34 -04:00
Peter Steinberger 84f342a87e refactor(webui): drop gateway-version fallbacks per version-lock decision (#124971)
* refactor(webui): drop gateway-version fallbacks per version-lock decision

* refactor(webui): drop chat.startup history fallbacks

* test(webui): advertise full method catalog in terminal e2e scenarios

* refactor(webui): enforce the build-identity handshake at connect

* docs(webui): state exempted skew paths fail visibly by design
2026-08-16 23:09:25 -07:00
Peter Steinberger b097d64574 test(node-host): wait for complete grandchild pid (#125034)
Amp-Thread-ID: https://ampcode.com/threads/T-01a00b7c-b9f0-73d5-8fb7-e619e8e458e1

Co-authored-by: Amp <amp@ampcode.com>
2026-08-16 23:03:31 -07:00
Peter Steinberger 67ea9c2281 fix(cli): replace stale service selectors on profile switch (#124790)
* fix(cli): replace stale service profile selectors

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* refactor(cli): own service identity checks with daemon constants

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* fix(update): preserve managed home selector ownership

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* test(update): keep selector coverage within limits

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-08-16 23:00:08 -07:00
Peter Steinberger e84a45e28e test(plugins): reset the whole plugin metadata lifecycle between tests (#125037)
* test(plugins): reset the whole plugin metadata lifecycle between tests

Tests that touch process-global plugin metadata were calling
clearCurrentPluginMetadataSnapshot, which drops the published snapshot but leaves
every memo registered through registerPluginMetadataProcessMemoLifecycleClear
populated. Those memos are not content-addressed: setup-registry keys its cached
registries on resolveCurrentSetupSnapshotCacheId, which returns the literal
"nosnap" whenever no snapshot is published. A registry cached by one file is
therefore served to any later file that also runs without a snapshot.

The affected lanes run isolate: false, so that is a live cross-file leak. It
matches the observed failure shape, e.g. setup-registry descriptor lookup failing
with "Cannot read properties of undefined (reading 'flatMap')" only inside a
shared worker and never in isolation.

Switch these files to clearPluginMetadataLifecycleCaches, which clears the
snapshot and every registered memo as one lifecycle unit. Two files kept both
calls; the narrow one is now redundant and removed.

current-plugin-metadata-snapshot.test.ts keeps the narrow primitive: it is the
unit test for that function.

No production change.

* test(plugins): drop the duplicate lifecycle reset in provider-runtime hooks

The blanket clearCurrentPluginMetadataSnapshot -> clearPluginMetadataLifecycleCaches
substitution collided with the lifecycle call this suite already made, so each
test boundary ran every registered metadata-cache clear twice. Keep one call per
hook. Addresses the ClawSweeper P3 finding on #125037.
2026-08-16 22:56:01 -07:00
Peter Steinberger f9b1ccc4fa refactor(plugin-sdk): discriminate delivery and exec results (#124979)
* refactor(plugin-sdk): discriminate delivery and exec results

* test(plugin-sdk): update delivery target expectations

* fix(outbound): preserve canonical delivery targets

* fix(irc): normalize message delivery target
2026-08-16 22:55:41 -07:00
Peter Steinberger 54cbce25bd fix(onboard): give actionable managed gateway recovery (#124783)
* fix(onboard): keep managed gateway recovery on service path

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* fix(onboard): distinguish install and restart recovery

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* fix(onboard): localize gateway recovery guidance

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* style(onboard): format gateway recovery test

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* chore(ci): refresh service recovery validation

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-08-16 22:55:25 -07:00
Peter Steinberger 515fe51d8c perf(test): reuse setup registry module (#125087) 2026-08-16 22:52:46 -07:00
Peter Steinberger c4eb9078d9 feat(workboard): link boards to owning automations (#125076)
* feat(workboard): reference the automation job that owns a board's prompt

Boards can now record automationJobId, linking a board to the cron
automation whose prompt/model/schedule categorize its cards. The cron
job remains the single owner of that configuration; workboard stores
only the reference. The board toolbar shows an Automation chip linking
to the cron page when set. Deleting a board never deletes the job.

Proof: pnpm test extensions/workboard packages/workboard-contract
ui/src/pages/workboard ui/src/lib/workboard (9,492 passed), autoreview
clean (codex/gpt-5.6-sol). check:changed blocked by remote capacity
(Blacksmith down, Daytona lease cap); local fallback lanes green except
a parent-branch lint finding fixed on that branch.

* fix(ui): describe workboard automation destination
2026-08-16 22:52:26 -07:00
Peter Steinberger 3d1185f137 refactor(ui): colocate sidebar panel header actions (#125019) 2026-08-16 22:51:44 -07:00
Peter Steinberger d6be4f3552 fix(ui): fill short transcripts with earlier history (#125080) 2026-08-16 22:45:02 -07:00
Peter Steinberger b2709ce10d fix(gateway): honor selected model for worktree titles (#124902)
* fix(gateway): honor selected model for worktree titles

Carry the create request’s explicit or catalog-backed provider, model, runtime, and auth profile into create-time title routing without blocking on model-catalog loading.

Related: #124787

* fix(gateway): validate worktree title route

Reuse the canonical model-policy resolver before prestarting title generation. Denied or invalid selections now skip title completion while persisted creation remains the sole live-catalog availability validator.

* fix(gateway): normalize denied title selection

Preserve the null suppression sentinel at the preparation boundary while forwarding only the title generator’s optional entry shape.

* fix(gateway): inherit worktree title route

* fix(gateway): preserve normalized session categories

Keep post-commit category registration aligned with the normalized category persisted by session creation after the helper extraction.
2026-08-16 22:37:22 -07:00
Peter Steinberger ea3a5bf726 fix(config): record when the post-write canonical reread degrades (#125075)
The canonical reread after a committed config write silently kept the
in-memory config whenever the reread came back invalid or missing (a
concurrent edit racing the commit). Runtime and disk then diverged with
no recorded reason. Record the degradation via the config IO logger,
including the reread's issue summary. Expose the deps logger on the
createConfigIO facade (it already carries logger internally).
2026-08-16 22:37:08 -07:00
Peter Steinberger 49ff3e5b69 fix(validation): preserve wave 2 boundary compatibility (#125031) 2026-08-16 22:36:06 -07:00
Peter Steinberger bb5c27bf76 refactor(tts): derive runtime types from implementation (#125055) 2026-08-16 22:35:09 -07:00
Peter Steinberger 2af054fb9a fix(qa): prove repeated gateway restart recovery (#125052) 2026-08-16 22:34:55 -07:00
Peter Steinberger 0c4e2f6681 fix(onboard): open browser handoff in display-less WSL (#124704)
* fix(onboard): use canonical browser open support

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* test(onboard): type WSL browser detector mock

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* fix(onboard): preserve WSL detection cache

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* fix(onboard): remove stale browser platform probe

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* test(onboard): tolerate injected browser probes

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-08-16 22:33:46 -07:00
Peter Steinberger 816ae22921 fix(ui): recover channel setup after status check failure (#124680)
* fix(ui): recover Custodian channel status

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* fix(ui): invalidate channel status on reconnect

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* fix(ui): retain channel retry feedback

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* chore(ci): refresh channel retry validation

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-08-16 22:30:09 -07:00
Peter Steinberger fecb223e5b fix(plugins): align registry freshness inspection (#125048) 2026-08-16 22:29:55 -07:00
Peter Steinberger e3595c50ad fix(plugins): failed plugins retain diagnostics without activating capabilities (#125046)
* fix(plugins): make loader failures atomic

* style(plugins): avoid rollback key shadow
2026-08-16 22:29:26 -07:00
Peter Steinberger b3ab276077 fix: steering near run completion receives a response (#125038)
* fix(agent): consume late steering before terminalizing

* test(gateway): split provider bootstrap coverage
2026-08-16 22:19:27 -07:00
Peter Steinberger a2b5345b2b docs(agents): require explicit advisory authorization 2026-08-16 22:17:03 -07:00