|
|
@@ -16,7 +16,7 @@ import { clampTimeout, deadline, timeoutOf, TimeoutReason } from '@deepseek-ai/d
|
|
|
|---|---|
|
|
|
| `clampTimeout(requested, def, max, name?)` | Validate the caller's optional positive-finite hint, fill from `def`, cap at `max`. Throws (with `name`) on a non-positive/non-finite hint. |
|
|
|
| `deadline(upstream, timeoutMs, code)` | Fuse `upstream` cancellation with a timeout into one `AbortSignal` (`AbortSignal.any`); the timeout carries a `TimeoutReason`. `[Symbol.dispose]` clears the timer. |
|
|
|
-| `timeoutOf(signal \| { reason })` | Recover the `TimeoutReason` from an aborted signal/error, else `undefined` — the timeout-vs-cancel classifier. |
|
|
|
+| `timeoutOf(signal \| { reason }, code?)` | Recover the `TimeoutReason` from an aborted signal/error, else `undefined` — the timeout-vs-cancel classifier. Pass `code` to match only THIS deadline's timer (see nesting below). |
|
|
|
| `TimeoutReason` | The internal reason (`code` + `timeoutMs`) stamped on a timeout abort. Not a public error — providers translate it into their own error/field. |
|
|
|
|
|
|
## The `timeoutMs <= 0` sentinel
|
|
|
@@ -28,13 +28,15 @@ import { clampTimeout, deadline, timeoutOf, TimeoutReason } from '@deepseek-ai/d
|
|
|
```ts ignore-check
|
|
|
// Scope-lifetime consumer (foreground bash, one fetch): `using` disposes the timer.
|
|
|
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) !== undefined // classify the first abort
|
|
|
-const aborted = d.signal.aborted && !timedOut // mutually exclusive: timeout won, or cancel did
|
|
|
+const outcome = await runWork({ signal: d.signal }) // work listens on d.signal and terminates itself
|
|
|
+const timedOut = timeoutOf(d.signal, 'BASH_TIMEOUT') !== undefined // classify the first abort, scoped to OUR code
|
|
|
+const aborted = d.signal.aborted && !timedOut // mutually exclusive: timeout won, or cancel did
|
|
|
```
|
|
|
|
|
|
The signal only *notifies* — the caller MUST attach its own termination (`d.signal.addEventListener('abort', kill)`, or hand `d.signal` to `fetch`). Racing a promise against a timer would resolve the tool-call while the child process or socket leaks on; handing out a signal forces a real termination path to exist.
|
|
|
|
|
|
+Pass your own `code` to `timeoutOf` so classification composes under nesting: when the `upstream` you were handed is *itself* a deadline signal (a future `tools/execute` middleware arming a per-call deadline), `AbortSignal.any` preserves the outer `TimeoutReason` if the outer timer fires first. Scoping to your `code` makes a foreign timeout read as an ordinary upstream cancel — the correct classification from your capability's view — instead of your own timeout firing when your local timer never expired.
|
|
|
+
|
|
|
## What does NOT get a timeout
|
|
|
|
|
|
Local file `read`/`write`/`edit` take no `timeoutMs`: a syscall is best-effort-abortable at most, a timeout could not force `fsync`/`rename` to stop, and adding one would be an implicit default that violates explicit-over-implicit. See [`fs/`](../../fs/README.md).
|