@deepseek-ai/dsh-timeout 0.1.1-rc.2 → 0.1.2-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/util/timeout/README.md
5
- README.md: c29f6a7661abf40eadafc7637c1bbca509a62a24
6
- README.zh.md: 38bbebe1462e4351a207b28a831d1274d9578e58
5
+ README.md: f446f9b33edad5badc1218802b27547ef116f7db
6
+ README.zh.md: 31b4c4f5d42777b2a2439f85d3d517ec6cd435f9
package/README.md CHANGED
@@ -1,70 +1,154 @@
1
- # dsh-timeout
1
+ ---
2
+ 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."
3
+ kind: "package-library"
4
+ ---
5
+
6
+ # @deepseek-ai/dsh-timeout
2
7
 
3
8
  English | [中文](README.zh.md)
4
9
 
5
- The **timing-and-classification** half of a timeout — a zero-dependency library of pure functions (no runtime harness deps) shared by every capability that clamps a caller's timeout hint, arms a deadline, and later has to tell "timed out" apart from "cancelled".
10
+ ## Summary
11
+
12
+ `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.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
6
24
 
7
- It owns **no termination**. The signal it hands out only *notifies*; actually stopping the work stays in each capability, because that mechanism differs — bash SIGKILLs an OS process group, web tears down a `fetch` socket — and no shared layer can own all of them. This is the boundary the [Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md) draws: share the timing/classification, keep the hard kill local.
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
8
27
 
9
- It is a **library, not a service or plugin**: no `ctx`, registers nothing, holds no state, emits no events. A "timeout service" would have to understand how to stop every capability's work exactly the knowledge a microkernel keeps out of shared layers.
28
+ 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.
10
29
 
11
- ## API
30
+ ### Clamping a timeout hint
12
31
 
13
32
  ```ts
14
- import { clampTimeout, deadline, idleWatchdog, MAX_TIMER_DELAY_MS, timeoutOf, TimeoutReason } from '@deepseek-ai/dsh-timeout'
33
+ import { clampTimeout } from '@deepseek-ai/dsh-timeout'
34
+
35
+ declare const requested: number | undefined
36
+ declare const DEFAULT_TIMEOUT_MS: number
37
+ declare const MAX_TIMEOUT_MS: number
38
+
39
+ const timeoutMs = clampTimeout(requested, DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS, 'bash-local: request.timeoutMs')
15
40
  ```
16
41
 
17
- | Export | Role |
18
- |---|---|
19
- | `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. |
20
- | `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. |
21
- | `idleWatchdog(upstream, timeoutMs, code)` | Keep one stable fused signal and arm only while its guarded async-iterator `next()` is outstanding. Resolution disarms; later demand or `pulse()` activity rearms; disposal clears; concurrent demand rejects. |
22
- | `MAX_TIMER_DELAY_MS` | Largest delay Node schedules without clamping it to one millisecond (`2_147_483_647`). Timer-owning config must not exceed it. |
23
- | `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). |
24
- | `TimeoutReason` | The internal reason (`code` + `timeoutMs`) stamped on a timeout abort. Not a public error — providers translate it into their own error/field. |
42
+ `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.
43
+
44
+ ### Running work under a deadline
45
+
46
+ ```text
47
+ import { deadline, timeoutOf } from '@deepseek-ai/dsh-timeout'
48
+
49
+ using d = deadline(upstream, timeoutMs, 'BASH_TIMEOUT')
50
+ const outcome = await runWork({ signal: d.signal }) // work listens on d.signal and terminates itself
51
+ const timedOut = timeoutOf(d.signal, 'BASH_TIMEOUT') !== undefined
52
+ const aborted = d.signal.aborted && !timedOut
53
+ ```
25
54
 
26
- ## The `timeoutMs <= 0` sentinel
55
+ 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.
27
56
 
28
- `0` is the **internal** "no timeout" value for backend-owned background work (bash `start()`): `deadline()` arms no timer and forwards only `upstream`; with no upstream either, it returns a never-aborting signal plus a no-op disposer, so every caller keeps one call shape. External request hints validate as **positive finite** via `clampTimeout` before they reach `deadline`, so `0` is never a model-/plugin-facing "disable timeout" value.
57
+ ### Classifying the outcome
29
58
 
30
- ## Usage shape
59
+ `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.
60
+
61
+ ### Streaming with an idle watchdog
31
62
 
32
63
  ```ts
33
- import { deadline, timeoutOf } from '@deepseek-ai/dsh-timeout'
64
+ import { idleWatchdog } from '@deepseek-ai/dsh-timeout'
34
65
 
35
- declare function runWork(options: { signal: AbortSignal }): Promise<unknown>
66
+ declare const upstream: AbortSignal | undefined
67
+ declare const idleMs: number
68
+ declare const providerIterator: AsyncIterator<unknown>
36
69
 
37
- // Scope-lifetime consumer (foreground bash, one fetch): `using` disposes the timer.
38
- export async function runWithDeadline(upstream: AbortSignal | undefined, timeoutMs: number): Promise<unknown> {
39
- using d = deadline(upstream, timeoutMs, 'BASH_TIMEOUT')
40
- const outcome = await runWork({ signal: d.signal }) // work listens on d.signal and terminates itself
41
- const timedOut = timeoutOf(d.signal, 'BASH_TIMEOUT') !== undefined // classify the first abort, scoped to OUR code
42
- const aborted = d.signal.aborted && !timedOut // mutually exclusive: timeout won, or cancel did
43
- return { outcome, timedOut, aborted }
44
- }
70
+ using watchdog = idleWatchdog(upstream, idleMs, 'LLM_STREAM_IDLE_TIMEOUT')
71
+ const next = await watchdog.next(providerIterator) // timer runs only while this read is outstanding
45
72
  ```
46
73
 
47
- 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.
74
+ 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`.
48
75
 
49
- Pass your own `code` to `timeoutOf` so classification composes under nesting. When `upstream` is itself a deadline signal, `AbortSignal.any` preserves its `TimeoutReason` if that timer fires first. Scoping to your code makes a foreign timeout read as an ordinary upstream cancel instead of claiming that the local timer expired.
76
+ ### What does not get a timeout
50
77
 
51
- For a streamed transport, create one `idleWatchdog`, pass its stable `signal` into the transport, and call `watchdog.next(iterator)` for each provider read. Call `watchdog.pulse()` when transport activity does not yield an iterator value. The interval must be positive, finite, and no greater than `MAX_TIMER_DELAY_MS`; Node otherwise clamps it to one millisecond. It measures only outstanding demand, so no timer runs while downstream code renders or otherwise waits before asking for the next chunk. The primitive still only notifies, so the transport must observe the stable signal; the DeepSeek and pi-ai adapters prove that timeout closes their real response body or SDK request.
78
+ Local file `read`/`write`/`edit` take no `timeoutMs`: file IO runs untimed because a deadline would kill work the OS will still finish.
52
79
 
53
- ## What does NOT get a timeout
80
+ -----
54
81
 
55
- Local file `read`/`write`/`edit` take no `timeoutMs`: file IO runs untimed because a deadline would kill work the OS will still finish. See [the filesystem subsystem page](../../../docs/subsystems/filesystem.md).
82
+ <a id="understand-the-implementation"></a>
83
+ ## Understand the implementation
84
+
85
+ <details>
86
+ <summary>Implementation internals — click to expand</summary>
87
+
88
+ The library is built on one boundary: share the timing and classification, keep the hard kill local.
89
+
90
+ ### Source map
91
+
92
+ | File | Role |
93
+ |---|---|
94
+ | [`src/index.ts`](src/index.ts) | `clampTimeout`, `deadline`, `idleWatchdog`, `timeoutOf`, `TimeoutReason`, `MAX_TIMER_DELAY_MS` |
95
+ | [`src/invariant.ts`](src/invariant.ts) | Invariant companion (no runtime invariant; the timing algebra is exercised by unit tests) |
56
96
 
97
+ ### How a deadline fuses sources
98
+
99
+ `deadline` arms one timer and fuses its abort with the upstream signal via `AbortSignal.any`, which adopts the reason of whichever source aborts first — so a race resolves to a single cause. The `TimeoutReason` carries the capability-owned `code` and the elapsed `timeoutMs`; `timeoutOf` reads it only when the timeout won, and upstream-wins leaves an ordinary abort reason. `[Symbol.dispose]` clears the timer.
100
+
101
+ ### The no-timeout sentinel
102
+
103
+ `timeoutMs <= 0` arms no timer and forwards only the upstream signal — or a never-aborting signal when there is none — so every caller keeps one call shape. The sentinel exists for backend-owned background work; external request hints are validated positive and finite before they reach `deadline`.
104
+
105
+ ### Why an idle watchdog rearms
106
+
107
+ `idleWatchdog` keeps one stable fused signal and arms the timer only while `next()` is outstanding; resolution disarms, later demand or `pulse()` rearms, disposal clears, and concurrent demand rejects. Only the transport observes the signal, so the provider's real read must listen to it — the DeepSeek and pi-ai adapters close their response body or SDK request on abort.
108
+
109
+ </details>
110
+
111
+ -----
112
+
113
+ <a id="further-exploration"></a>
114
+ ## Further Exploration
115
+
116
+ Read these pages when you need the consumers or the boundary decision behind the library.
117
+
118
+ - [Timeout-deadline library Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md) — the shared-timing, local-kill boundary.
119
+ - [Tool-call timeout policy](../../guard/timeout-policy/README.md) — the consumer that enforces declared tool timeouts.
120
+ - [Bash provider](../../shell/bash-local/README.md) — a foreground deadline consumer that kills a process group.
121
+ - [Filesystem subsystem](../../../docs/subsystems/filesystem.md) — why local file IO runs untimed.
122
+
123
+ -----
124
+
125
+ <a id="model-experience"></a>
57
126
  ## Model Experience
58
127
 
59
- Indirectly, through consumers such as `dsh-tool-call-timeout-policy`, which may replace a provider result with a retained timeout error or suppress a late result.
128
+ Indirectly, through the timeout consumers that render timeout outcomes.
60
129
 
61
130
  #### KV Cache effect
62
131
 
63
- No direct invalidation; the named consumer owns any request-prefix changes.
132
+ No direct invalidation; the timeout consumers own any request-prefix changes.
64
133
 
65
134
  ## Known Limitations and Deferred Work
66
135
 
67
- - **Notification only** — a deadline cannot stop work that ignores its signal; every capability still needs its own socket/process/task termination path.
68
- - **`timeoutMs <= 0` is internal vocabulary** — it disables the local timer only after an owning backend has resolved policy, never as a public model/plugin knob.
136
+ <a id="known-limitations-and-deferred-work"></a>
137
+
138
+
139
+ These limits define what the library deliberately does not do. They are current package constraints, not a task backlog.
140
+
141
+ - **Notification only** — a deadline cannot stop work that ignores its signal; every capability still needs its own socket, process, or task termination path.
142
+ - **`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.
69
143
  - **The first abort reason wins classification** — when an upstream cancellation beats the local timer, this layer cannot later report that its own timeout would also have elapsed.
70
144
  - **An idle watchdog is not a total deadline** — it rearms per outstanding iterator demand and deliberately excludes consumer think time.
145
+
146
+ <a id="dev-note"></a>
147
+ ### Dev Note
148
+
149
+ <details>
150
+ <summary>Working context for maintainers — click to expand</summary>
151
+
152
+ None.
153
+
154
+ </details>
package/README.zh.md CHANGED
@@ -1,70 +1,154 @@
1
- # dsh-timeout
1
+ ---
2
+ description: "共享超时运算、截止时间融合与超时/取消分类,供需要限制调用方提示、启动 deadline 并在之后区分二者的能力使用。"
3
+ kind: "package-library"
4
+ ---
5
+
6
+ # @deepseek-ai/dsh-timeout
2
7
 
3
8
  [English](README.md) | 中文
4
9
 
5
- 超时的**时序与分类**部分:一个零依赖纯函数库(无运行时 harness 依赖),由每个需要限制调用方超时提示、启动 deadline,并在之后区分「已超时」与「已取消」的能力共享。
10
+ ## 概述
11
+
12
+ `dsh-timeout` 让能力在调用方可见的超时下运行一个工作单元,之后能把超时与取消区分开。调用方的可选提示会按后端默认值补齐、并按后端上限封顶,上游取消与截止时间融合为一个 `AbortSignal`。deadline 信号只负责通知——停止工作的机制由各能力自己拥有,因此没有任何共享层需要知道如何停止任何东西。对于流式传输,空闲 watchdog 只在提供方读取尚未完成时启动超时,因此消费方的思考时间绝不计入空闲。`timeoutMs` 为 0 是后端自有后台工作使用的内部「无超时」哨兵值,绝不是公开的禁用开关;这个零依赖库由 bash、web、subprocess 与 tool-timeout-policy 消费方共享。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
6
24
 
7
- 它**不负责终止**。它发出的信号只会*通知*;真正停止工作仍由各能力负责,因为机制各不相同:bash 对操作系统进程组发送 SIGKILL,web 关闭 `fetch` 套接字,没有任何共享层能够承担全部终止机制。[Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.zh.md) 将边界划定为:共享时序/分类,将强制终止保留在本地。
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
8
27
 
9
- 它是**库,而非服务或插件**:没有 `ctx`,不注册任何内容,不持有状态,也不发出事件。「超时服务」必须了解如何停止每项能力的工作,这正是微内核要排除在共享层之外的知识。
28
+ 当能力要在调用方可见的超时下运行一个工作单元时使用 `deadline`,读取流式传输时使用 `idleWatchdog`。先用 `clampTimeout` 验证调用方提示,确保到达 `deadline` 的 `timeoutMs` 总是正有限值。
10
29
 
11
- ## 对外接口
30
+ ### 限制超时提示
12
31
 
13
32
  ```ts
14
- import { clampTimeout, deadline, idleWatchdog, MAX_TIMER_DELAY_MS, timeoutOf, TimeoutReason } from '@deepseek-ai/dsh-timeout'
33
+ import { clampTimeout } from '@deepseek-ai/dsh-timeout'
34
+
35
+ declare const requested: number | undefined
36
+ declare const DEFAULT_TIMEOUT_MS: number
37
+ declare const MAX_TIMEOUT_MS: number
38
+
39
+ const timeoutMs = clampTimeout(requested, DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS, 'bash-local: request.timeoutMs')
15
40
  ```
16
41
 
17
- | 导出项 | 职责 |
18
- |---|---|
19
- | `clampTimeout(requested, def, max, name?)` | 验证调用方可选的、值为正且有限的提示,从 `def` 填充,并限制在 `max` 以内。如果提示为非正数或非有限数,则抛出错误(包含 `name`)。 |
20
- | `deadline(upstream, timeoutMs, code)` | 将 `upstream` 取消与超时融合为一个 `AbortSignal`(`AbortSignal.any`);超时携带 `TimeoutReason`。`[Symbol.dispose]` 清除 timer。 |
21
- | `idleWatchdog(upstream, timeoutMs, code)` | 保持一个稳定的融合信号,并且只在受保护的异步迭代器 `next()` 尚未完成时启动 timer。完成后停止 timer;后续需求或 `pulse()` 活动会重新启动 timer;dispose(资源释放)时清除;并发需求被拒绝。 |
22
- | `MAX_TIMER_DELAY_MS` | Node 在不将延迟限制为 1 毫秒时可调度的最大延迟(`2_147_483_647`)。负责 timer 的配置不得超过该值。 |
23
- | `timeoutOf(signal \| { reason }, code?)` | 从已中止的信号/错误中恢复 `TimeoutReason`,否则返回 `undefined`,即超时与取消的分类器。传入 `code` 可仅匹配这个 deadline 的 timer(见下文的嵌套)。 |
24
- | `TimeoutReason` | 标记在超时中止上的内部原因(`code` + `timeoutMs`)。它不是公开错误;提供方将其转换为自己的错误/字段。 |
42
+ 提示缺失时 `clampTimeout` 填入后端默认值,把结果限制在后端最大值以内,并以调用方提供的名字拒绝非正数或非有限值的提示。此处绝不接受 0:它不是公开的禁用超时值。
43
+
44
+ ### deadline 下运行工作
45
+
46
+ ```text
47
+ import { deadline, timeoutOf } from '@deepseek-ai/dsh-timeout'
48
+
49
+ using d = deadline(upstream, timeoutMs, 'BASH_TIMEOUT')
50
+ const outcome = await runWork({ signal: d.signal }) // work listens on d.signal and terminates itself
51
+ const timedOut = timeoutOf(d.signal, 'BASH_TIMEOUT') !== undefined
52
+ const aborted = d.signal.aborted && !timedOut
53
+ ```
25
54
 
26
- ## `timeoutMs <= 0` 哨兵值
55
+ 该信号只负责通知:调用方必须接入自己的终止机制——把 `d.signal` 传给 `fetch`,或监听 `abort` 并杀死子进程。让 promise 与 timer 竞速,会在子进程或套接字仍在泄漏时就让工具调用完成。
27
56
 
28
- `0` 是后端自有后台工作(bash `start()`)使用的**内部**「无超时」值。`deadline()` 不启动 timer,只转发 `upstream`;如果也没有 upstream,它将返回永不中止的信号和无操作 disposer,因此每个调用方都能保持同一种调用形态。外部请求提示会通过 `clampTimeout` 验证为**正有限数**,之后才进入 `deadline`,因此 `0` 绝不是面向模型/插件的「禁用超时」值。
57
+ ### 分类结果
29
58
 
30
- ## 使用形态
59
+ 只有当本 deadline 的 timer 先触发时,`timeoutOf(signal, code)` 才恢复超时原因。传入你自己的 `code`,让分类在嵌套场景中正确组合:当 `upstream` 本身是 deadline 信号时,外部超时会被当作普通的上游取消,而不是声称本地 timer 已到期。
60
+
61
+ ### 用空闲 watchdog 处理流式传输
31
62
 
32
63
  ```ts
33
- import { deadline, timeoutOf } from '@deepseek-ai/dsh-timeout'
64
+ import { idleWatchdog } from '@deepseek-ai/dsh-timeout'
34
65
 
35
- declare function runWork(options: { signal: AbortSignal }): Promise<unknown>
66
+ declare const upstream: AbortSignal | undefined
67
+ declare const idleMs: number
68
+ declare const providerIterator: AsyncIterator<unknown>
36
69
 
37
- // Scope-lifetime consumer (foreground bash, one fetch): `using` disposes the timer.
38
- export async function runWithDeadline(upstream: AbortSignal | undefined, timeoutMs: number): Promise<unknown> {
39
- using d = deadline(upstream, timeoutMs, 'BASH_TIMEOUT')
40
- const outcome = await runWork({ signal: d.signal }) // work listens on d.signal and terminates itself
41
- const timedOut = timeoutOf(d.signal, 'BASH_TIMEOUT') !== undefined // classify the first abort, scoped to OUR code
42
- const aborted = d.signal.aborted && !timedOut // mutually exclusive: timeout won, or cancel did
43
- return { outcome, timedOut, aborted }
44
- }
70
+ using watchdog = idleWatchdog(upstream, idleMs, 'LLM_STREAM_IDLE_TIMEOUT')
71
+ const next = await watchdog.next(providerIterator) // timer runs only while this read is outstanding
45
72
  ```
46
73
 
47
- 该信号只会*通知*;调用方必须接入自己的终止机制(`d.signal.addEventListener('abort', kill)`,或将 `d.signal` 传给 `fetch`)。让 promise 与 timer 竞速,会在子进程或套接字仍在泄漏时就让工具调用完成;发出信号则会强制要求存在真正的终止路径。
74
+ timer 只在某个迭代器 `next()` 尚未完成时启动,并会因不产生值的传输活动通过 `pulse()` 重新启动,因此读取之间的消费方思考时间绝不计入空闲。间隔必须为正有限数,且不得大于 `MAX_TIMER_DELAY_MS`。
48
75
 
49
- 将你自己的 `code` 传给 `timeoutOf`,使分类可在嵌套场景中正确组合。当 `upstream` 本身是 deadline 信号时,如果该 timer 先触发,`AbortSignal.any` 会保留它的 `TimeoutReason`。将匹配范围限定为你的 code,会把外部超时视为普通的 upstream 取消,而不会声称本地 timer 已到期。
76
+ ### 哪些操作不设置超时
50
77
 
51
- 对于流式传输,创建一个 `idleWatchdog`,将其稳定的 `signal` 传给传输层,并为提供方的每次读取调用 `watchdog.next(iterator)`。当传输活动不产生迭代器值时,调用 `watchdog.pulse()`。间隔必须为正有限数,且不得超过 `MAX_TIMER_DELAY_MS`;否则 Node 会将其限制为 1 毫秒。它只对尚未完成的读取请求计时,因此当下游代码进行渲染或在请求下一个分片前以其他方式等待时,timer 不会运行。该原语仍然只会通知,因此传输层必须观察稳定信号;DeepSeek 和 pi-ai 适配器证明,超时会关闭它们的真实响应正文或 SDK 请求。
78
+ 本地文件 `read`/`write`/`edit` 不接受 `timeoutMs`:文件 IO 不设时限地运行,因为截止时间会中止操作系统仍会完成的工作。
52
79
 
53
- ## 哪些操作不设置超时
80
+ -----
54
81
 
55
- 本地文件 `read`/`write`/`edit` 不接受 `timeoutMs`:文件 IO 不设时限地运行,因为截止时间会中止操作系统仍会完成的工作。详见[文件系统子系统页面](../../../docs/subsystems/filesystem.zh.md)。
82
+ <a id="understand-the-implementation"></a>
83
+ ## 理解实现
84
+
85
+ <details>
86
+ <summary>实现细节——点击展开</summary>
87
+
88
+ 本库建立在一个边界之上:共享时序与分类,把强制终止保留在本地。
89
+
90
+ ### 源码地图
91
+
92
+ | 文件 | 职责 |
93
+ |---|---|
94
+ | [`src/index.ts`](src/index.ts) | `clampTimeout`、`deadline`、`idleWatchdog`、`timeoutOf`、`TimeoutReason`、`MAX_TIMER_DELAY_MS` |
95
+ | [`src/invariant.ts`](src/invariant.ts) | 不变式伴生插件(无运行时不变式;时序运算由单元测试覆盖) |
56
96
 
97
+ ### deadline 如何融合来源
98
+
99
+ `deadline` 启动一个 timer,并通过 `AbortSignal.any` 把它与上游信号融合;`AbortSignal.any` 采纳最先中止的来源的原因,因此竞争会归结为单一原因。`TimeoutReason` 携带能力自有的 `code` 与已流逝的 `timeoutMs`;只有当超时胜出时 `timeoutOf` 才读取它,上游胜出则保留普通的中止原因。`[Symbol.dispose]` 清除 timer。
100
+
101
+ ### 无超时哨兵值
102
+
103
+ `timeoutMs <= 0` 不启动 timer,只转发上游信号——没有上游时返回永不中止的信号——因此每个调用方都保持同一种调用形态。该哨兵值服务于后端自有后台工作;外部请求提示在到达 `deadline` 之前先被验证为正有限值。
104
+
105
+ ### 空闲 watchdog 为何重新启动
106
+
107
+ `idleWatchdog` 保持一个稳定的融合信号,只在 `next()` 尚未完成时启动 timer;完成后停止,后续需求或 `pulse()` 重新启动,dispose(资源释放)时清除,并发需求被拒绝。只有传输层观察该信号,因此提供方的真实读取必须监听它——DeepSeek 与 pi-ai 适配器会在中止时关闭响应正文或 SDK 请求。
108
+
109
+ </details>
110
+
111
+ -----
112
+
113
+ <a id="further-exploration"></a>
114
+ ## 进一步探索
115
+
116
+ 当你需要消费方或库背后的边界决策时,阅读以下页面。
117
+
118
+ - [超时 deadline 库 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.zh.md)——共享时序、本地强制终止的边界。
119
+ - [工具调用超时策略](../../guard/timeout-policy/README.zh.md)——强制执行已声明工具超时的消费方。
120
+ - [bash 提供方](../../shell/bash-local/README.zh.md)——杀死进程组的前台 deadline 消费方。
121
+ - [文件系统子系统](../../../docs/subsystems/filesystem.zh.md)——本地文件 IO 为何不设时限。
122
+
123
+ -----
124
+
125
+ <a id="model-experience"></a>
57
126
  ## 模型体验
58
127
 
59
- 通过 `dsh-tool-call-timeout-policy` 等消费方间接影响模型;消费方可能会将提供方结果替换为已保留的超时错误,或抑制延迟结果。
128
+ 通过渲染超时结果的超时消费方间接影响模型。
60
129
 
61
130
  #### KV Cache 影响
62
131
 
63
- 不会直接导致 KV Cache 失效;请求前缀变更由上述消费方负责。
132
+ 不会直接导致失效;请求前缀的任何变更由超时消费方负责。
133
+
134
+ ## 已知限制与延期工作
135
+
136
+ <a id="known-limitations-and-deferred-work"></a>
137
+
138
+
139
+ 这些限制说明本库刻意不做什么。它们是当前包约束,不是任务积压。
140
+
141
+ - **只发出通知**——deadline 无法停止忽略其信号的工作;每项能力仍需要自己的 socket、进程或任务终止路径。
142
+ - **`timeoutMs <= 0` 是内部词汇**——只有在所属后端已解析策略后,它才会禁用本地 timer;绝不会作为面向模型或插件的公开开关。
143
+ - **第一个中止原因决定分类**——当上游取消早于本地 timer 发生时,即使自己的超时之后也会到期,该层也无法再报告。
144
+ - **空闲 watchdog 不是总 deadline**——它针对每个尚未完成的迭代器需求重新启动,并刻意排除消费方的思考时间。
145
+
146
+ <a id="dev-note"></a>
147
+ ### 开发备注
148
+
149
+ <details>
150
+ <summary>维护者的工作上下文——点击展开</summary>
64
151
 
65
- ## 已知限制与暂缓事项
152
+ 无。
66
153
 
67
- - **只发出通知**:deadline 无法停止忽略其信号的工作;每项能力仍需要自己的 socket/进程/任务终止路径。
68
- - **`timeoutMs <= 0` 是内部词汇**:只有在所属后端已解析策略后,它才会禁用本地 timer;绝不会作为面向模型/插件的公开开关。
69
- - **第一个中止原因决定分类**:当 upstream 取消早于本地 timer 发生时,即使自己的超时之后也会到期,该层也无法再报告。
70
- - **空闲 watchdog 不是总 deadline**:它针对每个尚未完成的迭代器需求重新启动,并刻意排除消费方的处理时间。
154
+ </details>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-timeout",
3
3
  "description": "Zero-dependency timeout/deadline primitive: clampTimeout, deadline, timeoutOf, TimeoutReason (timing + classification only, no termination)",
4
- "version": "0.1.1-rc.2",
4
+ "version": "0.1.2-alpha.3",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -32,11 +32,11 @@
32
32
  ],
33
33
  "license": "MIT",
34
34
  "peerDependencies": {
35
- "@deepseek-ai/cordis": "^4.0.1",
36
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2"
35
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.3",
36
+ "@deepseek-ai/cordis": "^4.0.2"
37
37
  },
38
38
  "devDependencies": {
39
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
40
- "@deepseek-ai/cordis": "^4.0.1"
39
+ "@deepseek-ai/cordis": "^4.0.2",
40
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.3"
41
41
  }
42
42
  }