description: "Shared timeout arithmetic, deadline fusion, and timeout-versus-cancel classification for capabilities that clamp a caller's hint, arm a deadline, and must tell the two apart later."
English | 中文
dsh-timeout lets a capability run one unit of work under a caller-visible timeout and later tell a timeout apart from a cancellation. A caller's optional hint is clamped against a backend default and cap, and upstream cancellation fuses with the deadline into one AbortSignal. The deadline signal only notifies — each capability owns the mechanism that stops its work, so no shared layer needs to know how to stop anything. For streamed transports an idle watchdog arms a timeout only while a provider read is outstanding, so consumer think time never counts as idle. A timeoutMs of zero is the internal no-timeout sentinel for backend-owned background work, never a public disable switch; the zero-dependency library is shared by the bash, web, subprocess, and tool-timeout-policy consumers.
Use deadline when a capability runs one unit of work under a caller-visible timeout, and idleWatchdog when it reads a streamed transport. Validate caller hints with clampTimeout first so the timeoutMs that reaches deadline is always positive and finite.
import { clampTimeout } from '@deepseek-ai/dsh-timeout'
declare const requested: number | undefined
declare const DEFAULT_TIMEOUT_MS: number
declare const MAX_TIMEOUT_MS: number
const timeoutMs = clampTimeout(requested, DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS, 'bash-local: request.timeoutMs')
clampTimeout fills the backend default when the hint is absent, caps the result at the backend maximum, and rejects a non-positive or non-finite hint with the caller-provided name. Zero is never accepted here: it is not a public disable-timeout value.
import { deadline, timeoutOf } from '@deepseek-ai/dsh-timeout'
using d = deadline(upstream, timeoutMs, 'BASH_TIMEOUT')
const outcome = await runWork({ signal: d.signal }) // work listens on d.signal and terminates itself
const timedOut = timeoutOf(d.signal, 'BASH_TIMEOUT') !== undefined
const aborted = d.signal.aborted && !timedOut
The signal only notifies: the caller must attach its own termination — hand d.signal to fetch, or listen for abort and kill the child. Racing a promise against a timer would resolve the tool call while the child process or socket leaks on.
timeoutOf(signal, code) recovers the timeout reason only when this deadline's timer fired first. Pass your own code so classification composes under nesting: when upstream is itself a deadline signal, a foreign timeout reads as an ordinary upstream cancellation instead of claiming that the local timer expired.
import { idleWatchdog } from '@deepseek-ai/dsh-timeout'
declare const upstream: AbortSignal | undefined
declare const idleMs: number
declare const providerIterator: AsyncIterator<unknown>
using watchdog = idleWatchdog(upstream, idleMs, 'LLM_STREAM_IDLE_TIMEOUT')
const next = await watchdog.next(providerIterator) // timer runs only while this read is outstanding
The timer is armed only while an iterator next() is outstanding and rearms on pulse() for transport activity that yields no value, so consumer think time between reads never counts as idle. The interval must be positive, finite, and no greater than MAX_TIMER_DELAY_MS.
Local file read/write/edit take no timeoutMs: file IO runs untimed because a deadline would kill work the OS will still finish.
Read these pages when you need the consumers or the boundary decision behind the library.
Indirectly, through the timeout consumers that render timeout outcomes.
No direct invalidation; the timeout consumers own any request-prefix changes.
These limits define what the library deliberately does not do. They are current package constraints, not a task backlog.
timeoutMs <= 0 is internal vocabulary — it disables the local timer only after an owning backend has resolved policy, never as a public model- or plugin-facing knob.