@deepseek-ai/dsh-timeout 0.0.1-rc.1

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/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, DeepSeek
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/util/timeout/README.md
5
+ README.md: 40a655200780e25c434beed4953d96f46e3604fd
6
+ README.zh.md: 638dc1af60761f9c964e356edc070d20af0f4819
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # dsh-timeout
2
+
3
+ English | [中文](README.zh.md)
4
+
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".
6
+
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.
8
+
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.
10
+
11
+ ## Surface
12
+
13
+ ```ts
14
+ import { clampTimeout, deadline, idleWatchdog, MAX_TIMER_DELAY_MS, timeoutOf, TimeoutReason } from '@deepseek-ai/dsh-timeout'
15
+ ```
16
+
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. |
25
+
26
+ ## The `timeoutMs <= 0` sentinel
27
+
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.
29
+
30
+ ## Usage shape
31
+
32
+ ```ts
33
+ import { deadline, timeoutOf } from '@deepseek-ai/dsh-timeout'
34
+
35
+ declare function runWork(options: { signal: AbortSignal }): Promise<unknown>
36
+
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
+ }
45
+ ```
46
+
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.
48
+
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.
50
+
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.
52
+
53
+ ## What does NOT get a timeout
54
+
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).
56
+
57
+ ## Model Experience
58
+
59
+ Indirectly, through consumers such as `dsh-timeout-policy`, which may replace a provider result with a retained timeout error or suppress a late result.
60
+
61
+ #### KV Cache effect
62
+
63
+ No direct invalidation; the named consumer owns any request-prefix changes.
64
+
65
+ ## Known Limitations and Deferred Work
66
+
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.
69
+ - **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
+ - **An idle watchdog is not a total deadline** — it rearms per outstanding iterator demand and deliberately excludes consumer think time.
package/README.zh.md ADDED
@@ -0,0 +1,70 @@
1
+ # dsh-timeout
2
+
3
+ [English](README.md) | 中文
4
+
5
+ 超时的**时序与分类**部分:一个零依赖纯函数库(无运行时 harness 依赖),由每个需要限制调用方超时提示、启动 deadline,并在之后区分「已超时」与「已取消」的功能共享。
6
+
7
+ 它**不负责终止**。它发出的信号只会*通知*;真正停止工作仍由各功能负责,因为机制各不相同:bash 对操作系统进程组发送 SIGKILL,web 关闭 `fetch` 套接字,没有任何共享层能够承担全部终止机制。[Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md) 将边界划定为:共享时序/分类,将强制终止保留在本地。
8
+
9
+ 它是**库,而非服务或插件**:没有 `ctx`,不注册任何内容,不持有状态,也不发出事件。「超时服务」必须了解如何停止每项功能的工作,这正是微内核要排除在共享层之外的知识。
10
+
11
+ ## 对外接口
12
+
13
+ ```ts
14
+ import { clampTimeout, deadline, idleWatchdog, MAX_TIMER_DELAY_MS, timeoutOf, TimeoutReason } from '@deepseek-ai/dsh-timeout'
15
+ ```
16
+
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`)。它不是公开错误;提供方将其转换为自己的错误/字段。 |
25
+
26
+ ## `timeoutMs <= 0` 哨兵值
27
+
28
+ `0` 是后端自有后台工作(bash `start()`)使用的**内部**「无超时」值。`deadline()` 不启动 timer,只转发 `upstream`;如果也没有 upstream,它将返回永不中止的信号和无操作 disposer,因此每个调用方都能保持同一种调用形态。外部请求提示会通过 `clampTimeout` 验证为**正有限数**,之后才进入 `deadline`,因此 `0` 绝不是面向模型/插件的「禁用超时」值。
29
+
30
+ ## 使用形态
31
+
32
+ ```ts
33
+ import { deadline, timeoutOf } from '@deepseek-ai/dsh-timeout'
34
+
35
+ declare function runWork(options: { signal: AbortSignal }): Promise<unknown>
36
+
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
+ }
45
+ ```
46
+
47
+ 该信号只会*通知*;调用方必须接入自己的终止机制(`d.signal.addEventListener('abort', kill)`,或将 `d.signal` 传给 `fetch`)。让 promise 与 timer 竞速,会在子进程或套接字仍在泄漏时就让工具调用完成;发出信号则会强制要求存在真正的终止路径。
48
+
49
+ 将你自己的 `code` 传给 `timeoutOf`,使分类可在嵌套场景中正确组合。当 `upstream` 本身是 deadline 信号时,如果该 timer 先触发,`AbortSignal.any` 会保留它的 `TimeoutReason`。将匹配范围限定为你的 code,会把外部超时视为普通的 upstream 取消,而不会声称本地 timer 已到期。
50
+
51
+ 对于流式传输,创建一个 `idleWatchdog`,将其稳定的 `signal` 传给传输层,并为提供方的每次读取调用 `watchdog.next(iterator)`。当传输活动不产生迭代器值时,调用 `watchdog.pulse()`。间隔必须为正有限数,且不得超过 `MAX_TIMER_DELAY_MS`;否则 Node 会将其限制为 1 毫秒。它只对尚未完成的读取请求计时,因此当下游代码进行渲染或在请求下一个分片前以其他方式等待时,timer 不会运行。该原语仍然只会通知,因此传输层必须观察稳定信号;DeepSeek 和 pi-ai 适配器证明,超时会关闭它们的真实响应正文或 SDK 请求。
52
+
53
+ ## 哪些操作不设置超时
54
+
55
+ 本地文件 `read`/`write`/`edit` 不接受 `timeoutMs`:文件 IO 不设时限地运行,因为截止时间会中止操作系统仍会完成的工作。详见[文件系统子系统页面](../../../docs/subsystems/filesystem.md)。
56
+
57
+ ## 模型体验
58
+
59
+ 通过 `dsh-timeout-policy` 等消费方间接影响模型;消费方可能会将提供方结果替换为已保留的超时错误,或抑制延迟结果。
60
+
61
+ #### KV Cache 影响
62
+
63
+ 不会直接导致 KV Cache 失效;请求前缀变更由上述消费方负责。
64
+
65
+ ## 已知限制与暂缓事项
66
+
67
+ - **只发出通知**:deadline 无法停止忽略其信号的工作;每项功能仍需要自己的 socket/进程/任务终止路径。
68
+ - **`timeoutMs <= 0` 是内部词汇**:只有在所属后端已解析策略后,它才会禁用本地 timer;绝不会作为面向模型/插件的公开开关。
69
+ - **第一个中止原因决定分类**:当 upstream 取消早于本地 timer 发生时,即使自己的超时之后也会到期,该层也无法再报告。
70
+ - **空闲 watchdog 不是总 deadline**:它针对每个尚未完成的迭代器需求重新启动,并刻意排除消费方的处理时间。
package/lib/index.js ADDED
@@ -0,0 +1,140 @@
1
+ //#region lib/types/index.js
2
+ /**
3
+ * Shared timeout arithmetic, signal fusion, and classification. The library
4
+ * only notifies through abort signals; each capability still owns the mechanism
5
+ * that stops its work and translates timeout reasons into public outcomes.
6
+ * @module @deepseek-ai/dsh-timeout
7
+ */
8
+ /**
9
+ * Internal abort reason carrying a capability-owned code and elapsed deadline.
10
+ * Providers translate it through {@link timeoutOf} before returning to callers.
11
+ */
12
+ var TimeoutReason = class extends Error {
13
+ code;
14
+ timeoutMs;
15
+ name = "TimeoutReason";
16
+ /**
17
+ * @param code Capability-owned timeout code (e.g. `BASH_TIMEOUT`).
18
+ * @param timeoutMs The deadline that elapsed, in milliseconds.
19
+ */
20
+ constructor(code, timeoutMs) {
21
+ super(`${code} after ${timeoutMs}ms`);
22
+ this.code = code;
23
+ this.timeoutMs = timeoutMs;
24
+ }
25
+ };
26
+ /** Largest delay Node schedules without clamping it to one millisecond. */
27
+ const MAX_TIMER_DELAY_MS = 2147483647;
28
+ function assertTimerDelay(timeoutMs, name) {
29
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647) throw new Error(`${name} must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
30
+ }
31
+ /**
32
+ * Validate a caller's optional timeout hint, use the backend default, then cap
33
+ * it. Supplied values must be positive and finite; zero is not a public
34
+ * disable-timeout sentinel.
35
+ *
36
+ * @param requested The caller's optional hint; validated when present.
37
+ * @param def The backend default applied when `requested` is absent.
38
+ * @param max The backend upper bound the result is capped to.
39
+ * @param name Field name used in the thrown message (so the caller sees which input was
40
+ * bad).
41
+ * @returns The effective timeout in milliseconds: `min(requested ?? def, max)`.
42
+ */
43
+ function clampTimeout(requested, def, max, name = "timeoutMs") {
44
+ if (requested !== void 0 && (!Number.isFinite(requested) || requested <= 0)) throw new Error(`${name} must be a positive finite number`);
45
+ return Math.min(requested ?? def, max);
46
+ }
47
+ /**
48
+ * Fuse upstream cancellation with an identifiable timeout. `timeoutMs <= 0` is
49
+ * the internal no-timer sentinel; the returned disposer clears an armed timer.
50
+ * The signal only notifies, so callers must stop their own work.
51
+ *
52
+ * @param upstream The caller's cancellation signal, if any, fused into the result.
53
+ * @param timeoutMs Deadline in milliseconds; `<= 0` means "no timeout" (arm no timer).
54
+ * @param code Capability-owned code stamped onto the timeout's {@link TimeoutReason}.
55
+ * @returns The fused {@link Deadline} (signal + timer cleanup).
56
+ */
57
+ function deadline(upstream, timeoutMs, code) {
58
+ if (timeoutMs <= 0) return {
59
+ signal: upstream ?? new AbortController().signal,
60
+ [Symbol.dispose]() {}
61
+ };
62
+ assertTimerDelay(timeoutMs, "deadline timeoutMs");
63
+ const timer = new AbortController();
64
+ const id = setTimeout(() => {
65
+ timer.abort(new TimeoutReason(code, timeoutMs));
66
+ }, timeoutMs);
67
+ return {
68
+ signal: upstream !== void 0 ? AbortSignal.any([upstream, timer.signal]) : timer.signal,
69
+ [Symbol.dispose]() {
70
+ clearTimeout(id);
71
+ }
72
+ };
73
+ }
74
+ /**
75
+ * Create a rearmable idle watchdog for an async iterator. The timer exists only
76
+ * while {@link IdleWatchdog.next} is outstanding, so consumer think time does
77
+ * not count as provider idle time. The returned signal is stable for the whole
78
+ * call and only notifies; the iterator must observe it to terminate its work.
79
+ *
80
+ * @param upstream - caller cancellation fused into the stable signal.
81
+ * @param timeoutMs - positive finite idle interval in milliseconds.
82
+ * @param code - capability-owned code carried by the timeout reason.
83
+ * @returns a stable signal, guarded next operation, and timer disposer.
84
+ */
85
+ function idleWatchdog(upstream, timeoutMs, code) {
86
+ assertTimerDelay(timeoutMs, "idleWatchdog timeoutMs");
87
+ const timeout = new AbortController();
88
+ const signal = upstream === void 0 ? timeout.signal : AbortSignal.any([upstream, timeout.signal]);
89
+ let timer;
90
+ let outstanding = false;
91
+ let disposed = false;
92
+ const arm = () => {
93
+ if (timer !== void 0) clearTimeout(timer);
94
+ timer = setTimeout(() => {
95
+ timeout.abort(new TimeoutReason(code, timeoutMs));
96
+ }, timeoutMs);
97
+ };
98
+ return {
99
+ signal,
100
+ async next(iterator) {
101
+ if (disposed) throw new Error("idleWatchdog is disposed");
102
+ if (outstanding) throw new Error("idleWatchdog next is already outstanding");
103
+ outstanding = true;
104
+ arm();
105
+ try {
106
+ return await iterator.next();
107
+ } finally {
108
+ clearTimeout(timer);
109
+ timer = void 0;
110
+ outstanding = false;
111
+ }
112
+ },
113
+ pulse() {
114
+ if (disposed || !outstanding) return;
115
+ arm();
116
+ },
117
+ [Symbol.dispose]() {
118
+ if (disposed) return;
119
+ disposed = true;
120
+ if (timer !== void 0) clearTimeout(timer);
121
+ timer = void 0;
122
+ }
123
+ };
124
+ }
125
+ /**
126
+ * Recover a timeout reason from a reason-bearing object. Supplying `code`
127
+ * distinguishes this deadline from a nested upstream deadline; a foreign code
128
+ * follows the ordinary cancellation path.
129
+ *
130
+ * @param x An {@link AbortSignal} or any `{ reason }` carrier (e.g. a caught abort error).
131
+ * @param code When provided, only a {@link TimeoutReason} with this exact `code` matches.
132
+ * @returns The matching {@link TimeoutReason}, else `undefined`.
133
+ */
134
+ function timeoutOf(x, code) {
135
+ const reason = x.reason;
136
+ if (!(reason instanceof TimeoutReason)) return void 0;
137
+ return code === void 0 || reason.code === code ? reason : void 0;
138
+ }
139
+ //#endregion
140
+ export { MAX_TIMER_DELAY_MS, TimeoutReason, clampTimeout, deadline, idleWatchdog, timeoutOf };
@@ -0,0 +1,23 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@deepseek-ai/dsh-timeout`.
4
+ * @module @deepseek-ai/dsh-timeout/invariant
5
+ */
6
+ const PACKAGE_NAME = "@deepseek-ai/dsh-timeout";
7
+ /** Cordis companion plugin name. */
8
+ const name = "timeout-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * No runtime invariant: this pure utility owns no event stream or mutable runtime data; its value
13
+ * algebra is enforced by unit tests.
14
+ */
15
+ const install = () => {};
16
+ /**
17
+ * Register this package's invariant companion.
18
+ * @param ctx - Cordis context carrying the invariant service.
19
+ * @returns the installed registration's disposer after setup succeeds.
20
+ */
21
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
22
+ //#endregion
23
+ export { apply, inject, name };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Shared timeout arithmetic, signal fusion, and classification. The library
3
+ * only notifies through abort signals; each capability still owns the mechanism
4
+ * that stops its work and translates timeout reasons into public outcomes.
5
+ * @module @deepseek-ai/dsh-timeout
6
+ */
7
+ /**
8
+ * Internal abort reason carrying a capability-owned code and elapsed deadline.
9
+ * Providers translate it through {@link timeoutOf} before returning to callers.
10
+ */
11
+ export declare class TimeoutReason extends Error {
12
+ readonly code: string;
13
+ readonly timeoutMs: number;
14
+ name: string;
15
+ /**
16
+ * @param code Capability-owned timeout code (e.g. `BASH_TIMEOUT`).
17
+ * @param timeoutMs The deadline that elapsed, in milliseconds.
18
+ */
19
+ constructor(code: string, timeoutMs: number);
20
+ }
21
+ /** Largest delay Node schedules without clamping it to one millisecond. */
22
+ export declare const MAX_TIMER_DELAY_MS = 2147483647;
23
+ /**
24
+ * Validate a caller's optional timeout hint, use the backend default, then cap
25
+ * it. Supplied values must be positive and finite; zero is not a public
26
+ * disable-timeout sentinel.
27
+ *
28
+ * @param requested The caller's optional hint; validated when present.
29
+ * @param def The backend default applied when `requested` is absent.
30
+ * @param max The backend upper bound the result is capped to.
31
+ * @param name Field name used in the thrown message (so the caller sees which input was
32
+ * bad).
33
+ * @returns The effective timeout in milliseconds: `min(requested ?? def, max)`.
34
+ */
35
+ export declare function clampTimeout(requested: number | undefined, def: number, max: number, name?: string): number;
36
+ /** A deadline signal plus the cleanup that clears its timer (dispose-once). */
37
+ export interface Deadline {
38
+ /** Aborts on upstream cancellation OR on timeout (the timeout carries a {@link TimeoutReason}). */
39
+ readonly signal: AbortSignal;
40
+ /** Clear the timer. Safe to call once; `using` calls it at scope exit. */
41
+ [Symbol.dispose](): void;
42
+ }
43
+ /** Rearmable timeout around one outstanding async-iterator demand. */
44
+ export interface IdleWatchdog {
45
+ /** Stable signal aborted by upstream cancellation or this watchdog's timeout. */
46
+ readonly signal: AbortSignal;
47
+ /**
48
+ * Await one iterator demand while the idle timer is armed.
49
+ * @param iterator - iterator whose next value represents provider progress.
50
+ * @returns the iterator's next result.
51
+ */
52
+ next<T>(iterator: AsyncIterator<T>): Promise<IteratorResult<T>>;
53
+ /** Rearm an outstanding demand after transport activity that yields no iterator value; otherwise a no-op. */
54
+ pulse(): void;
55
+ /** Clear an armed timer; safe to call once at the owning stream's exit. */
56
+ [Symbol.dispose](): void;
57
+ }
58
+ /**
59
+ * Fuse upstream cancellation with an identifiable timeout. `timeoutMs <= 0` is
60
+ * the internal no-timer sentinel; the returned disposer clears an armed timer.
61
+ * The signal only notifies, so callers must stop their own work.
62
+ *
63
+ * @param upstream The caller's cancellation signal, if any, fused into the result.
64
+ * @param timeoutMs Deadline in milliseconds; `<= 0` means "no timeout" (arm no timer).
65
+ * @param code Capability-owned code stamped onto the timeout's {@link TimeoutReason}.
66
+ * @returns The fused {@link Deadline} (signal + timer cleanup).
67
+ */
68
+ export declare function deadline(upstream: AbortSignal | undefined, timeoutMs: number, code: string): Deadline;
69
+ /**
70
+ * Create a rearmable idle watchdog for an async iterator. The timer exists only
71
+ * while {@link IdleWatchdog.next} is outstanding, so consumer think time does
72
+ * not count as provider idle time. The returned signal is stable for the whole
73
+ * call and only notifies; the iterator must observe it to terminate its work.
74
+ *
75
+ * @param upstream - caller cancellation fused into the stable signal.
76
+ * @param timeoutMs - positive finite idle interval in milliseconds.
77
+ * @param code - capability-owned code carried by the timeout reason.
78
+ * @returns a stable signal, guarded next operation, and timer disposer.
79
+ */
80
+ export declare function idleWatchdog(upstream: AbortSignal | undefined, timeoutMs: number, code: string): IdleWatchdog;
81
+ /**
82
+ * Recover a timeout reason from a reason-bearing object. Supplying `code`
83
+ * distinguishes this deadline from a nested upstream deadline; a foreign code
84
+ * follows the ordinary cancellation path.
85
+ *
86
+ * @param x An {@link AbortSignal} or any `{ reason }` carrier (e.g. a caught abort error).
87
+ * @param code When provided, only a {@link TimeoutReason} with this exact `code` matches.
88
+ * @returns The matching {@link TimeoutReason}, else `undefined`.
89
+ */
90
+ export declare function timeoutOf(x: AbortSignal | {
91
+ reason?: unknown;
92
+ }, code?: string): TimeoutReason | undefined;
93
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@deepseek-ai/dsh-timeout`.
3
+ * @module @deepseek-ai/dsh-timeout/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "timeout-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@deepseek-ai/dsh-timeout",
3
+ "description": "Zero-dependency timeout/deadline primitive: clampTimeout, deadline, timeoutOf, TimeoutReason (timing + classification only, no termination)",
4
+ "version": "0.0.1-rc.1",
5
+ "publishConfig": {
6
+ "access": "restricted"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/util/timeout"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./src/*": "./src/*",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "lib/index.js",
30
+ "lib/invariant.js",
31
+ "lib/types/**/*.d.ts"
32
+ ],
33
+ "license": "BSD-3-Clause",
34
+ "peerDependencies": {
35
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
36
+ "@deepseek-ai/cordis": "^4.0.1-rc.1"
37
+ },
38
+ "devDependencies": {
39
+ "@deepseek-ai/cordis": "^4.0.1-rc.1",
40
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1"
41
+ }
42
+ }