OpenAI-compatible stream idle timer ignores SSE keepalive chunks → false 'no data' timeouts + prefill retry livelock
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 19.9k
- Forks
- 2.3k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 30
Description
OpenAI-compatible stream idle timer ignores SSE keepalive chunks → false "no data" timeouts + prefill retry livelock
Environment
- jcode v0.76.0 (bbef1f6a8), macOS aarch64
- Provider:
openai-compatible→ local oMLX server (OpenAI-compatible chat completions, SSE streaming) - Local model: 27B hybrid (prefill ~110–160 tok/s on an M4 Max), so large prompts legitimately spend minutes in prefill before the first content token
Summary
When streaming from a server with a long prefill, jcode aborts the stream with OpenRouter SSE stream timed out (no data for 600s) even though the server emits SSE keepalive chunks every ~9 seconds throughout the prefill. The idle timer appears to reset only on content-bearing deltas; keepalive chunks ("model":"keepalive", "created":0, empty delta.content) do not count as activity.
Worse, the "no data" deadline appears to be inherited across retry attempts rather than reset when a new stream opens.
Evidence (jcode log, 2026-08-15 local time)
stream_idle_timeout_secs = 600in config (confirmed live).- Attempt 1: stream opened
21:51:31, timed out21:55:16(only 225 s later) reportingno data for 600s— 600 s before was21:45:16, the open time of a previous request's stream. - Attempt 2: opened
21:55:17, timed out22:01:31— exactly 600 s counted from attempt 1's open time. - Same pattern for attempts 3–8 (
22:05:17,22:15:20, …): each deadline = previous attempt's start + 600 s. - Server-side (oMLX) logs confirm keepalives were flowing and the prefill was progressing normally during every one of these "no data" windows.
Consequence: retry livelock
The server cannot resume an aborted prefill, so every client-side abort restarts the full prefill from token 0. With the inherited deadline, each attempt dies before prefill completes, and all 8/8 attempts fail. Real measurements from one session:
- 14,903-token prompt (~2–3 min prefill): answered only after 68 minutes and 8 attempts — and one attempt had already generated 33 chars of reply when the client timer rolled it back (
Mid-stream retry rollback (attempt 7/8): discarding partial output (33 text chars, 0 tool calls)). - 82,089-token prompt (~12–15 min prefill): never completed; 8/8 attempts timed out.
A related second issue: after switching the session's route away from the local model mid-turn, the old route's retry loop kept running in the background for ~1 hour (attempts at 21:55, 22:05, 22:15, 22:25, 22:35… after the route switch at 21:49), hammering the local server with 82k-token prefills. A turn cancel / route switch should cancel the retry loop.
Repro
- Point
openai-compatibleat any server whose prefill exceeds the idle timeout (a stub that emits SSE keepalives then sleeps N minutes before the first content token works). - Send a prompt. Watch attempts die at the inherited deadline despite keepalives arriving every few seconds.
Suggested fixes
- Count any received SSE frame — including
model:"keepalive"chunks, comment frames, and empty deltas — as stream activity for idle-timeout purposes. - Reset the idle/no-data deadline per attempt when the new stream opens.
- Cancel a turn's provider retry loop when the turn is cancelled or the session route is switched.
Happy to provide full jcode + server logs privately if useful.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by tracing the openai-compatible SSE streaming path, its idle-timeout bookkeeping, and the provider retry loop. Reproduce the issue with a stub that emits keepalives before content, then verify that every SSE frame resets activity, each attempt gets a fresh deadline, and cancellation or route switching stops retries.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- api, cli
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100