feat: implement bounded LLM request recovery

This commit is contained in:
Tianyi Cui
2026-07-20 03:34:19 +08:00
parent 7cf966fc0e
commit 3b0b0cefeb
115 changed files with 3311 additions and 366 deletions

View File

@@ -21,6 +21,15 @@ export class TimeoutReason extends Error {
}
}
/** Largest delay Node schedules without clamping it to one millisecond. */
export const MAX_TIMER_DELAY_MS = 2_147_483_647
function assertTimerDelay(timeoutMs: number, name: string): void {
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_TIMER_DELAY_MS) {
throw new Error(`${name} must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`)
}
}
/**
* Validate a caller's optional timeout hint, use the backend default, then cap
* it. Supplied values must be positive and finite; zero is not a public
@@ -53,6 +62,20 @@ export interface Deadline {
[Symbol.dispose](): void
}
/** Rearmable timeout around one outstanding async-iterator demand. */
export interface IdleWatchdog {
/** Stable signal aborted by upstream cancellation or this watchdog's timeout. */
readonly signal: AbortSignal
/**
* Await one iterator demand while the idle timer is armed.
* @param iterator - iterator whose next value represents provider progress.
* @returns the iterator's next result.
*/
next<T>(iterator: AsyncIterator<T>): Promise<IteratorResult<T>>
/** Clear an armed timer; safe to call once at the owning stream's exit. */
[Symbol.dispose](): void
}
/**
* Fuse upstream cancellation with an identifiable timeout. `timeoutMs <= 0` is
* the internal no-timer sentinel; the returned disposer clears an armed timer.
@@ -74,6 +97,8 @@ export function deadline(
return { signal: upstream ?? new AbortController().signal, [Symbol.dispose]() {} }
}
assertTimerDelay(timeoutMs, 'deadline timeoutMs')
const timer = new AbortController()
const id = setTimeout(() => { timer.abort(new TimeoutReason(code, timeoutMs)) }, timeoutMs)
return {
@@ -85,6 +110,57 @@ export function deadline(
}
}
/**
* Create a rearmable idle watchdog for an async iterator. The timer exists only
* while {@link IdleWatchdog.next} is outstanding, so consumer think time does
* not count as provider idle time. The returned signal is stable for the whole
* call and only notifies; the iterator must observe it to terminate its work.
*
* @param upstream - caller cancellation fused into the stable signal.
* @param timeoutMs - positive finite idle interval in milliseconds.
* @param code - capability-owned code carried by the timeout reason.
* @returns a stable signal, guarded next operation, and timer disposer.
*/
export function idleWatchdog(
upstream: AbortSignal | undefined,
timeoutMs: number,
code: string,
): IdleWatchdog {
assertTimerDelay(timeoutMs, 'idleWatchdog timeoutMs')
const timeout = new AbortController()
const signal = upstream === undefined
? timeout.signal
: AbortSignal.any([upstream, timeout.signal])
let timer: ReturnType<typeof setTimeout> | undefined
let outstanding = false
let disposed = false
return {
signal,
async next<T>(iterator: AsyncIterator<T>): Promise<IteratorResult<T>> {
if (disposed) throw new Error('idleWatchdog is disposed')
if (outstanding) throw new Error('idleWatchdog next is already outstanding')
outstanding = true
timer = setTimeout(() => {
timeout.abort(new TimeoutReason(code, timeoutMs))
}, timeoutMs)
try {
return await iterator.next()
} finally {
clearTimeout(timer)
timer = undefined
outstanding = false
}
},
[Symbol.dispose](): void {
if (disposed) return
disposed = true
if (timer !== undefined) clearTimeout(timer)
timer = undefined
},
}
}
/**
* Recover a timeout reason from a reason-bearing object. Supplying `code`
* distinguishes this deadline from a nested upstream deadline; a foreign code