# dsh-timeout

[English](README.md) | 中文

超时的**时序与分类** 部分：一个零依赖纯函数库（无运行时 harness 依赖），由每个需要限制调用方超时提示、启动 deadline，并在之后区分「已超时」与「已取消」的功能共享。

它**不负责终止**。它发出的信号只会*通知*；真正停止工作仍由各功能负责，因为机制各不相同：bash 对操作系统进程组发送 SIGKILL，web 拆除 `fetch` socket，没有任何共享层能够承担全部终止机制。[Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md) 将边界划定为：共享时序/分类，将强制终止保留在本地。

它是**库，而非服务或插件**：没有 `ctx`，不注册任何内容，不持有状态，也不发出事件。「超时服务」必须了解如何停止每项功能的工作，这正是微内核要排除在共享层之外的知识。

## 对外接口

```ts
import { clampTimeout, deadline, idleWatchdog, MAX_TIMER_DELAY_MS, timeoutOf, TimeoutReason } from '@deepseek-ai/dsh-timeout'
```

| 导出项 | 职责 |
|---|---|
| `clampTimeout(requested, def, max, name?)` | 验证调用方可选的正有限提示，从 `def` 填充，并限制在 `max` 以内。如果提示不为正数或有限数，则抛出错误（包含 `name`）。 |
| `deadline(upstream, timeoutMs, code)` | 将 `upstream` 取消与超时融合为一个 `AbortSignal`（`AbortSignal.any`）；超时携带 `TimeoutReason`。`[Symbol.dispose]` 清除 timer。 |
| `idleWatchdog(upstream, timeoutMs, code)` | 保持一个稳定的融合信号，并且只在受保护的异步迭代器 `next()` 尚未完成时启动。解析后取消启动；后续需求重新启动；dispose 清除；并发需求被拒绝。 |
| `MAX_TIMER_DELAY_MS` | Node 在不将延迟限制为 1 毫秒时可调度的最大延迟（`2_147_483_647`）。拥有 timer 的配置不得超过该值。 |
| `timeoutOf(signal \| { reason }, code?)` | 从已中止的信号/错误中恢复 `TimeoutReason`，否则返回 `undefined`，即超时与取消的分类器。传入 `code` 可仅匹配这个 deadline 的 timer（见下文的嵌套）。 |
| `TimeoutReason` | 印在超时中止上的内部原因（`code` + `timeoutMs`）。它不是公开错误；提供方将其转换为自己的错误/字段。 |

## `timeoutMs <= 0` 哨兵值

`0` 是后端自有后台工作（bash `start()`）使用的「无超时」值，其可见范围为：**内部**。`deadline()` 不启动 timer，只转发 `upstream`；如果也没有 upstream，它将返回永不中止的信号和无操作 disposer，因此每个调用方都能保持同一种调用形态。外部请求提示会通过 `clampTimeout` 验证为**正有限数**，之后才进入 `deadline`，因此 `0` 绝不是面向模型/插件的「禁用超时」值。

## 使用形态

```ts
import { deadline, timeoutOf } from '@deepseek-ai/dsh-timeout'

declare function runWork(options: { signal: AbortSignal }): Promise<unknown>

// Scope-lifetime consumer (foreground bash, one fetch): `using` disposes the timer.
export async function runWithDeadline(upstream: AbortSignal | undefined, timeoutMs: number): Promise<unknown> {
  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 // classify the first abort, scoped to OUR code
  const aborted = d.signal.aborted && !timedOut                     // mutually exclusive: timeout won, or cancel did
  return { outcome, timedOut, aborted }
}
```

该信号只会*通知*；调用方必须连接自己的终止机制（`d.signal.addEventListener('abort', kill)`，或将 `d.signal` 传给 `fetch`）。让 promise 与 timer 竞速，会在子进程或 socket 泄漏的情况下就解析工具调用；发出信号则会强制要求存在真正的终止路径。

将你自己的 `code` 传给 `timeoutOf`，以便分类可在嵌套中组合：当你收到的 `upstream` *本身*就是 deadline 信号时（未来启动每次调用 deadline 的 `tools/execute` 中间件），如果外层 timer 首先触发，`AbortSignal.any` 会保留外层 `TimeoutReason`。将范围限定为你的 `code`，可将外部超时视为普通 upstream 取消，这才是你所属功能视角下的正确分类，而不会在本地 timer 尚未到期时就声称自己超时。

对于流式传输，创建一个 `idleWatchdog`，将其稳定的 `signal` 传入传输，并为每次提供方读取调用 `watchdog.next(iterator)`。间隔必须为正有限数，且不得超过 `MAX_TIMER_DELAY_MS`；否则 Node 会将其限制为 1 毫秒。它只测量尚未完成的需求，因此当下游代码进行渲染或在请求下一个分片前以其他方式等待时，timer 不会运行。该原语仍然只会通知，因此传输必须观察稳定信号；DeepSeek 和 pi-ai 适配器证明，超时会关闭它们的真实响应正文或 SDK 请求。

## 哪些操作不设置超时

本地文件 `read`/`write`/`edit` 不接受 `timeoutMs`：系统调用最多只能尽力中止，超时无法强制 `fsync`/`rename` 停止，而添加超时将成为违反显式优于隐式的默认值。详见 [`fs/`](../../fs/README.md)。

## 模型体验

通过 `dsh-timeout-policy` 等消费方间接影响模型；消费方可能会将提供方结果替换为已保留的超时错误，或抑制延迟结果。

#### KV 缓存影响

不直接导致失效；指定的消费方负责其引起的任何请求前缀变更。

## 已知限制与待完成工作

- **只发出通知**：deadline 无法停止忽略其信号的工作；每项功能仍需要自己的 socket/进程/任务终止路径。
- **`timeoutMs <= 0` 是内部词汇**：只有在所属后端已解析策略后，它才会禁用本地 timer；绝不会作为面向模型/插件的公开开关。
- **第一个中止原因决定分类**：当 upstream 取消早于本地 timer 发生时，即使自己的超时之后也会到期，该层也无法再报告。
- **空闲 watchdog 不是总 deadline**：它针对每个尚未完成的迭代器需求重新启动，并刻意排除消费方的思考时间。
