diff --git a/.env.example b/.env.example index f3acb7ba..6d7dec43 100644 --- a/.env.example +++ b/.env.example @@ -225,11 +225,26 @@ MCP_DOCMOST_PASSWORD= # Silence timeout (ms) for EXTERNAL-MCP transport ONLY (not the chat provider). # Tighter than AI_STREAM_TIMEOUT_MS so a byte-silent/hung MCP server is broken in -# ~1 min instead of 15. Note it also cuts a legitimately long but byte-silent -# single tool call (a slow crawl that emits nothing until done) and an SSE -# transport idling >1 min BETWEEN tool calls. Default 60000 (1 min). +# ~1 min instead of 15. It cuts a legitimately long but byte-silent single tool +# call (a slow crawl that emits nothing until done) on the HTTP (streamable) +# transport, which opens a fresh request per call. The SSE transport — one +# long-lived body across many calls — is NO LONGER governed by this timeout +# (as of #489): its idle-BETWEEN-calls window has its own, raised bodyTimeout, +# AI_MCP_SSE_BODY_TIMEOUT_MS below. Default 60000 (1 min). # AI_MCP_STREAM_TIMEOUT_MS=60000 +# bodyTimeout (ms) for the EXTERNAL-MCP SSE transport ONLY (#489). The SSE +# transport holds ONE response body open across many tool calls, so undici's +# bodyTimeout (time between body bytes) counts the LEGITIMATE silence BETWEEN the +# model's tool calls, not just a hung single call. At the tight 1-min silence +# timeout above, a normal >1-min gap between calls would break the SSE socket and +# the cache would serve a dead client until TTL — so the SSE transport gets its +# OWN, RAISED bodyTimeout. A single stuck call is still bounded by the per-call +# cap (AI_MCP_CALL_TIMEOUT_MS), and a socket that does break is healed by the +# in-run transport-error retry. The HTTP (streamable) transport keeps the tight +# timeout. Default 600000 (10 min). +# AI_MCP_SSE_BODY_TIMEOUT_MS=600000 + # Total wall-clock cap (ms) for ONE external MCP tool call (app-level, not # transport). Aborts a tool that keeps the socket warm (SSE heartbeats / trickle) # but never returns a result — which the silence timeout above never breaks. diff --git a/CHANGELOG.md b/CHANGELOG.md index 30d27413..adecf01f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -336,6 +336,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **A chat with one malformed message part no longer 500s on every turn, and a + failed send no longer duplicates the user's message.** Incoming client parts + are now whitelisted to `text` (a forged tool-result part can no longer reach + the persisted history or the model context), and the turn is converted BEFORE + the user row is inserted, so a mid-flight failure cannot leave a duplicate + user row that a retry then compounds. A single part that still fails to convert + degrades to a `[tool context omitted]` marker on that one row instead of + bricking the whole chat. (#489) +- **A transport drop to an external MCP server now heals within the same turn.** + On an undici transport error, a read-only MCP tool reconnects its server and + retries once within the run; a write is never auto-retried (it may already have + applied). One flapping server no longer nulls the shared client cache, so other + servers' cached clients are untouched. The SSE transport also gets a raised + body-timeout so a legitimate >1-min idle between the model's tool calls no + longer breaks a long-lived SSE socket (new `AI_MCP_SSE_BODY_TIMEOUT_MS`, default + 10 min; see `.env.example`). (#489) + - **The server no longer runs out of heap during long autonomous agent runs.** A new pnpm patch on `ai@6.0.134` stops the SDK from building a cumulative snapshot of the ENTIRE turn text on every streamed text-delta when no output