Files
deepseek-harness/packages/subprocess/subprocess-local/README.zh.md
T
Tianyi Cui f81fcccd93 refactor(subprocess): keep terminate() as the seam's only termination verb
Delete kill(signal?) from SubprocessHandle: consumers stop a process only
through terminate()'s tree-scoped SIGTERM→graceMs→SIGKILL escalation
(idempotent, also driven by the spec's abort signal, a no-op once the tree
is gone). The single-signal verb had exactly one consumer family —
lsp-local — and what it bought there was a private re-implementation of
the same escalation. The internal kill closure stays in spawn.ts as the
dispose ladder's tier primitive; terminate() now routes through it too.

lsp-local collapses onto the seam's escalation:
- LspConnection replaces its terminate()/kill() pair with one terminate()
  that delegates to handle.terminate(). Behavior change: the
  framing-failure path terminates instead of instant SIGKILL, so a
  misbehaving server now gets SIGTERM plus the killGraceMs window to
  flush before SIGKILL.
- ConnectionSpec.pipeDrainGraceMs becomes killGraceMs: one grace, the
  spawn spec's graceMs, drives both the escalation window and post-exit
  pipe draining (the provider already passed killGraceMs for it).
- LspInstance.forceTerminate() drops its hand-rolled bounded first wait
  (LSP_KILL_GRACE) and escalateProcessTree (deleted with its export and
  unit test): the seam's escalation already commits to SIGKILL after
  killGraceMs, so only the unbounded quiescence awaits stay load-bearing.

Tests: kill()-shaped spawn specs become terminate()-shaped or fold into
the terminate() suites (group-wide delivery; the settled no-op case was
already pinned by 'terminate() after the tree died'); tree-survivor
coverage is intact. A stderr-'inherit' disposition test completes the
stdout/stderr symmetry so the scoped subprocess+lsp coverage gate stands
alone instead of leaning on subagent-acp's cross-package runs.

Docs: SubprocessHandle type-equiv block, seam/impl/group READMEs, and the
consumer-migration Agent Note lose the kill(signal?) vocabulary (zh pairs
re-recorded); cordis api/services catalogs regenerated.
2026-07-27 04:41:04 +08:00

4.9 KiB
Raw Blame History

@deepseek-ai/dsh-subprocess-local

English | 中文

@deepseek-ai/dsh-subprocess seam 的本地实现:LocalSubprocessService 把每个 spec 的 argv 作为 detached 进程树 spawn,依照 spec 中按流划分的 stdio 处置方式(disposition)完成接线(原始管道、inherit、附带可选 spill 文件的有界尾部保留收集),并以进程树为范围、按 SIGTERM→SIGKILL 升级发送信号。该实现没有任何配置:每项处置方式、限制与目录都随 spawn spec 到达,因此随部署变化的旋钮留在各调用方 seam 的配置里(dsh-bash-localdsh-lsp-localdsh-subagent-acp)。

行为(以及设计来源)

  • 带平台正确信号发送的 detached 进程树POSIX 子进程使用 detached spawn(拥有独立进程组),信号以负 pgid 发送并以直接子进程作为回退;Windows 通过 taskkill /PID <pid> /T /F 终止进程树(可为测试注入)。terminate()(句柄唯一的终止动词)先发送 SIGTERM,经过 spec 的宽限期后再发送 SIGKILL(沿用 OpenCode 的升级策略;管道与子 shell 会随父进程一起结束),进程树消亡后为空操作;dispose(graces) 以调用方提供的时间窗运行 stdin EOF→SIGTERM→SIGKILL 阶梯,dispose(资源释放)按句柄 memoize 化、只执行一次。组长进程退出后,仍然打开的管道也只获得同样有界的排空宽限期,因此存活的后代进程无法无限期地拖住结果不结算。系统会容忍 ESRCH;脱离该组重新挂载的 daemon 仍可能存活,这与调研工具的局限相同。
  • 按流划分的处置方式'pipe' 把原始流原样交给调用方(协议分帧仍归消费方所有);'inherit' 直通父进程的描述符;收集模式(collect)在输出超过上限后于内存中保留尾部(错误与结果通常聚集在末尾,沿用 pi/OpenCode 的理由),并在配置了 spill 上限时把完整流追加到一个私有临时文件;省略 spill 则只保留尾部,即诊断尾部的形状。某条流大于 spill 上限时,会丢弃已不完整的 spill,仅返回带截断标记的尾部;spill 文件描述符在结算时封存,最终关闭失败时则不公布路径,以免声称存在不完整的文件。spill 文件权限为 0600、名称随机,位于按需延迟创建的 0700 每进程目录之下。
  • 凭据清除 + 受管 DSH_* 合并:以 process.env 为基础,移除形似凭据的变量(*KEY**SECRET**TOKEN*)和所有环境中已有的 DSH_* 名称;spec 的普通 env 在清除后合并,但会拒绝 DSH_*;受管 dshEnv 会拒绝普通名称并最后合并,防止陈旧的嵌套 harness 身份。提供的 stdin 会被写入后关闭;否则 fd 0 指向 /dev/null。参见 stdin/env Agent Noteagent 决策记录)受管环境 Agent Note
  • 基于偏移量的读取:收集模式的读取器以全流字节坐标返回增量;服务自身从不持有游标,因此消费方自有的游标(bash 的后台读取路径)与完整流重读可以共存,结算前后皆然。
  • 先终止再等待退出的 dispose:服务保留存活句柄,只为让自身的 dispose 能对每个仍在运行的进程树执行升级并等待其退出;已结算与 spawn 失败的句柄在结算时即离开存活集合。

模型体验

通过消费方 seam 间接影响(目前是 dsh-tool-bash 背后的 bash 执行器家族);进程输出与生命周期面向模型的全部渲染归消费方所有。

KV Cache 影响

不会直接失效;请求前缀变更由具名消费方负责。

已知限制与暂缓事项

  • Windows 进程树支持仅为尽力而为,且未经 CI 测试:终止经由 taskkill /PID <pid> /T /F 完成,所有结果都被就地吸收,不向外抛出(进程树已不存在、竞态、二进制缺失),存活探测则回退到直接子进程边界;测试套件只通过注入的运行器覆盖这条路由,且 packages/subprocess/* 被排除在 Windows 测试矩阵之外。
  • 凭据清除依赖名称启发式规则:只匹配 *KEY**SECRET**TOKEN*;名称不同的 secret(例如 *PASSWORD*)会继续传递,对误删变量引入白名单属于已记录的后续工作。
  • 不会删除已完成的 spill 文件:有界的完整输出恢复文件(以及每个进程的私有 spill 目录)会在 OS tmpdir 下累积,直到外部机制进行清理;超大的不完整 spill 会被丢弃并立即尝试删除,但清理失败可能留下一个有界文件。

原始进程处理位于 src/spawn.tssrc/index.ts 负责服务接线。