MoonshotAI / MoonshotAI/kimi-code

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

Open
#1,050 3 comments 0 reactions 0 assignees View on GitHub

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

  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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.