* fix(vault): keep request deadline through response body
* test(vault): use generic stalled-body fixture
* test(vault): mark fixture token synthetic
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* improve(gateway): restart only the changed account on channel config reload
When a config change is scoped entirely to one channel account
(channels.{kind}.accounts.{accountId}[.*]), restart just that account
instead of the whole channel. Wholesale channel restarts disconnect every
account on the channel; on gateways running many accounts, each
account-scoped config write (adding an account, changing one account's
settings) briefly dropped every other account's connection.
The reload plan gains a restartChannelAccounts bucket populated only when
all of a channel's changed paths are account-scoped; any channel-global
path falls back to the existing wholesale restart, and a channel scheduled
for wholesale restart drops its per-account entries so each (channel,
account) pair restarts at most once. The executor runs per-account
restarts through the existing stopChannel/startChannel accountId parameter.
* fix(gateway): preserve account reload admission
* fix(gateway): scope surgical reloads to isolated plugins
* test(gateway): avoid preactivating reload snapshot
* docs(channels): define account reload isolation contract
* fix(gateway): preflight scoped account reloads
* fix(gateway): re-drain live scoped reload targets
* docs: refresh generated docs map
* fix(mattermost): align scoped reload with durable ingress
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(agents): use fetchWithSsrFGuard in minimax-vlm.ts instead of raw fetch
Replace raw fetch() with fetchWithSsrFGuard + withTrustedEnvProxyGuardedFetchMode
in minimaxUnderstandImage, aligning with all other agent network transport
paths (provider-transport-fetch, mcp-http-fetch, web-guarded-fetch, tools-manager).
This adds SSRF protection, redirect safety, debug capture, and proper
dispatcher lifecycle management. The ensureGlobalUndiciEnvProxyDispatcher
call is removed since guarded fetch handles proxy routing in trusted env-proxy
mode.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(agents): preserve configured MiniMax origins in guarded-fetch migration
* fix(agents): resolve lint and type-suppression regressions in MiniMax VLM guarded-fetch tests
Replace as-any casts in vi.mock vi.importActual calls with generic type
parameters, and replace the opts:any parameter in the fetch-guard bridge
with an inline type. Add SSRF policy assertions covering default hosts,
custom public hosts, and explicitly-authorized loopback origins.
* fix(minimax-vlm): thread allowPrivateNetwork into guarded-fetch policy
Honor the explicit provider request private-network opt-out in the MiniMax
VLM guarded-fetch path. When an operator sets
models.providers.<id>.request.allowPrivateNetwork: false, skip the origin
exception for non-default hosts so the explicit denial is authoritative.
Also fix an invalid Vitest fn generic (two-parameter form -> function
signature) that caused check-test-types to fail.
* fix(agents): use canonical provider SSRF policy for MiniMax VLM
* fix(media): harden MiniMax VLM transport
Co-authored-by: Peter Lee <li.xialong@xydigit.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
* improve(diagnostics): surface run startup phases as session-correlated events
The embedded runner reports ordered startup milestones (workspace, auth,
context_engine, ..., model_call_started) through the private
onExecutionPhase callback, so they reach typing indicators and cron
watchdogs but never the diagnostic bus. External status surfaces that
already consume run.attempt / run.progress / model.call.* cannot see
where a turn is during startup — exactly the window that matters when a
run hangs before its first model call.
Wrap params.onExecutionPhase once at the runner entry so every phase
transition also emits a session-correlated run.execution_phase event
(runId, sessionId, sessionKey, closed EmbeddedAgentExecutionPhase +
provider/model/tool context). The event rides the async diagnostic lane
like the other high-frequency lifecycle events, so ordering against
model.call.* is preserved and no subscriber work runs on the hot path.
The wrapper tracks session rotation via onSessionIdChanged so
post-compaction phases carry the current session id. Stability records
project the milestone into their dedicated phase/provider/model fields.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(diagnostics): preserve trusted phase observability
* chore(plugin-sdk): refresh diagnostic API baseline
* fix(diagnostics): publish execution phases to plugins
* refactor(logging): keep public diagnostic recorder path
* fix(plugin-sdk): keep deprecated budget exact
* fix(plugin-sdk): refresh diagnostic surface budget
* fix(diagnostics): preserve execution phase coverage
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(elevenlabs): fall back to default base URL when config value is malformed
* fix(elevenlabs): reject malformed and non-http(s) base URL overrides
* fix(elevenlabs): redact configured URL from base URL validation errors
* fix(elevenlabs): preserve realtime WebSocket overrides
* test(elevenlabs): avoid stale realtime test overlap
* style(elevenlabs): format realtime URL assertion
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(mattermost): adopt durable ingress drain at the websocket chokepoint
Posted events processed detached from the websocket receive with only a
5-minute in-memory guard; a crash lost the post and reconnect never replays
it. Raw posted envelopes now journal durably (event_id = post.id per the
upstream Post model, lane per channel_id, one row per post) before handler
scheduling; dispatch runs through the core drain with deferred claims through
debounce, merged-flush fan-out adoption, gated-turn settlement, and 30d/20k
tombstones covering the old 5min/2k guard, which is deleted after its parity
test. post_edited stays excluded and cannot be swallowed by posted
tombstones. Cold-gap limitation stated: Mattermost cannot replay posts missed
while disconnected.
Autoreview blocked by codex sandbox network in the build stage; full manual
review performed (updated one websocket test asserting the pre-adoption
parsed-post contract to the raw-envelope contract). Part of #109657 wave 2.
* style(mattermost): keep ingress monitor type internal
* fix(mattermost): retry then loudly escalate a failed durable append
Landing autoreview caught a real loss path: a durable enqueue failure at the
websocket chokepoint was logged and swallowed — the raw envelope discarded,
the connection kept running against a broken store, and reconnect never
replays, so a transient SQLite failure silently lost the post. The append now
retries with short backoff for transient blips; a persistent failure
propagates and the websocket terminates loudly so the outage is
operator-visible instead of silently dropping every subsequent post.
Regression test covers both the absorbed-transient and escalation paths.
* style(mattermost): format rebased ingress handler
* docs(mattermost): document bounded auth-failure retries under deferred claims
* fix(mattermost): guard the drain pump against stop racing the async prune
stop() disposing before the startup pump finished pruning let the pump
lazily create a fresh undisposed drain and dispatch after shutdown. The pump
now re-checks running after the prune, and stop() disposes again after
awaiting the pump so a drain created mid-race is torn down. Regression test
blocks the prune across stop and asserts no dispatch.
* fix(mattermost): serialize durable admissions to preserve lane order
Concurrent websocket callbacks let a post in append-retry backoff be
overtaken by its successor, inverting same-channel arrival order in the
queue. Admissions now chain (order over latency, mirroring the iMessage
admission tail); regression proves a retried post still lands ahead of a
concurrently received one.
* test(mattermost): assert lane order via dispatch sequence
* fix(mattermost): stop() awaits in-flight admissions before disposal
* fix(mattermost): satisfy ingress lint checks
* fix(mattermost): honor envelope-level channel ids in the durable inspector
Posts can carry their channel id on the post, the event data, or the
broadcast envelope — the monitor dispatch honors all three, but the ingress
inspector and claim-side validator required the nested field, rejecting valid
posts as permanent and (via the storage-failure escalation) tearing down the
socket for a failure that never happened, losing posts reconnect cannot
replay. Both sites accept the three shapes; regression proves an
envelope-level post dispatches.
* fix(anthropic): add claude-fable-5 to CLI allowlist, aliases, labels, and context window
CLI path was missing claude-fable-5 across all four metadata layers while
the direct Anthropic provider path in register.runtime.ts already had full
support via isAnthropicFable5Model() / resolveAnthropicFixedContextWindow().
- CLAUDE_CLI_DEFAULT_ALLOWLIST_REFS: add claude-cli/claude-fable-5
- CLAUDE_CLI_MODEL_ALIASES: add fable/fable-5/claude-fable-5 mappings
- CLAUDE_CLI_CONTEXT_WINDOWS: add claude-fable-5 -> 1_000_000 (was falling back to 200K)
- CLAUDE_CLI_MODEL_LABELS: add Claude Fable 5 (Claude CLI)
- resolveClaudeCliImageMediaInput: add fable-5 to 2576 max-side tier
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(anthropic): route fable alias through context/model-ref canonicalization paths
* fix(agent): include claude-cli in fable-5/mythos-5 fixed context window resolution
* fix(anthropic): add claude-fable-5 to static CLI manifest catalog
* test(anthropic): cover fable-5 alias canonicalization in bare and provider-qualified forms
* style(anthropic): fix indentation in plugin manifest
* fix(anthropic): publish Fable CLI output limit
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* refactor(whatsapp): replay durable inbound through the shared drain
Accept-before-processing stays; the private startup replay loop and inline
complete/release bookkeeping move to the core drain. Drain-owned entries keep
their durable id so downstream failures release for replay, dedupe-claim
contention stays retryable instead of tombstoning, and undeliverable claims
still send configured read receipts. Rows tombstone at dispatch return
(pre-migration timing), not at adoption (#108656).
* fix(whatsapp): finish durable ingress drain adoption with serialized conversation lanes
Core durable ingress intentionally serializes claims by remote JID. Same-conversation bursts now merge as queued followups in the core reply lane after adoption; channel debouncing remains for control and retry paths, with merged fan-out and gated settlement.
Built on the durable drain branch base by @obviyus.
* fix(whatsapp): fail closed on persistence failure and guard prepared-inbound races
Landing autoreview caught two holes: the persistence-failure fallback
dispatched live, bypassing drain dedupe and lane serialization now that the
replay guard is deleted (duplicate replies / session races); and duplicate
pending rows could orphan or clobber preparedInboundByDurableId entries. The
append now retries transient failures with short backoff then drops loudly
(fail closed — the fallback traded a correctness race for availability
against an already-broken store); redeliveries neither clobber the first
delivery's in-flight preparation nor keep orphan entries. The old
fallback-contract test is repurposed to prove retry-then-durable-delivery.
* fix(whatsapp): dispose the durable drain when the close drain times out
The close timeout abandoned the graceful drain path before its finally could
dispose, so a successor monitor could pump the same account queue against a
still-live drain. The timeout wrapper now owns disposal; dispose is
idempotent so the graceful path's cleanup is unaffected.
* style(whatsapp): keep ingress internals unexported and drop a dead harness helper
* refactor(whatsapp): move durable payload serialization to its own module
The dead-export gate refused serializer/error exports whose only external
consumers were tests. The payload contract (Long-timestamp-preserving
serialize/deserialize + the permanent-error class) now lives in
durable-payload.ts with durable-receive as its production consumer; the dead
WhatsAppRetryableInboundError class is deleted (any non-permanent error is
retryable to the drain classifier, so test-support throws a plain Error).
* fix(whatsapp): guard prepared-map ownership and bound its growth
A duplicate pending delivery's finishPreparation deleted the first
delivery's kept entry (ownership guard now requires the deleter to be the
installer), and queue pruning could evict pending rows whose prepared-map
entries then lingered forever on a blocked lane (map now evicts oldest-first
at a bound above the queue's pending cap; dispatch already re-normalizes the
journaled payload when its entry is gone). Regression covers the duplicate
case.
* style(whatsapp): prune split leftovers and satisfy consistent-return lint
* style(whatsapp): satisfy type-aware lint across the drain monitor
Closed verdict shape for the retry result (queue enqueue's metadata generic
leaks any into the union), Promise.resolve-wrapped abandonment aggregators,
braced timeout executor, and the drain merged to a single const (lazy
closures above only run post-start, so const-after-use is TDZ-safe).
---------
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* fix(imessage): adopt durable ingress drain with cursor advanced after append
The chat.db watcher advanced its ROWID cursor after yielding rows to an
in-memory pipeline; a crash between read and dispatch skipped those messages
forever. Raw rows now journal durably (event_id = message GUID, lane per
chat) at the monitor chokepoint, and the cursor advances only after the
durable append — mirroring Telegram's offset-after-spool contract. Dispatch
runs through the core drain with deferred claims, merged fan-out adoption,
and gated settlement; legacy catchup enters the same raw path. The
transitional GUID replay guard is deleted per the layering contract with
4h/10k tombstone parity; the permanent age fence and recovery caps are
untouched. Twin verdict: tapbacks are distinct rows with their own
ROWID/GUID; fresh-GUID stale backlog remains age-fence territory (upstream
imsg source cited in PR).
Autoreview blocked by codex sandbox network in the build stage; full manual
review performed. Part of the #109657 fleet adoption program (wave 2).
* fix(imessage): exempt operator-requested catchup rows from the live age fence
Landing review caught real message loss: legacy catchup accepts rows up to
its configured maxAge (120min default), but the unified durable path ran them
through the 15-minute live Push-flush fence — suppressing AND tombstoning
any catchup row older than 15 minutes, permanently losing history the
operator explicitly asked to replay. Catchup rows now carry provenance
through the journal payload and skip the live fence; the catchup query's own
maxAge window is their age gate. Regression test proves provenance survives
the durable round trip.
* test(imessage): resolve ingress rebase mocks
* docs(imessage): document the fence-suppression vs catchup overlap tradeoff
* fix(imessage): satisfy durable ingress lint gates
* fix(msteams): adopt durable ingress drain with ack gated on activity append
Bot Framework webhooks acked before detached processing with no platform
retry, silently losing inbound activities on a crash. Dispatchable activities
(message + adaptiveCard/action invokes) now persist their raw JSON durably
(event_id = activity.id per the @microsoft/teams.api uniqueness contract,
lane = conversation.id) before the webhook 200; dispatch runs through the
core drain with deferred claims through debounce, merged-flush fan-out
adoption, gated-turn settlement, retry/dead-letter classification, and
30d/20k tombstones. Restart replay reconstructs proactive routing context.
No inbound replay guard existed; the outbound echo cache is untouched. Twin
check: messageUpdate/votes/consent/reactions run dedicated non-agent
workflows and are excluded from the journal.
Autoreview blocked by codex sandbox network in the build stage; full manual
review performed. Part of the #109657 fleet adoption program (wave 2).
* fix(msteams): close live-context races and double next() in drain dispatch
Three defects found in landing review: the live turn context could swap a
duplicate delivery's mutated activity in place of the journaled payload
(dispatch now always uses the journaled activity, live context is transport
surface only); the context registry installed entries after the durable
append, leaking them when the drain consumed the claim first (install now
precedes enqueue, tombstoned duplicates clean up, first delivery's context
wins); and a failed next() in the message handler was invoked a second time
from the catch fall-through (ran-flag guards it).
* fix(msteams): uninstall a failed append's live context before retry
Follow-up to the install-before-enqueue race fix: an enqueue rejection
bypassed cleanup, so a retry with the same activity id dispatched the failed
request's stale context. Uninstall is identity-guarded (only our context is
removed, never a concurrent redelivery's fresh install) and covers both the
throw and tombstoned-duplicate paths. Regression proves the retry's own
context dispatches.
* test(msteams): track the buffered dispatcher seam after main retired the settled variant
* test(msteams): resolve dispatch union before catch for promise lint
* test(msteams): explicit promise-returning mock for the promise-misuse lint
* test(msteams): type the ingress accept mock promise-returning
ReturnType<typeof vi.fn> types the implementation callback void-returning, so
every promise-returning accept mock tripped typescript(no-misused-promises)
in CI regardless of call-site shape.
* test(msteams): restore compact async mock now that its type permits promises
* test(msteams): extract gated-accept helper to stay under max-lines