@buddhilive/dsh-timeout 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -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: f446f9b33edad5badc1218802b27547ef116f7db
6
+ README.zh.md: 31b4c4f5d42777b2a2439f85d3d517ec6cd435f9
package/README.md ADDED
@@ -0,0 +1,154 @@
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
+ # @buddhilive/dsh-timeout
7
+
8
+ English | [中文](README.zh.md)
9
+
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
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
27
+
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.
29
+
30
+ ### Clamping a timeout hint
31
+
32
+ ```ts
33
+ import { clampTimeout } from '@buddhilive/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')
40
+ ```
41
+
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 '@buddhilive/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
+ ```
54
+
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.
56
+
57
+ ### Classifying the outcome
58
+
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
62
+
63
+ ```ts
64
+ import { idleWatchdog } from '@buddhilive/dsh-timeout'
65
+
66
+ declare const upstream: AbortSignal | undefined
67
+ declare const idleMs: number
68
+ declare const providerIterator: AsyncIterator<unknown>
69
+
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
72
+ ```
73
+
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`.
75
+
76
+ ### What does not get a timeout
77
+
78
+ Local file `read`/`write`/`edit` take no `timeoutMs`: file IO runs untimed because a deadline would kill work the OS will still finish.
79
+
80
+ -----
81
+
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) |
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>
126
+ ## Model Experience
127
+
128
+ Indirectly, through the timeout consumers that render timeout outcomes.
129
+
130
+ #### KV Cache effect
131
+
132
+ No direct invalidation; the timeout consumers own any request-prefix changes.
133
+
134
+ ## Known Limitations and Deferred Work
135
+
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.
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.
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 ADDED
@@ -0,0 +1,154 @@
1
+ ---
2
+ description: "共享超时运算、截止时间融合与超时/取消分类,供需要限制调用方提示、启动 deadline 并在之后区分二者的能力使用。"
3
+ kind: "package-library"
4
+ ---
5
+
6
+ # @buddhilive/dsh-timeout
7
+
8
+ [English](README.md) | 中文
9
+
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
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
27
+
28
+ 当能力要在调用方可见的超时下运行一个工作单元时使用 `deadline`,读取流式传输时使用 `idleWatchdog`。先用 `clampTimeout` 验证调用方提示,确保到达 `deadline` 的 `timeoutMs` 总是正有限值。
29
+
30
+ ### 限制超时提示
31
+
32
+ ```ts
33
+ import { clampTimeout } from '@buddhilive/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')
40
+ ```
41
+
42
+ 提示缺失时 `clampTimeout` 填入后端默认值,把结果限制在后端最大值以内,并以调用方提供的名字拒绝非正数或非有限值的提示。此处绝不接受 0:它不是公开的禁用超时值。
43
+
44
+ ### 在 deadline 下运行工作
45
+
46
+ ```text
47
+ import { deadline, timeoutOf } from '@buddhilive/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
+ ```
54
+
55
+ 该信号只负责通知:调用方必须接入自己的终止机制——把 `d.signal` 传给 `fetch`,或监听 `abort` 并杀死子进程。让 promise 与 timer 竞速,会在子进程或套接字仍在泄漏时就让工具调用完成。
56
+
57
+ ### 分类结果
58
+
59
+ 只有当本 deadline 的 timer 先触发时,`timeoutOf(signal, code)` 才恢复超时原因。传入你自己的 `code`,让分类在嵌套场景中正确组合:当 `upstream` 本身是 deadline 信号时,外部超时会被当作普通的上游取消,而不是声称本地 timer 已到期。
60
+
61
+ ### 用空闲 watchdog 处理流式传输
62
+
63
+ ```ts
64
+ import { idleWatchdog } from '@buddhilive/dsh-timeout'
65
+
66
+ declare const upstream: AbortSignal | undefined
67
+ declare const idleMs: number
68
+ declare const providerIterator: AsyncIterator<unknown>
69
+
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
72
+ ```
73
+
74
+ timer 只在某个迭代器 `next()` 尚未完成时启动,并会因不产生值的传输活动通过 `pulse()` 重新启动,因此读取之间的消费方思考时间绝不计入空闲。间隔必须为正有限数,且不得大于 `MAX_TIMER_DELAY_MS`。
75
+
76
+ ### 哪些操作不设置超时
77
+
78
+ 本地文件 `read`/`write`/`edit` 不接受 `timeoutMs`:文件 IO 不设时限地运行,因为截止时间会中止操作系统仍会完成的工作。
79
+
80
+ -----
81
+
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) | 不变式伴生插件(无运行时不变式;时序运算由单元测试覆盖) |
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>
126
+ ## 模型体验
127
+
128
+ 通过渲染超时结果的超时消费方间接影响模型。
129
+
130
+ #### KV Cache 影响
131
+
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>
151
+
152
+ 无。
153
+
154
+ </details>
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 @buddhilive/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 `@buddhilive/dsh-timeout`.
4
+ * @module @buddhilive/dsh-timeout/invariant
5
+ */
6
+ const PACKAGE_NAME = "@buddhilive/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 @buddhilive/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 `@buddhilive/dsh-timeout`.
3
+ * @module @buddhilive/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": "@buddhilive/dsh-timeout",
3
+ "description": "Zero-dependency timeout/deadline primitive: clampTimeout, deadline, timeoutOf, TimeoutReason (timing + classification only, no termination)",
4
+ "version": "0.1.2-alpha.3",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Buddhilive/buddhi-ai-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": "MIT",
34
+ "peerDependencies": {
35
+ "@buddhilive/dsh-invariants": "^0.1.2-alpha.3",
36
+ "@deepseek-ai/cordis": "^4.0.2"
37
+ },
38
+ "devDependencies": {
39
+ "@buddhilive/dsh-invariants": "^0.1.2-alpha.3",
40
+ "@deepseek-ai/cordis": "^4.0.2"
41
+ }
42
+ }