* feat(channels): add channel-owned setup contracts
* test(channels): align legacy setup fixtures
* chore(channels): regenerate config and SDK baselines after rebase
* fix(update): run fresh doctor after current-process core changes
* fix(channels): align add pre-scan with execution precedence
* style(cli): format channels-cli test additions
* fix(channels): restore option-before-positional channel resolution via metadata arity scan
* fix(channels): keep help flags out of metadata arity escalation
* test(update): mock fresh post-update doctor in current-process suites
* style: format review fixes and correct entrypoint mock type
* fix(channels): register only modern contract options for dual-publishing plugins
* test(update): align downgrade suites with fresh-doctor child invocation
* docs(channels): record empty-contract and input-forwarding invariants
* fix(line): keep the shipped --token switch as a channel access token alias
* fix(signal): stop treating exact cross-family loopback endpoints as bind-aligned
* chore(config): regenerate docs config baselines after second rebase
* style: format rebased channels add tests
* fix(channels): enforce field-key and flag-name agreement in setup contracts
* fix(signal): detect container endpoints for bare --http-url setup
* fix(signal): ignore unconfigured accounts in transport collision checks
* fix(channels): validate negated setup flags in contract and normalizer
* fix(signal): preserve existing transport kind when setup detection is unreachable
* style(signal): use direct boolean check in collision guard
* style(signal): type test config literals
* docs(update): record two-read design of fresh-doctor validation gate
* fix(channels): satisfy post-rebase architecture gates
* docs: refresh channel setup map
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* refactor(channels): trim deprecated setup-input fields and legacy promotion keys to published readers
* ci: retrigger after dropped ready event
* fix(channels): update setup-input guardrails and dead code for the trimmed tier
* feat(cron): stream schedule sources (supervised command stdout)
Add gated argv stream schedules with bounded line batching and trigger.streamBatch composition.
Reuse the gateway ProcessSupervisor for source ownership, deterministic teardown, capped restart backoff, and schedule-key guarded batch execution. Expose additive protocol, CLI, tool, UI, docs, and generated snapshot surfaces without storage DDL.
Contract: stream schedules are event-driven, require cron.triggers.enabled, reject command payloads, and retain at most one bounded pending batch.
* fix(cron): reject retired stream source epochs at run admission
Thread an invalidatable per-owner source-generation token (ownerNonce.generation)
through cron.run admission alongside the schedule key. A batch handed to cron.run
under one source epoch can wait behind another run while its owner is stopped; a
disable→re-enable or A→B→A edit leaves the schedule key unchanged, so the key
check alone would admit the retired epoch's batch. The token is persisted in
job.state on every lifecycle write and compared at every admission site plus the
executeJobCore guard, so a stale epoch's batch is skipped.
Also fix direct stream-job mutations recording the wrong lifecycle status when
global cron is off but triggers are on: extract resolveStreamStopReason so the
direct path reports the remediable cron-disabled state like reconcile does.
* fix(cron): close stream admission windows from round-10 review
- Persist the retired source generation before draining stop teardown, so a
batch queued behind another cron run cannot gain admission during the up-to-10s
in-flight-batch wait (server-cron routed stop path).
- Add streamSourceGeneration to the closed gateway response schema (excluded from
the writable patch schema) so a running stream job passes strict result
validation without letting callers spoof source identity.
- Close the mutation-epoch ABA: track an eviction epoch so a snapshotted absent-0
is trusted as unchanged only when no LRU eviction happened during the await.
* refactor(cron): stream sources own a durable logical identity
Split the conflated restart-generation/admission token into two concepts:
a persisted streamSourceIdentity owned by cron store mutations (rotates on
enable/disable, source replacement, once-trigger auto-disable, and explicit
retirement; stable across supervised child restarts) and a watcher-local
process generation used only to fence stale child callbacks. Admission now
requires schedule key + identity together at every window, closing the
A-to-B-to-A and restart-flush races from review rounds 8-11.
Also: match-mode regexes now see raw source text (the [truncated] marker is
applied after matching), stop-timeout failures set the live restartExhausted
mirror so shutdown preserves the terminal diagnostic, and the watcher is
split into owner/output/registry modules under the max-lines budget.
* fix(cron): harden stream teardown and intake from round-4 review
An exit queued ahead of a requested stop no longer counts toward restart
exhaustion (the synchronous stop fence owns it), overlapping cron.stop and
stopAndDrain share one memoized shutdown drain instead of double-stopping
every owner, raw output intake is bounded at 4x the batch cap so normal
64 KiB pipe reads stop losing complete lines to OS chunk boundaries, and
persisted-shape quarantine coverage for unsafe match expressions is pinned.
* fix(cron): keep watcher-internal owner disposal from retiring live identity
Disposing an obsolete owner while a start replaces it is not a durable
removal; a retiring stop there rotated the live job's identity and stranded
the replacement behind the CAS ownership guard. Also align the docs with the
implemented match semantics: complete lines match on full text past the
batch cap, only intake-cut prefixes are unmatchable.
* fix(cron): make oversized-line matching independent of pipe chunking
Partial lines are retained up to the raw-intake bound rather than the
delivery cap, so a complete over-cap line matches identically whether it
arrives in one callback or several; only a line the intake bound itself cut
remains an unprovable prefix. Reconcile also contains schedule-replacement
stop failures per job, matching the other stop branches.
* fix(cron): bound assembled lines, drop stale payload override, barrier stopAll
- enforce the 4x raw-intake per-line cap while assembling split callbacks,
so an oversized line stays an unprovable prefix regardless of chunking
- stop passing the watcher-cached payload as a cron.run override; the run
snapshots the persisted payload under its admission lock
- stopAll waits for every owner stop to settle before surfacing failures
- split cron-stream-output interleaving tests into their own file (max-lines)
* fix(cron): address first full-CI round (lint, knip, schema test, unused param)
* chore(cron): refresh codex prompt snapshots for stream schedule schema
* fix(cron): keep the first clean line after an intake drop ending at a newline
* fix(cron): fence stream reconcile list snapshots against direct mutation routes
A cron.list snapshot captured across the reconcile await could be applied
after a direct add/update route already started the owner, stopping it as
removed and retiring its live identity. A mutation revision bumped at every
direct route start invalidates the stale snapshot; reconcile re-lists
(bounded) instead of applying it.
* style(cron): format stream owner imports
* fix(cron): discard severed stream prefixes at EOF
* fix(cron): retry failed stream shutdown drains
* fix(cron): honor stream stop fence after output drain
* chore(cron): refresh landing checks
Preserve existing lower-level high effort while making xhigh/max available before runtime activation.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(plugins): persist binding approval before trusting it in memory
"Allow always" plugin conversation-binding approvals published the grant
into the in-memory approvals cache before the SQLite write that persists
it. If that write threw (SQLITE_BUSY, disk full, read-only state dir), the
cache kept a grant nothing had persisted, with no rollback: hasPersistentApproval
then auto-approved later same-scope binds without prompting, even though the
triggering approval rejected and the grant vanished on restart.
Persist first, then publish into the cache only after the write succeeds,
recomputing the merged set from the current cache so a concurrently-approved
grant is not dropped by a stale pre-await snapshot. On failure the cache is
left untouched and the user is re-prompted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(plugins): format conversation binding imports
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(gateway): session observer digests over the utility model
* fix(gateway): split session-observer modules and satisfy ci gates
* fix(gateway): observer reads session entries without materializing agent state
* test(plugins): cover manifest tool-availability gating
manifest-tool-availability.ts decides which plugin tools surface based on
manifest config/auth signals (gating in src/plugins/tools.ts and the
loaded-vs-manifest-only attribution in manifest-command-aliases.runtime.ts)
but had no test coverage anywhere in the tree. Characterize the shipped
contract: the unlisted-tool fail-open rule, config-signal overlay and
overlay-map merging, required/requiredAny value semantics incl. env secret
refs, mode allow/disallow gating, the provider base-URL guard with
trailing-slash normalization, setup-vs-providerAuthEnvVars precedence, and
the legacy authProviders/aliases shorthand.
* test(plugins): tighten manifest availability coverage
Co-authored-by: Krasimir Kralev <krasi@idrobots.com>
---------
Co-authored-by: KrasimirKralev <krasi@idrobots.com>
* fix(plugins): enhance plugin API compatibility diagnostic messages with troubleshooting guidance
Improve the diagnostic messages in shouldSkipIncompatiblePackagePluginApi to
include actionable troubleshooting hints when plugin discovery is skipped due
to version mismatches or invalid metadata.
Related to #93908
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(plugins): align compatibility diagnostics
Extend the discovery guidance to the manifest-registry load path and assert the exact remediation text.\n\nCo-authored-by: 杨爱文 <yang.aiwen@xydigit.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* refactor(ai): invert plugin coupling behind the transport host port
* fix(ai): queue custom transport registrations until the host is configured
* refactor(ai): remove relocated transport sources from src/agents
* fix(ai): source core stream types from canonical packages and fix tarball fixtures
* fix(ai): invert plugin transport host wiring
* fix(ai): harden managed transport projection
* test(ai): register synchronous stream in transport mock
* fix(ai): lazily install transport runtime host
* fix(ai): preserve completion compat detection
* fix(plugins): preserve UTF-16 boundaries in API key preview
* test(plugins): wrap map callback to avoid passing index as opts
* test(plugins): streamline API key preview cases
---------
Co-authored-by: chengzhichao-xydt <chengzhichao-xydt@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Channel plugins declare package env triggers in manifests, and ambient
environment variables count as "channel configured". A dev gateway
(gateway run --dev) inherits the operator's shell, so real channel
credentials silently configured channels and could connect development
instances to live services (observed with reef and a Telegram-range
connection during isolated stress testing).
Dev mode now drops presence signals whose only sources are env or
manifest-env across activation planning, auto-enable, autostart, health
recovery, readiness, reloads, and startup warnings. Explicit
channels.<id> config still works, --dev-ambient-channels restores the
old behavior, and startup logs the suppressed channel ids once. Non-dev
gateways are unchanged.
* feat(channels): batch 3 producers drop media placeholder bodies
Media-placeholder program batch 3: iMessage, Signal, and WhatsApp stop
minting <media:kind> placeholder bodies. Primary bodies carry captions
only with one aligned structured fact per native attachment (type-only
for missing/rejected/oversized). Echo, quote, and pending-history
records gain structured media facts so dedupe and previews work without
rendered text. Signal media-only outbound sends drop the placeholder
body entirely (signal-cli accepts attachment-only sends; recipients now
see just the media). WhatsApp audio preflight de-stringed; qa-lab
sticker recognizer moved to structured facts; WhatsApp gif maps to
video (presentation mode, no behavioral consumer).
* test(whatsapp): keep media classifier private
* fix(clickclack): split API base and refresh discussion provider
* chore: regenerate bundled channel config metadata and docs map for the apiBaseUrl key