Repository navigation
Conversation
`onStreamComplete` runs from the SSE transform's flush, and the flush only runs on a clean upstream EOF. A client disconnect cancels the pipeline instead, so the usage row, the request detail and the "done" line were all skipped — silently, even though the provider had already generated and billed the partial response. Agentic CLI clients cancel aggressively, so Recent Requests can look frozen while traffic flows. `createSSEStream` now exposes `getStreamSnapshot()`, which reads the content, thinking and usage accumulated so far and resolves usage the same way the flush paths do — including the estimate-from-content fallback for an upstream that had not sent usage yet. `handleStreamingResponse` wraps the stream controller so a disconnect or a mid-stream upstream error records that snapshot as an "aborted" detail under the same detail id, plus the usage row. The recording fires at most once per stream and a normal completion claims the slot, so the flush path and the abort path can never both write. Addresses bug 1 of decolua#3488
ntdat812
force-pushed
the
fix/aborted-stream-usage-3488
branch
from
September 7, 2026 08:58
6bacd8b to
bcc2567
Compare
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.
Addresses bug 1 of #3488 (aborted streaming requests are never recorded).
Bug 2 is not touched — see the note at the end.
Root cause
buildOnStreamComplete's callback is invoked from the SSE transform'sflush,and
flushonly runs on a clean upstream EOF. When the client goes away thepipeline is cancelled (
createDisconnectAwareStream.cancel→handleDisconnect→
AbortController.abort()), soflushnever runs and all three side effects —saveRequestDetail,saveUsageStats, the📊 doneline — are skipped. The samehole swallows a mid-stream upstream failure, which errors the pipeline instead.
The provider has already generated (and billed) the partial response, so usage
accounting silently under-counts, and Recent Requests looks frozen for clients
that cancel often.
Change
createSSEStreamexposesgetStreamSnapshot()— the content, thinking andusage accumulated so far. Usage is resolved exactly as the two flush paths
resolve it, including the
estimateUsage(...)fallback when the upstream hadnot sent a usage chunk yet, so an aborted stream is accounted for the same way
a completed one is instead of being dropped.
handleStreamingResponsewraps the stream controller (withAbortRecording)so a disconnect or a mid-stream error records that snapshot through a new
onStreamAborted: an"aborted"request detail carrying the reason, writtenunder the same
streamDetailIdso the dashboard row is updated rather thanduplicated, plus the usage row.
The recording fires at most once per stream and a normal completion claims the
slot first, so the flush path and the abort path can never both write. When
onStreamAbortedis absent the controller is passed through untouched, sonothing changes for callers that do not opt in.
Tests
tests/unit/aborted-stream-usage-3488.test.js(new, 3 cases) drives the realhandleStreamingResponsewith a fake upstream that stays open, reads one chunkon the client side and then cancels the reader — which is what
DISCONNECT: ResponseAbortedis:completion_tokens;under the same id, with statuses
["success", "aborted"];[DONE]normally records no abort.Mutation check — bypassing only the wrapper (
recordingController = streamController)fails the first two and leaves the third green.
Suite comparison (
cd tests && npx vitest run unit/), same machine:npx eslintclean on all changed files.tests/unit/responses-abort-terminal.test.js(the other disconnect-path test) still passes.
Note on bug 2 (skipped
usageHistoryids)Not addressed here, and the suggested fix looks to be already in place:
PRAGMA busy_timeout = 5000is part ofPRAGMA_SQLinsrc/lib/db/schema.js,which all four adapters (
better-sqlite3,bun:sqlite,node:sqlite,sql.js)execon open. If the gap ids persist under that timeout, a retry wrapper aroundthe
saveRequestUsagetransaction would be the next step — worth its own PR.