Use node:timers/promises for hand-rolled cancellable sleeps
用 node:timers/promises 替代手写的可取消休眠
Three packages hand-roll promise-wrapped timers that the `node:timers/promises` builtin already provides, while other packages (`dsh-llm-mock-server` `pause()`, `dsh-lsp-stdio`, `dsh-acp-snapshot`) already use the builtin — so the hand-rolled copies are also a consistency gap: - `packages/llm/llm-retry/src/index.ts` `cancellableDelay()` (~14 lines): `new Promise` + `setTimeout` + manual abort-listener add/remove, res
English
Problem
Three packages hand-roll promise-wrapped timers that the node:timers/promises builtin already provides, while other packages (dsh-llm-mock-server pause(), dsh-lsp-stdio, dsh-acp-snapshot) already use the builtin — so the hand-rolled copies are also a consistency gap:
packages/llm/llm-retry/src/index.tscancellableDelay()(~14 lines):new Promise+setTimeout+ manual abort-listener add/remove, resolvingtrueon elapse andfalseon abort, consumed once for the backoff wait.packages/workflow/workflow-worker-thread/src/host.tssleep()(~7 lines): promise-wrapped unref'dsetTimeoutused as the dispose-grace bound.packages/terminal/terminal-bash/src/session.tsdelay()(~4 lines): bare promise-wrappedsetTimeoutused in polling/teardown waits.
Proposal
Replace all three with import { setTimeout } from 'node:timers/promises':
- llm-retry:
try { await setTimeout(delayMs, undefined, { signal }); /* retry */ } catch { /* abort → fail */ }— with a signal, the promise rejects only with the abort error, and a pre-aborted signal rejects immediately; behavior is identical, including timer clearing on abort. The emptycatchnames the abort rejection per the repo's empty-catch rule. - workflow-worker-thread:
setTimeout(ms, undefined, { ref: false })— exact semantics including not holding the event loop open. - terminal-bash:
import { setTimeout as delay } from 'node:timers/promises'— identical signature, call sites unchanged.
No dedicated tests pin the helpers themselves; the packages' behavior suites keep passing.
Alternatives considered
p-timeout/p-deferstyle packages. Rejected: the builtin covers both call sites exactly; an external package for a one-line await is negative-net.- Leave them. Rejected only weakly — the cost is small, but the repo already uses the builtin idiom elsewhere, and two hand-rolled variants of a builtin invite a third.
Acceptance criteria
- None of the three packages defines a promise-wrapped
setTimeouthelper; all import fromnode:timers/promises. - The
llm-retry,workflow-worker-thread, andterminal-bashtest suites pass unchanged (behavioral parity).
Risks
Essentially none: no model-visible output, no platform concerns, no new dependency. The llm-retry rewrite changes a boolean-returning helper into try/catch control flow — a local readability judgment the implementing PR makes.
中文
问题
三个包手写了用 promise 包装的定时器,而 node:timers/promises 内置模块早已提供同等能力;其他包(dsh-llm-mock-server 的 pause()、dsh-lsp-stdio、dsh-acp-snapshot)已经在使用该内置模块,因此这些手写副本同时也是一处一致性缺口:
packages/llm/llm-retry/src/index.ts的cancellableDelay()(约 14 行):new Promise+setTimeout+ 手动添加和移除中止监听器,定时器触发时 resolve 为true、被中止时 resolve 为false,仅在退避等待处消费一次。packages/workflow/workflow-worker-thread/src/host.ts的sleep()(约 7 行):promise 包装、已 unref 的setTimeout,用作 dispose(资源释放)宽限的时间上界。packages/terminal/terminal-bash/src/session.ts的delay()(约 4 行):朴素的 promise 包装setTimeout,用于轮询与拆卸等待。
提案
用 import { setTimeout } from 'node:timers/promises' 替换这三处实现:
- llm-retry:
try { await setTimeout(delayMs, undefined, { signal }); /* retry */ } catch { /* abort → fail */ }。传入 signal 后,该 promise 只会因中止错误而拒绝,已提前中止的 signal 则立即拒绝;行为完全一致,包括中止时清除定时器。按仓库的空 catch 规则,这个空catch注明其吞下的是 abort 拒绝。 - workflow-worker-thread:
setTimeout(ms, undefined, { ref: false }),语义完全等价,包括不会让事件循环保持存活。 - terminal-bash:
import { setTimeout as delay } from 'node:timers/promises',签名完全相同,调用点无需改动。
没有专属测试固定这些辅助函数本身;各包的行为测试套件继续通过。
曾考虑的替代方案
p-timeout/p-defer一类的包。 不予采纳:内置模块恰好精确覆盖这些调用点;为一行 await 引入外部包是负收益。- 维持现状。 不予采纳,但理由较弱:成本确实很小,但仓库其他地方已经在用这一内置惯用法,而同一内置能力存在两个手写变体,就会招来第三个。
验收标准
- 这三个包都不再各自定义 promise 包装的
setTimeout辅助函数,而是都从node:timers/promises导入。 llm-retry、workflow-worker-thread与terminal-bash的测试套件原样通过(行为等价)。
风险
基本没有风险:不涉及模型可见的输出,没有平台顾虑,也不新增依赖。llm-retry 的改写把一个返回布尔值的辅助函数变成 try/catch 控制流,这是一项局部可读性判断,由实施 PR(Pull Request)裁量。