MoonshotAI / MoonshotAI/kimi-code
kosong: streaming response hangs forever when no chunks arrive (no idle timeout)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 7.5k
- Forks
- 1.2k
- Avg merge
- 11h 53m
- Merged PRs (30d)
- 350
Description
Bug description
When a model stream stalls mid-response — connection remains ESTABLISHED but no bytes arrive for a long period — packages/kosong/src/generate.ts waits forever inside its for await (const part of stream) loop. There is no chunk-level idle timeout, so the UI stays in the "thinking" state indefinitely and the user has no recovery path other than killing the process.
Reproduction
- Start a Kimi Code session and send a message that streams a long response.
- At some point during streaming, the connection becomes silent (server stops sending chunks but does not close the stream). This has been observed as
nettopshowing the PID'sbytes_inflat for 10+ seconds while the stream is still open. - The TUI continues to show the thinking/spinner state forever.
- No timeout, no retry, no error is surfaced.
Expected behavior
If no chunk arrives for a configurable idle period (e.g. 30 seconds), the generator should abort the stalled stream and let the existing retry logic (isRetryableGenerateError) attempt to resume or fail with a clear error.
Actual behavior
for await (const part of stream) at packages/kosong/src/generate.ts:114 blocks forever. The SDK has first-byte timeout and request-level retry, but no between-chunk idle timeout.
Root cause
for await (const part of stream) {
await throwIfAborted(options?.signal, stream);
// ...
}
The async iterator read has no Promise.race(read, idleTimeout) guard. A silent stream is indistinguishable from a healthy but slow stream at this layer.
Suggested fix
Wrap each stream.next() read with an idle timeout:
const iterator = stream[Symbol.asyncIterator]();
while (true) {
await throwIfAborted(options?.signal, stream);
const { value: part, done } = await withIdleTimeout(
iterator.next(),
STREAM_IDLE_TIMEOUT_MS,
);
if (done) break;
// ... existing part handling ...
}
On timeout, abort the stream and throw an error that isRetryableGenerateError treats as retryable, reusing the existing retry path.
Alternatively, add the idle timeout inside each provider's stream reader and surface it as a standard error.
Affected code
packages/kosong/src/generate.ts(main loop)- Possibly provider-specific stream implementations if the timeout is pushed down
Environment
packages/kosong- Any provider / model
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 in packages/kosong/src/generate.ts at the streaming loop around line 114, then read isRetryableGenerateError and the existing abort and retry flow. Implement a configurable between-chunk idle timeout that aborts a silent stream and reaches the retryable error path; done means a stalled stream no longer leaves the UI waiting forever.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 67/100