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
nettop showing the PID's bytes_in flat 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
Bug description
When a model stream stalls mid-response — connection remains
ESTABLISHEDbut no bytes arrive for a long period —packages/kosong/src/generate.tswaits forever inside itsfor 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
nettopshowing the PID'sbytes_inflat for 10+ seconds while the stream is still open.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)atpackages/kosong/src/generate.ts:114blocks forever. The SDK has first-byte timeout and request-level retry, but no between-chunk idle timeout.Root cause
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:On timeout, abort the stream and throw an error that
isRetryableGenerateErrortreats 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)Environment
packages/kosong