* fix(gateway): rate-limit control-plane writes per method with a 30/min budget
* test(gateway): align control-plane rate-limit coverage
* ci: retrigger checks after main repairs
* refactor(channels): shared supplemental sender gating, allowlist-match adoption, outbound mop-ups
* fix(plugin-sdk): skip-aware media sequence with text fallback for empty URLs
* chore(plugin-sdk): align surface budgets after rebase
* test(qqbot): type media sender mock calls
* fix(plugin-sdk): distinguish empty media sequences
* fix(plugin-sdk): track void media sends
* feat: consume live featured catalog state
* fix(plugins): honor hosted curation for published bundled plugins
* fix(plugins): accept package-less trusted ClawHub provenance
* fix(channels): restart gateway when channels.start fails for unloaded plugin
After channels login, the CLI calls channels.start to reconcile the running
gateway. If the channel plugin was installed or enabled after the gateway
started, the plugin is not in the runtime registry and channels.start rejects
with 'invalid channels.start channel'. Detect this error and fall back to
gateway.restart.request so the gateway reloads plugins and starts the channel
automatically.
Fixes#90296
* ci: retrigger checks (update proof section format)
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: retrigger proof check (real behavior evidence)
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(channels): narrow unloaded-plugin restart
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(acpx): keep leaked non-openai model out of the Codex ACP thinking slot
Codex ACP spawn mis-routed an inherited non-OpenAI fleet default into the
reasoning-effort slot and aborted (#95780). Replace the splitter with a closed
classifier and make the spawn path provenance-aware: drop an inherited leaked
default so Codex starts on its own default, but fail closed with
ACP_INVALID_RUNTIME_OPTION when a caller explicitly selects an unsupported or
malformed model. Thread a modelExplicit flag from resolveAcpSpawnRuntimeOptions
through the ACP runtime ensure contract; strip it before the acpx delegate.
Dropping the inherited default only at ensureSession was not enough: the manager
still persisted the leaked model in runtimeOptions, and the first turn replayed
it through applyRuntimeControls -> setConfigOption(model), which the new
fail-closed Codex control path then rejected. ensureSession now reports the
effective model it applied on the returned handle (applied | dropped), and the
manager persists that effective model, so a dropped inherited default is never
saved or replayed as a model control before the first turn. Explicit unsupported
selections still fail closed at spawn and never persist.
* test(acp): split runtime config validation tests
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* [AI] fix(daemon): set default --max-old-space-size=8192 in gateway service env
Generate a managed NODE_OPTIONS with --max-old-space-size=8192 for the
gateway systemd service, discarding ambient host NODE_OPTIONS to prevent
--require/--inspect persistence. 8192 MB is chosen as a safe default
that prevents the common OOM crash-loop with the default Node ~4 GB heap
under sustained load.
Related to #96203
* fix(daemon): adapt gateway heap limit to host memory
* fix(daemon): keep gateway heap helpers internal
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(gateway): add sessions.files.reveal to open a session workspace on the gateway host
Resolves the session's workspace root server-side (same resolution as
sessions.files.list), refuses exec-node and cloud-worker placements, and
opens the directory with the platform opener. The opener helpers move out
of config.ts into a shared open-path module that config.openFile now uses.
* feat(ui): always-on chat title bar with inline rename and workspace menu
Every chat pane now renders its header: click the session title to rename
it inline (Enter commits via sessions.patch, Escape cancels), a workspace
chip names the session's checkout with reveal/copy-path/copy-branch
actions, and cloud-worker sessions show a globe. The single-pane floating
toggle cluster and the transcript's reserved titlebar drag band fold into
the header, which keeps native macOS window dragging.
* fix(ui): resolve title-bar session aliases and clear the collapsed-nav overlay
Live verification found two gaps: route aliases like ?session=main showed
the generic key-derived title instead of the session's label (resolve via
hello defaults + key equivalence, matching the pane), and the floating
sidebar-expand pill overlapped the header title on plain web while the nav
was collapsed (52px inset, mirroring the native-shell clearances).
* fix(ui): keep the protocol schema layer out of the Control UI startup bundle
The title-bar change made session-row-badges runtime-import the
gateway-protocol barrel for one classifier, dragging typebox and every
schema into startup JS (395 KiB gzip vs the 370 KiB budget). The placement
vocabulary now lives in a dependency-free session-placement-state module:
the schema union derives from it, the barrel re-exports it, and the UI
deep-imports it (startup back to 361.8 KiB).
* fix(gateway-protocol): keep SessionPlacementStateSchema statically typed
Type.Union over a mapped array loses tuple inference and collapses Static
to never; the literal list stays explicit with a compile-time guard tying
it to the shared SESSION_PLACEMENT_STATES vocabulary.
* chore(protocol): regenerate Swift/Kotlin bindings for sessions.files.reveal
* chore: narrow new exports flagged by the deadcode gate
* chore(i18n): leave generated locale artifacts to the locale-refresh workflow
The generated-artifact isolation gate (067635cb51) landed mid-flight;
source PRs now carry en.ts only.
* fix(agents): wake top-level requester when its last parallel child settles [AI]
A top-level session (normal chat/dashboard, not itself a subagent) that
spawns parallel subagents and waits via sessions_yield never receives the
"all descendants settled -> synthesize" wake: wakeSubagentRunAfterDescendants
only targets orchestrators with a subagent-registry run record. The parent
only sees passive per-child announces, commonly mis-tracks the outstanding
set on the final completion turn (or never hears results whose announce gave
up), and then parks until a human sends a message.
Fix: when a child reaches a terminal settle (announce delivered, give-up, or
delivery suspended) and its requester has no more descendants awaiting
settle, deliver a one-shot "all spawned subagents settled - synthesize and
deliver now" wake to the parked top-level requester through the existing
announce delivery pipeline (active requesters get it steered into the live
turn; parked ones get a new origin-routed turn).
- hasDescendantRunAwaitingSettle: early-exit drain check where a suspended final
delivery counts as settled (suspension is terminal for automatic retries).
- maybeWakeRequesterAfterAllChildrenSettled (subagent-announce.ts): scopes
the batch to the settling child's parallel wave (the connected component of overlapping run
lifetimes), skips nested/cron requesters (owned by the descendant-settle
wake), skips single delivered completions and fire-and-forget children,
and dedupes concurrent last-sibling settles via a batch-stable
announce idempotency key (requester-settle:<requester>:<sorted runIds>).
- Trigger fires from completeCleanupBookkeeping (all cleanup-terminal paths)
and suspendPendingFinalDelivery, and is skipped by the suspended-delivery
discard sweep, so the wake is outcome-independent: a child whose announce
gave up with NO_REPLY still counts toward - and can trigger - the drain.
Companion to the nested-orchestrator wake; covers the top-level case it
left open.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rukuut98qvDwzSwMp3u4kN
* fix(agents): retry the requester settle wake on transient turn failures [AI]
The settle wake is the only event that ever fires after a fan-out drains, so
a wake turn lost to a transient infra failure re-parked the requester
permanently. Observed live: the wake turn survived dispatch, reconciled all
investigators, and then died to a provider stream stall (LLM idle timeout)
mid-synthesis — with nothing left to retry it.
Bounded recovery: up to 3 attempts with 30s/120s backoff. Each retry uses a
fresh `:retry-N` idempotency suffix because the gateway dedupe caches
terminal run outcomes per key, so re-dispatching the same key would no-op. A
legitimately silent wake reply already classifies as delivered and never
retries; terminal failures and an abandoned requester stop immediately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rukuut98qvDwzSwMp3u4kN
* fix(agents): ledger retired delete-cleanup rows for the requester settle wake [AI]
cleanup="delete" retires a child's registry row in the same funnel pass
that schedules the settle wake, so a pure delete-mode fan-out could never
rebuild its drained wave from live rows: empty batch, no wake, requester
parked — the exact incident class this wake exists to close.
Retiring cleanup paths (delete branch, reconciled-killed tombstone) now
pass settledRowRetired, and the wake ledgers the in-hand record before
its first await: concurrent last-sibling settles all see the same
ledgered rows by the time any batch computation runs, keeping batch
membership and the idempotency key stable — the guarantee keep-mode gets
from rows persisting in the registry. Ledgered rows merge into the batch
candidates (a live registry row wins by runId); entries clear on
delivered/terminal wakes and the nested/requester-gone exits, with a 24h
TTL prune as the memory backstop. In-memory only: a restart degrades a
mid-wave delete fan-out to waking with the surviving rows.
* fix(agents): snapshot delete-mode child results before cleanup clears them [AI]
The delivered-finalize path clears completion.resultText (delete mode) and
the frozen delivery payload before cleanup bookkeeping runs, so the
requester-settle wake ledgered a row whose findings had already been
emptied: a cleanup="delete" fan-out woke its requester with "(no output)"
for successfully delivered children.
Capture an immutable snapshot of the entry (completion + delivery payload)
at the top of finalizeSubagentCleanup and in the resumed-announce give-up
path — before any clearing — and pass it through completeCleanupBookkeeping
to the settle wake for the retired-row branches. Keep-mode is unchanged:
its registry rows keep their result text and win over ledgered copies.
Regression: a lifecycle-driven test drives the real delivered delete-mode
cleanup (completeSubagentRun -> announce -> finalize -> funnel) and asserts
the wake's settledEntry still carries the child's result text after the
live entry was cleared and the row retired.
* fix(agents): release drained no-wake batches from the settle ledger [AI]
The zero-required and single-delivered guard exits decline to wake a wave
that has already fully drained. Ledgered delete-mode rows in such a batch
can never join a later wake (a child spawned after the drain cannot
lifetime-overlap rows that already ended, and any still-running overlap
would have failed the drain gate before these exits), so holding their
child-result snapshots for the 24h TTL was pure retention: high-volume
delete-mode fan-outs could pin large result texts in process memory.
Release the batch at both exits. No behavior change: waves with pending
overlapping children return earlier at the drain gate, which still keeps
their rows.
* test(agents): route e2e session fixture through the sqlite accessor [AI]
Upstream flipped session reads to the sqlite-backed session accessor
(#98236), which bypasses the loadSessionStore mock this e2e used for its
in-memory session fixture — requester entry lookups came back empty and
the settle wake exited at the usable-session guard. Serve loadSessionEntry
from the same fixture; everything else in the accessor stays real.
* refactor(agents): move the requester settle wake into its own module [AI]
Two pieces of fallout from rebasing across upstream tooling changes:
- The max-lines lint budget (#107315) has no grandfathered suppression for
subagent-announce.ts, and the settle wake + retired-row ledger pushed it
past the cap. The wake is a coherent unit, so it moves to
subagent-announce.requester-settle-wake.ts (matching its test file) with
its own registry-runtime seam; the registry's lazy dep and the lifecycle
type alias point at the new module. announce.ts drops back well under
the budget with no suppression needed.
- The announce read-path refactor rewired the runtime barrels onto
subagent-registry-announce-read.js, orphaning the
hasDescendantRunAwaitingSettle re-export on subagent-registry.ts, which
the deadcode-exports gate now flags. Removed; the retry-grace e2e reads
the announce-read implementation directly.
* fix(agents): admit the requester settle wake as tracked gateway root work [AI]
The settle wake was launched as a detached promise from cleanup
bookkeeping, so registry cleanup or shutdown could reach quiescence
before the wake admitted its gateway turn and the last-child completion
could still be lost during restart or teardown. Route the wake through
runWithGatewayIndependentRootWorkContinuation: a live cleanup parent
reserves the root synchronously, and restart drain now waits for the
in-flight wake. Adds a deterministic quiescence-race regression.
* fix(agents): persist requester settle recovery
Store requester-settle wake obligations on subagent run rows, replay them after restart, and retire cleanup rows only after a durable outcome. Persist admitted attempts and retry deadlines, coalesce live restores, drain disconnected waves, and let the sweeper recover stranded processors.
Co-authored-by: smthfoxy <263563487+smthfoxy@users.noreply.github.com>
* test(agents): allow durable settle persistence
Keep the bulk-cancellation regression focused on recovery after the injected persistence failure instead of assuming an exact write count; requester-settle bookkeeping now adds a legitimate durable write.
Co-authored-by: smthfoxy <263563487+smthfoxy@users.noreply.github.com>
* style(agents): declare settle wake scheduler
Use a hoisted function declaration for the lifecycle scheduler so lint accepts the intentional callback cycle without a mutable binding.
Co-authored-by: smthfoxy <263563487+smthfoxy@users.noreply.github.com>
* fix(agents): harden requester settle recovery
Co-authored-by: smthfoxy <263563487+smthfoxy@users.noreply.github.com>
* fix(agents): release settle wake during retry backoff
Co-authored-by: smthfoxy <263563487+smthfoxy@users.noreply.github.com>
* test(agents): type settle wake fixtures
Co-authored-by: smthfoxy <263563487+smthfoxy@users.noreply.github.com>
* style(agents): bind settle wake transition callbacks
Co-authored-by: smthfoxy <263563487+smthfoxy@users.noreply.github.com>
* test(agents): restore past-due settle wake
Co-authored-by: smthfoxy <263563487+smthfoxy@users.noreply.github.com>
---------
Co-authored-by: smthfoxy <263563487+smthfoxy@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(mattermost): add thread.requireExplicitMention to opt out of thread auto-follow
Mattermost treats any reply in a thread the bot has participated in as an
implicit mention, so requireMention only gates the first message and the bot
then answers follow-ups addressed to other people for the participation TTL.
Slack exposes channels.slack.thread.requireExplicitMention for exactly this;
Mattermost had no equivalent and its strict schema rejected the key.
Add channels.mattermost.thread.requireExplicitMention (channel + per-account),
mirroring Slack. When set, thread participation no longer counts as a mention.
Default (unset/false) keeps today's auto-follow behavior unchanged.
Related: #108269
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(channels): add implicit mention policy foundation
* feat(channels): unify implicit mention policy
Co-authored-by: leon <dodoma0919@gmail.com>
* refactor(channels): keep implicit policy inside evaluator
* fix(channels): use exported implicit mention type
* chore(channels): satisfy extension lint
* fix(config): break implicit mention type cycle
* fix(plugin-sdk): account for implicit mention config export
* refactor(config): isolate implicit mention schema
* chore(plugin-sdk): align implicit mention surface budget
* fix(config): remove unused schema re-export
---------
Co-authored-by: leon <dodoma0919@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(process): clarify command lane timeout cause
* fix(process): complete timeout cause formatting
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>