Skip to content

kosong: streaming response hangs forever when no chunks arrive (no idle timeout) #1050

Description

@tomsen02

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

  1. Start a Kimi Code session and send a message that streams a long response.
  2. 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.
  3. The TUI continues to show the thinking/spinner state forever.
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions