Repository navigation
fix(#437): make upstream timeout idle-based so long healthy streams aren't truncated - #439
Merged
Merged
Conversation
…ren't truncated fetchWithTimeout used a single TOTAL timer (UPSTREAM_TIMEOUT_MS = 10min) that only cleared once the body was fully consumed. A healthy stream that legitimately runs longer than 10 minutes (e.g. a DeepSeek generation via bili dsh) was aborted at the 10-minute mark mid-stream: the for-await loop threw, emitStreamError wrote 'stream error' + finish, and the client saw the conversation truncated. Re-arm the timer on every response-body chunk (via an idle-wrapped body), so the timeout is IDLE-based during streaming: healthy long streams survive, while a genuinely stuck stream (no chunk for timeoutMs) still trips the abort. Time-to-first-byte bounding is unchanged. clearTimer/externalSignal semantics are preserved and callers see an identical Response shape (status/headers/body).
Closed
Contributor
📦 Built Package ArtifactBranch: Option A — Install from npm PR tag (recommended)npm install -g billion-context@pr-439Each push to this PR publishes a new version under the Option B — Download artifact
tar xzf billion-context-pr439.tgz
npm install -g packageThis comment is automatically updated on each push. |
Owner
Author
[bot] 🏷 Reviewed Verification (ran locally on the PR branch)
Code review
Minor (non-blocking) notes
No new issues to file. Ready to merge once you've had a chance to run the E2E core (per the AGENTS.md rule to run it before merging request-pipeline changes) — or merge now if you consider the full unit suite + regression test sufficient coverage for this isolated timeout-semantics change. |
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Issue #437: with
bili dsh(DeepSeek), a long streaming generation got cut off ~10 minutes in ("流式输出十分钟后对话被截断").Root cause:
fetchWithTimeout(src/fetch-util.ts) armed a single totalsetTimeout(abort, UPSTREAM_TIMEOUT_MS)(10 min) that was only cleared once the response body was fully consumed. A healthy stream that legitimately runs longer than 10 minutes was aborted at the 10-minute mark mid-stream — thefor awaitloop threw,emitStreamErrorwrote❌ [ACP] stream error: …+ a finish event, and the client saw the conversation truncated.Fix
Make the timeout idle-based during the body phase: the timer is re-armed on every response-body chunk (via a backpressure-preserving
armIdleBodybody wrapper). So:timeoutMs) still trips the abort;clearTimer/externalSignalsemantics are preserved, and callers see an identicalResponseshape (status/headers/body).Centralized in
fetchWithTimeout, so it covers every upstream path (mainforward(), preflight, compress-loop re-requests, registry).Tests
tests/fix-437-idle-timeout.test.ts:src/fetch-util.ts);npm run typecheck— cleannpm test— 899/899 passnpm run build— cleanE2E note
tests/e2e/e2e-codex.test.tsrequires a livecodexbinary + a local Responses upstream, which aren't available in this environment (spawn codex ENOENT). The change is an isolated timeout-semantics change infetchWithTimeout(no message-rebuild/rewrite logic touched) and is covered by the full unit suite above. Please run the E2E core before merging if you have the upstream available.