You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
compaction: stays on "Compacting" with no feedback when the provider fails or only sends SSE keepalives #61346
A manual /compact sat on "Compacting" with an empty summary for over 90 minutes. There was no error and no retry notice. I had to call /api/session/:id/interrupt to get the session back. The provider was returning 503 no eligible upstream account is currently available.
I traced this to two causes on dev:
Compaction retries are silent. In packages/core/src/session/compaction.ts (deliver, around L619-L686), retryable provider errors go through SessionRunnerRetry.policy(...) and then Effect.sleep(decision.delay). Normal steps use SessionRunnerRetry.make(...).wait(...), which publishes SessionEvent.RetryScheduled. Compaction publishes nothing and logs nothing, so up to 10 retries look the same as a frozen stream.
SSE keepalives reset the 15-minute chunk timeout. In packages/ai/src/route/transport/http.ts (L119-L125), Stream.timeoutOrElse (DEFAULT_CHUNK_TIMEOUT_MS = 900_000) wraps the raw byte stream before framing.frame(...). sseFraming drops : keepalive comments later, but each one has already reset the timer. I checked this with a test next to packages/ai/test/http-timeout.test.ts: one data: event, then silence, fails with "Timed out waiting for response data". One data: event, then : keepalive comments, has still not finished after 20 virtual minutes.
#43618 fixed this for the old aisdk.ts path, and #43519 was closed. The V2 transport has the same problem. #54544 adds logging to the step retry path, but it does not cover compaction.
Possible fix: apply the chunk timeout after framing, so it times protocol frames instead of bytes, and publish retry status from compaction.
Thanks for the detailed report. I reproduced the keepalive part on v2.2.5 and on the latest dev (3ba597f).
The script below is a transport test in packages/ai. It sends one SSE data: event and then advances a test clock 20 virtual minutes:
Silence after the event: the request fails with "Timed out waiting for response data", as expected.
A : keepalive comment every minute: the request is still running after 20 minutes. The 15-minute chunk timeout in packages/ai/src/route/transport/http.ts is applied to the raw bytes before framing, so the comments keep resetting it.
I confirmed the silent retries by reading the code; I didn't run a 503 case. deliver in packages/core/src/session/compaction.ts waits with Effect.sleep between retries and never publishes SessionEvent.RetryScheduled, which normal steps publish through SessionRunnerRetry.make().wait.
Related: open PR #52866 moves the chunk timeout onto framed events, and #53747 reports steps hanging without a timeout.
Reproduction script (keepalive-timeout.test.ts)
// Reproduction for anomalyco/opencode#61346 (chunk timeout reset by SSE keepalive comments).// Copy into packages/ai/test/ of an opencode checkout and run:// cd packages/ai && bun test test/keepalive-timeout.test.tsimport{describe,expect}from"bun:test"import{Deferred,Effect,Fiber}from"effect"import*asTestClockfrom"effect/testing/TestClock"import{ai}from"../src/effect.js"import{LLMClient}from"../src/route.js"import{configure}from"../src/providers/openai.js"import{dynamicResponse}from"./lib/http.js"import{deltaChunk}from"./lib/openai-chunks.js"import{sseRaw}from"./lib/sse.js"import{it}from"./lib/effect.js"constmodel=configure({baseURL: "https://api.openai.test/v1/",apiKey: "test"}).chat("gpt-4.1-mini")constSSE={headers: {"content-type": "text/event-stream"}}constencoder=newTextEncoder()// Sends one `data:` event, then hands the controller to the test so it can push keepalive comments.constserver=Effect.gen(function*(){constready=yield*Deferred.make<ReadableStreamDefaultController<Uint8Array>>()constlayer=dynamicResponse((input)=>Effect.sync(()=>input.respond(newReadableStream<Uint8Array>({start(controller){controller.enqueue(encoder.encode(sseRaw(`data: ${JSON.stringify(deltaChunk({content: "Hi"}))}`)))Deferred.doneUnsafe(ready,Effect.succeed(controller))},}),SSE,),),)return{ layer, ready }})construn=(keepalive: boolean)=>Effect.gen(function*(){consts=yield*serverconstfiber=yield*LLMClient.generate(ai.request({ model,prompt: "Hello"})).pipe(Effect.provide(s.layer),Effect.flip,Effect.forkChild({startImmediately: true}),)constcontroller=yield*Deferred.await(s.ready)// 20 virtual minutes, one keepalive comment per minute (or silence).for(leti=0;i<20;i++){if(keepalive)controller.enqueue(encoder.encode(": keepalive\n\n"))yield*Effect.promise(()=>newPromise((r)=>setTimeout(r,5)))yield*TestClock.adjust("1 minute")yield*Effect.yieldNow}returnfiber.pollUnsafe()})describe("#61346 chunk timeout vs SSE keepalives",()=>{it.effect("control: data then silence times out within 20 minutes",()=>Effect.gen(function*(){constexit=yield*run(false)console.log("silence ->",exit ? JSON.stringify(exit) : "still running")expect(exit).toBeDefined()}),)it.effect("data then only keepalives should also time out within 20 minutes",()=>Effect.gen(function*(){constexit=yield*run(true)console.log("keepalive ->",exit ? JSON.stringify(exit) : "still running after 20 virtual minutes")expect(exit).toBeDefined()}),)})
I found the earlier closed PR #50757 covering the same compaction retry notice behavior. To avoid duplicating that work, I'm withdrawing this claim and will not submit another PR for this scope.
I can reproduce the keepalive part on dev (3ba597f): the chunk timeout wraps the raw byte stream before SSE framing, so : keepalive comments keep resetting it. I'd like to take that part (transport only, not the compaction retry notice) with a test in http-timeout.test.ts.
Activity
opencodejr commentedon Nov 3, 2026
Thanks for the detailed report. I reproduced the keepalive part on v2.2.5 and on the latest
dev(3ba597f).The script below is a transport test in
packages/ai. It sends one SSEdata:event and then advances a test clock 20 virtual minutes:: keepalivecomment every minute: the request is still running after 20 minutes. The 15-minute chunk timeout inpackages/ai/src/route/transport/http.tsis applied to the raw bytes before framing, so the comments keep resetting it.I confirmed the silent retries by reading the code; I didn't run a 503 case.
deliverinpackages/core/src/session/compaction.tswaits withEffect.sleepbetween retries and never publishesSessionEvent.RetryScheduled, which normal steps publish throughSessionRunnerRetry.make().wait.Related: open PR #52866 moves the chunk timeout onto framed events, and #53747 reports steps hanging without a timeout.
Reproduction script (keepalive-timeout.test.ts)
Run it:
bun keepalive-timeout.test.tsdeosiree commentedon Nov 3, 2026
I found the earlier closed PR #50757 covering the same compaction retry notice behavior. To avoid duplicating that work, I'm withdrawing this claim and will not submit another PR for this scope.
rome-xi commentedon Nov 3, 2026
I can reproduce the keepalive part on dev (3ba597f): the chunk timeout wraps the raw byte stream before SSE framing, so
: keepalivecomments keep resetting it. I'd like to take that part (transport only, not the compaction retry notice) with a test inhttp-timeout.test.ts.