* 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>
* fix(tts): wrap tts.status provider enumeration with configuredOrFalse
`tts.status` enumerates provider states by calling
`getResolvedSpeechProviderConfig` then `isConfigured` for each
provider. If a provider's config resolution throws (e.g. Gradium
with an invalid baseUrl), the entire tts.status call fails.
Wrap the provider-state closure with `configuredOrFalse` (already
used by `talk.ts`) so a misconfigured provider is reported as
`configured: false` instead of crashing the enumeration.
Addresses review feedback on #108569.
* fix(tts): isolate provider configuration probes
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
A transiently-degraded per-turn catalog (discovery failure or static/empty
fallback) made a valid pinned model override look disallowed, so the reset
path destructively unpinned the user's model and showed a misleading "not
allowed; add it to agents.defaults.models" notice (#97517).
Thread authoritative-vs-degraded provenance through ModelCatalogSnapshot.
When allowsKey() rejects a pin only because the catalog is degraded, keep the
pin, fall back to the primary for this turn, and emit a non-destructive
"temporarily unavailable" notice. Genuine disallow and stale overrides keep
today's destructive reset (both are catalog-independent, reliable signals).
* 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>
readPipedStdin accumulated piped stdin without a size limit,
letting `models auth paste-token` and `paste-api-key` buffer
arbitrarily large input before any validation. Add a 1 MiB cap
matching the existing Matrix recovery-key and config-patch
stdin limits, and reject oversized input at the stream boundary.
* 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
* perf(sqlite): gate schema version before integrity scans and quarantine terminal open failures
* feat(gateway): refuse incompatible database schemas at boot and verify integrity in the background
* feat(update): refuse installing builds that cannot open the current databases
* docs: add database schema reference with version history and downgrade guidance
* test(update): tolerate optional runner options in git-mutation mocks
* refactor(update): split npm package-target metadata out of update-check
* chore: model verifier internals for knip and regenerate docs map
* chore(i18n): resync stale iOS localization catalog
* chore: allowlist read-only preflight and verifier raw SQLite access
* 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
Respawn one-shot macOS commands with Node's OpenSSL-backed CA store when system CA loading is inherited. This avoids an indefinite shutdown join on the Keychain loader worker while preserving system CA for Gateway, interactive, and explicit --use-system-ca processes.
* feat(clients): copy visible markdown, full-message reader for truncated messages, and independent reasoning/tool toggles
* chore(i18n): translate new reader and trace strings and regenerate catalogs
* test(ios): adopt reader display-options API in render smoke tests