can1357/oh-my-pi · warning · AIError.StreamTimeoutError
options.errorMessage
Error message
options.errorMessage
What it means
AIError.StreamTimeoutError thrown by iterateWithIdleTimeout when no item has arrived within options.idleTimeoutMs of the last progress timestamp (or since start) and no local work is pending. Unlike the first-item variant, this is the general idle path: the stream started (or was expected to) but stalled. The message is exactly options.errorMessage.
Source
Thrown at packages/ai/src/utils/idle-iterator.ts:349
if (firstItemDeadlineMs !== undefined) {
activeTimeoutMs = firstItemDeadlineMs - Date.now();
if (activeTimeoutMs <= 0) {
if (!hasPendingLocalWork()) {
options.onFirstItemTimeout?.();
closeIterator();
throw new AIError.StreamTimeoutError(options.firstItemErrorMessage ?? options.errorMessage);
}
extendDeadlineForLocalWork();
activeTimeoutMs = firstItemDeadlineMs! - Date.now();
}
}
} else if (options.idleTimeoutMs !== undefined && options.idleTimeoutMs > 0) {
activeTimeoutMs = options.idleTimeoutMs - (Date.now() - lastProgressAt);
if (activeTimeoutMs <= 0) {
if (!hasPendingLocalWork()) {
options.onIdle?.();
closeIterator();
throw new AIError.StreamTimeoutError(options.errorMessage);
}
extendDeadlineForLocalWork();
activeTimeoutMs = options.idleTimeoutMs;
}
}
pendingNext ??= withRacy(iterator.next());
const racers: Array<
Promise<
| { kind: "next"; result: IteratorResult<T> }
| { kind: "error"; error: unknown }
| { kind: "timeout" }
| { kind: "abort" }
>
> = [pendingNext];
const enforceTimeout = !noTimeoutEnforced && activeTimeoutMs !== undefined && activeTimeoutMs > 0;View on GitHub (pinned to 9690622007)
Solutions
- Raise idleTimeoutMs to exceed the longest legitimate gap between stream events (e.g. extended thinking).
- Handle the error with retry/resume logic — reconnect and continue if the provider supports resumable streams.
- Use options.onIdle as a hook to log or send keep-alive before the throw.
- Enable TCP/HTTP keep-alive at the fetch layer to survive NAT idle drops.
Example fix
// before
iterateWithIdleTimeout(stream, { idleTimeoutMs: 5_000, errorMessage: "stream timeout" });
// after
iterateWithIdleTimeout(stream, { idleTimeoutMs: 60_000, errorMessage: "stream idle >60s, aborting" }); Defensive patterns
Strategy: retry
Try / catch
try {
for await (const ev of timedStream) handle(ev);
} catch (err) {
if (err instanceof AIError.StreamTimeoutError) {
logger.warn("stream stalled past idleTimeoutMs — reconnecting");
await reconnectAndResume();
} else throw err;
} Prevention
- Size idleTimeoutMs above the longest legitimate inter-chunk gap (thinking/tool phases).
- Use onIdle for keep-alive or progress logging before abort.
- Enable transport-level keep-alive to survive NAT/LB idle drops.
When it happens
Trigger: A wrapped stream (timedAnthropic/timedOpenai/codex SSE/etc.) goes silent longer than idleTimeoutMs between items — provider stopped sending chunks mid-generation, connection hung without RST, or an intermediary buffering indefinitely.
Common situations: Long tool-use/thinking phases where the provider legitimately sends nothing (idle timeout set too low); NAT or load balancer dropping an idle TCP connection; provider-side generation stall.
Related errors
- options.firstItemErrorMessage ?? options.errorMessage
- timed out: {command}
- V2 compaction stream closed before response.completed
- AnthropicConnectionTimeoutError
- stream ended before message_start
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/11412e80f206e7df.
Report an issue: GitHub.