Skip to content

fix: send SSE comment keepalives so slow streams are not idle-killed - #188

Merged
solderzzc merged 2 commits into
SharpAI:mainfrom
CodeAndCanvas728:pr/sse-keepalive
Sep 25, 2026
Merged

solderzzc merged 2 commits into
SharpAI:mainfrom
CodeAndCanvas728:pr/sse-keepalive

Conversation

@CodeAndCanvas728

Copy link
Copy Markdown
Contributor

Summary

Streaming responses now send an SSE comment (: keepalive) every 5 s, so clients don't kill slow streams as idle.

On slow hardware (a 35B-A3B MoE with SSD expert streaming on a 16 GB Mac, ~4–5 tok/s) a streaming response can carry no bytes for minutes:

  • Long prefill. Several minutes at 15–30k tokens. The prefill-progress heartbeat is opt-in, via X-SwiftLM-Prefill-Progress.
  • Buffered tool calls. ToolCallProcessor holds tool-call text until the call parses, so a 4k-token write call streams nothing for 15+ minutes.

Node/Bun fetch aborts a response body that has been idle for about 300 s (TypeError: terminated). The agent then retries and pays the whole prefill again. Observed with pi/little-coder: a run died on its first large file write, and every retry then timed out behind the abandoned generation. #170's early : connected preamble fixes the silence before headers are sent; this PR covers the silence after that.

Change

  • startSSEKeepalive(_:interval:) yields ": keepalive\r\n\r\n" every 5 s until the stream terminates or the task is cancelled.
  • It is used by both the chat and text completion streaming paths, and cancelled in their existing defer cleanup.
  • SSE parsers ignore comment lines, and every existing yield is a complete event, so a comment can never land inside another event.

Tests

  • ServerSSETests: comments are emitted while the stream is silent, and the keepalive task stops by itself once the stream finishes.
  • Live check: keepalives arrived every ~5 s through a 31 s prefill and were followed by the normal data stream. A 60-minute agent run that previously died at +300 s completed with no retries.

Also in this PR

test: keep Test 38 prompt under 2k tokens for gemma-4-e2b shrinks the Test 38 streaming-TTFB prompt from 300 to 270 numbers. Measured with the gemma-4-e2b tokenizer plus chat wrapper, that's 1,916 tokens instead of 2,126, which keeps the gemma-e2b CI job below a 2k-token prompt budget. The change is identical in all three open PRs from this branch set, so it merges cleanly whichever lands first.

🤖 Generated with Claude Code

CodeAndCanvas728 and others added 2 commits September 25, 2026 13:20
On slow hardware a streaming response can carry no bytes for minutes:
through a long prefill (the progress heartbeat is opt-in) and while a tool
call is buffered, since no delta is sent until it parses. Node/Bun clients
abort a body idle for ~300 s ("terminated"), and the agent's retry re-pays
the whole prefill. Observed with little-coder/pi on a 16 GB M2: a 4k-token
write tool call went silent after 16 tokens and died at +300 s, then every
retry timed out.

Emit ": keepalive" every 5 s on chat and text completion streams until the
stream terminates. SSE parsers ignore comment lines, and every yield is a
complete event, so a comment never lands inside another event.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
300 six-digit numbers measure 2,126 tokens with the gemma-4-e2b tokenizer
and chat wrapper; 270 measure 1,916, keeping CI below the 2k-token budget.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@solderzzc
solderzzc merged commit b87b7c7 into SharpAI:main Sep 25, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants