@deepseek-ai/dsh-subprocess-local 0.1.5-rc.2 → 0.1.6-alpha.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/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/subprocess/subprocess-local/README.md
5
- README.md: 63d4d5d403d3559778dcad76a8c96cf677f1a977
6
- README.zh.md: f0a8b629b2ed17b6a6e9385a6eb565443c21ab26
5
+ README.md: 5379aff31cdcf036614a6664ce59330d319bd459
6
+ README.zh.md: f61596ac94b7f1270275cb93593c90a281c9ca90
package/README.md CHANGED
@@ -29,7 +29,7 @@ Mount the provider beside its consumers and start processes exactly as the subpr
29
29
 
30
30
  ### Mounting the provider
31
31
 
32
- Load the provider in the same composition as its consumers. It has no config fields: every choice arrives on the spawn request, so deployment-varying decisions stay with the caller's configuration.
32
+ Load the provider in the same composition as its consumers. It has no config fields: every choice arrives on the spawn request, so deployment-varying decisions stay with the caller's configuration. `terminalEnvironment()` reads a nonempty `SHELL` on POSIX, falling back to the account login shell, or a nonempty `ComSpec` on Windows. Empty values are omitted so the consumer can choose its platform fallback.
33
33
 
34
34
  ```yaml
35
35
  - name: '@deepseek-ai/dsh-subprocess-local'
@@ -44,6 +44,12 @@ Absolute executable paths are verified; bare names resolve against the scrubbed
44
44
 
45
45
  Collect mode keeps the last `maxBytes` of a stream in memory — errors and final results cluster at the end — and, when a `spill` cap is configured, appends the complete stream to a private file under a per-process directory in the OS temp dir (a `0700` directory, `0600` random-named files). A stream larger than the spill cap discards its incomplete spill and returns only the marked truncated tail. Reads are offset-based and non-consuming, so background and batch readers coexist before and after exit.
46
46
 
47
+ The `./output` export shares this collector and retained-spill storage with process adapters. `snapshot()` returns the retained raw bytes and total byte count, allowing remote adapters to preserve offsets without forwarding the complete stream.
48
+
49
+ ### Control transport
50
+
51
+ An ordinary spawn can request the [subprocess control pipe](../subprocess/README.md#using-a-control-pipe). A Node target receives fd 7 on every supported host; Windows descriptor numbering requires CRT initialization. POSIX runners preserve that descriptor across `execve`; Windows Job and ACL runners establish it in the child's CRT startup table before Node initializes and close their own carrier copies after spawning. Standard streams and the runner's private management channel remain independent.
52
+
47
53
  ### Running terminal sessions
48
54
 
49
55
  `spawnTerminal` allocates a real PTY and bridges UTF-8 text; you can inspect and signal the current foreground process group and await one `terminate()` operation. On supported Linux hosts, the original terminal argv runs directly inside a user-systemd scope, preserving the node-pty PID, session leader, controlling terminal, foreground `inputWaiting`, and readiness while the scope owns reparented or `setsid` descendants. On fallback hosts, cleanup retains exact identities from the rooted tree and observable session but cannot recover every escaped descendant. An exact Linux input wait requires a foreground thread whose fd 0 identifies the shell's controlling terminal and whose current syscall waits on that fd; if the kernel denies the syscall probe, the higher PTY backend uses its idle inference instead. On Windows, SIGINT is delivered as a Ctrl-C input write, SIGTSTP and SIGHUP are unsupported, and teardown verifies the shell's termination through the process table because an externally killed shell may never fire the PTY exit notification.
@@ -52,6 +58,8 @@ Collect mode keeps the last `maxBytes` of a stream in memory — errors and fina
52
58
 
53
59
  Normal disposal terminates every running managed range and terminal session and awaits quiescence. During a JavaScript-observable host exit — direct `process.exit()`, default uncaught exceptions, default unhandled rejections — synchronous finalization asks a Linux scope to kill its members, kills each Windows runner so its sole Job handle closes, and uses the existing PGID, `taskkill`, or captured-identity operation for fallbacks. It creates no promises or timers and does not claim quiescence. The same exit removes the private per-process spill directory when it holds no completed spill file; completed spill files remain as full-output recovery artifacts until an external cleanup. Unhandled `SIGTERM`/`SIGINT`/`SIGHUP`, `SIGKILL`, fatal OOM, native crashes, and power loss need an external supervisor.
54
60
 
61
+ Linux ordinary and terminal cancellation preserves the observed termination signal even before the bootstrap consumes its launch request. An unconsumed request still reports startup failure when no matching termination was requested; a recorded pre-exec error always takes precedence. `waitForExit()` independently proves the scope empty, including a scope the manager leaves active with no processes after a payload dies before it enters that scope's cgroup. State queries interrupted by a termination signal are repeated before deciding whether cleanup succeeded. After termination, a consumed launch request and zero scope processes prove quiescence even before the direct-process exit notification. If the final scope signal fails, an accepted direct `SIGKILL` or verified direct-process absence permits one wait for the pending direct-process settlement before rechecking an active range not yet proven empty. This wait is independent of output draining and whole-range settlement. The fresh scope observation must prove the range empty; surviving processes or an unknown process count retain the signal failure.
62
+
55
63
  ### What can go wrong
56
64
 
57
65
  An executable that cannot be resolved fails loud with a stable error. `done` rejects when spawn or provider failure prevents a direct outcome, and that rejection does not prove whether target execution began. `waitForExit()` rejects if the selected owner can no longer prove its range empty, and cleanup still attempts termination. A read past the retained tail is `lossy` and points at the spill file when one exists. A fallback process group or observed terminal session can miss a descendant that escapes before observation — see the limitations below.
@@ -90,7 +98,7 @@ Each spawn selects one owner for both signalling and quiescence. Supported Linux
90
98
 
91
99
  ### Main flow
92
100
 
93
- A spawn synchronously validates the final argv, cwd, and environment, selects containment before the user command can run, and returns a handle while target identity remains private. Linux ordinary and terminal launches use a private one-shot request whose scoped bootstrap restores the target cwd and environment, resolves the executable, clears close-on-exec on fd 0 through fd 2, and enters libc `execve()` with the original argv. Windows ordinary launches isolate runner fd 0 through fd 2, reserve fd 3 for IPC, and carry target stdio on fd 4 through fd 6; the runner resolves those CRT descriptors to OS handles, creates the target suspended, assigns it to the Job, resumes it, and closes only the carrier descriptors. `done` settles the direct command after its stdio barrier, while `waitForExit()` separately waits for the selected scope, Job, process group, or observed session to become empty.
101
+ A spawn synchronously validates the final argv, cwd, and environment, selects containment before the user command can run, and returns a handle while target identity remains private. Linux ordinary and terminal launches use a private one-shot request whose scoped bootstrap restores the target cwd and environment, resolves the executable, clears close-on-exec on fd 0 through fd 2 and optional control fd 7, and enters libc `execve()` with the original argv. Windows ordinary launches isolate runner fd 0 through fd 2, reserve fd 3 for IPC, and carry target stdio on fd 4 through fd 6 plus fd 7 when control is requested; the runner resolves those CRT descriptors to OS handles, creates the target suspended, assigns it to the Job, resumes it, and closes its standard-stream carriers and optional fd-7 carrier. `done` settles the direct command after its stdio barrier, while `waitForExit()` separately waits for the selected scope, Job, process group, or observed session to become empty.
94
102
 
95
103
  ### Safety invariants
96
104
 
@@ -113,6 +121,8 @@ Read these pages when the provider-level contract is not enough. They move from
113
121
 
114
122
  -----
115
123
 
124
+ Terminal allocation advertises the caller-provided `terminalType` through TERM and node-pty. Dynamic resize updates the existing PTY. Output backpressure pauses native reads until the consumer drains; explicit termination resumes a paused reader to receive the exit notification.
125
+
116
126
  <a id="model-experience"></a>
117
127
  ## Model Experience
118
128
 
@@ -129,6 +139,7 @@ No direct invalidation; the named consumers own any request-prefix changes.
129
139
 
130
140
  These limits define when the provider is a poor fit or needs special operational care. They are current package constraints, not a general platform comparison or a task backlog.
131
141
 
142
+ - **Linux direct exit has no independent deadline** — after direct `SIGKILL` is acknowledged, uninterruptible kernel I/O can keep disposal pending indefinitely; `graceMs` and scope-query polling budgets do not bound this wait.
132
143
  - **Native ownership has explicit host requirements** — Linux needs a readable user manager and `systemd-run --expand-environment=no`; older systemd versions use the warned PGID fallback. macOS always uses that fallback because no supported public persistent owner exists.
133
144
  - **Native selection has bounded per-spawn costs** — Linux repeats the bootstrap entry, libc `execve`/`fcntl` bindings, live user manager, and literal-argv scope probe until it first succeeds; later eligible ordinary or terminal spawns recheck only the live user manager. Windows rechecks the runner entry, bindings, and current Job support before every ordinary spawn. Successful Linux deep-probe state and fallback-warning de-duplication persist for the provider lifetime. All probes finish before the user command can run, and child-process probes have a 5-second timeout. Each Linux launch creates a private request directory, checks unresolved scope establishment every 50 milliseconds, then exponentially backs off an established active scope to at most 5 seconds between queries; a Windows ordinary launch keeps one runner and IPC channel until the Job reports zero active processes. Target standard handles are inherited directly, with no named-pipe stdio or result files.
134
145
  - **Windows Job inheritance has defined exclusions** — ordinary descendants inherit the Job by default, but breakaway processes are outside the guarantee. The target starts only after Job assignment; external termination of the runner in the narrow create-to-assignment interval can leave a suspended target behind.
package/README.zh.md CHANGED
@@ -29,7 +29,7 @@ kind: "package-reference"
29
29
 
30
30
  ### 挂载提供方
31
31
 
32
- 在与消费方相同的组合中加载本提供方。它没有任何配置字段:每项选择都随 spawn 请求到达,因此随部署变化的决策留在调用方的配置里。
32
+ 在与消费方相同的组合中加载本提供方。它没有任何配置字段:每项选择都随 spawn 请求到达,因此随部署变化的决策留在调用方的配置里。 `terminalEnvironment()` 在 POSIX 读取非空的 `SHELL`,缺失时使用账户登录 shell;在 Windows 读取非空的 `ComSpec`。空值会被省略,由消费者选择平台回退。
33
33
 
34
34
  ```yaml
35
35
  - name: '@deepseek-ai/dsh-subprocess-local'
@@ -44,17 +44,25 @@ kind: "package-reference"
44
44
 
45
45
  收集模式在内存中保留一条流的最后 `maxBytes`——错误与最终结果通常聚集在末尾——并在配置了 `spill` 上限时把完整流追加到 OS 临时目录下每进程目录中的私有文件(`0700` 目录、`0600` 随机命名文件)。某条流大于 spill 上限时,会丢弃不完整的 spill,只返回带截断标记的尾部。读取基于偏移量且从不消费,因此后台读取与批量读取在退出前后都可以共存。
46
46
 
47
+ `./output` 导出向进程适配器共享该收集器与保留 spill 的存储。`snapshot()` 返回保留的原始字节及总字节数,使远程适配器能够保留偏移量,而无需转发完整的流。
48
+
49
+ ### 控制传输
50
+
51
+ 普通 spawn 可以请求 [subprocess 控制管道](../subprocess/README.zh.md#using-a-control-pipe)。Node 目标在所有受支持的宿主上均收到 fd 7;Windows 描述符编号依赖 CRT 初始化。POSIX runner 在 `execve` 时保留该描述符;Windows Job 和 ACL runner 在 Node 初始化前通过子进程的 CRT 启动表建立它,并在 spawn 后关闭自身的承载副本。标准流与 runner 的私有管理通道保持独立。
52
+
47
53
  ### 运行终端会话
48
54
 
49
- `spawnTerminal` 分配真实 PTY 并桥接 UTF-8 文本;你可以检查当前前台进程组并向其发送信号,还可以等待一次 `terminate()` 操作。在受支持的 Linux 宿主上,原始终端 argv 直接在 user-systemd scope 内运行;node-pty PID、session leader、控制终端、前台 `inputWaiting` 与就绪状态保持不变,而 scope 会拥有已重新设定父进程或调用 `setsid` 的后代。在 fallback 宿主上,清理会保留根进程树和可观察 session 中的精确身份,但无法重新发现每个已经逃逸的后代。Linux 的精确输入等待要求前台线程的 fd 0 标识 shell 的控制终端,且线程当前的 syscall 正在等待该 fd;如果内核拒绝 syscall 探测,上层 PTY 后端会改用空闲推断。在 Windows 上,SIGINT 以 Ctrl-C 输入写入投递,SIGTSTP 与 SIGHUP 不受支持,拆卸会通过进程表验证 shell 已终止,因为被外部终止的 shell 可能永远不会触发 PTY 退出通知。
55
+ `spawnTerminal` 分配真实 PTY 并桥接 UTF-8 文本;你可以检查当前前台进程组并向其发送信号,还可以等待一次 `terminate()` 操作。在受支持的 Linux 宿主上,原始终端 argv 直接在 user-systemd scope 内运行;node-pty PID、会话 leader、控制终端、前台 `inputWaiting` 与就绪状态保持不变,而 scope 会拥有已重新设定父进程或调用 `setsid` 的后代。在 fallback 宿主上,清理会保留根进程树和可观察会话中的精确身份,但无法重新发现每个已经逃逸的后代。Linux 的精确输入等待要求前台线程的 fd 0 标识 shell 的控制终端,且线程当前的 syscall 正在等待该 fd;如果内核拒绝 syscall 探测,上层 PTY 后端会改用空闲推断。在 Windows 上,SIGINT 以 Ctrl-C 输入写入投递,SIGTSTP 与 SIGHUP 不受支持,拆卸会通过进程表验证 shell 已终止,因为被外部终止的 shell 可能永远不会触发 PTY 退出通知。
50
56
 
51
57
  ### 关闭行为
52
58
 
53
59
  正常 dispose 会终止每个仍在运行的受管范围与终端会话并等待其完全停稳。在 JavaScript 可观察的宿主退出期间——直接 `process.exit()`、默认未捕获异常、默认未处理 rejection——同步最终清理会请求 Linux scope 终止其成员,同步终止每个 Windows runner 以关闭其唯一 Job handle,并为 fallback 使用既有 PGID、`taskkill` 或已捕获身份操作。它不创建 Promise 或定时器,也不声称已经完全停稳。同一退出阶段会删除未持有任何已完成 spill 文件的每进程私有 spill 目录;已完成的 spill 文件作为完整输出恢复产物保留,直到外部机制清理。未处理的 `SIGTERM`/`SIGINT`/`SIGHUP`、`SIGKILL`、fatal OOM、native crash 与断电需要外部 supervisor。
54
60
 
61
+ Linux 普通进程和终端进程即使在 bootstrap 消费启动请求前被取消,也会保留实际观察到的终止信号。如果没有请求对应的终止信号,未消费的请求仍会报启动失败;已记录的 pre-exec 错误始终优先。`waitForExit()` 独立证明 scope 已为空,其中也包括 payload 在进入该 scope 的 cgroup 前就被杀死、manager 因此让它保持 active 却没有任何进程的 scope。状态查询期间若发出终止信号,会重新查询后再判定清理是否成功。请求终止后,启动请求已消费且 scope 进程数为零即可证明完全停稳,无需等待直接进程的退出通知。如果最终 scope 信号发送失败,直接进程接受 `SIGKILL` 或被独立确认已不存在,owner 才可以等待一次尚未送达的直接进程停稳通知,再重新查询尚未证明为空的 active 范围。此等待独立于输出排空和整个范围的停稳。新取得的 scope 状态必须证明范围已为空;仍有进程存活或进程数未知时,仍报告信号失败。
62
+
55
63
  ### 可能出错的地方
56
64
 
57
- 无法解析的可执行文件会以稳定错误快速失败。当 spawn 或 provider failure 使 direct outcome 无法产生时,`done` 会 reject;该 rejection 不能证明 target 是否已经开始执行。若所选 owner 无法再证明其范围为空,`waitForExit()` 会 reject,清理仍会尝试终止。越过保留尾部的读取是 `lossy` 的,并在 spill 文件存在时指向它。fallback 进程组或已观察终端 session 可能遗漏在观察前逃逸的后代——见下文限制。
65
+ 无法解析的可执行文件会明确报出稳定错误。当 spawn 或提供方故障使 direct outcome 无法产生时,`done` 会 reject;该 rejection 不能证明 target 是否已经开始执行。若所选 owner 无法再证明其范围为空,`waitForExit()` 会 reject,清理仍会尝试终止。越过保留尾部的读取是 `lossy` 的,并在 spill 文件存在时指向它。fallback 进程组或已观察终端 session 可能遗漏在观察前逃逸的后代——见下文限制。
58
66
 
59
67
  -----
60
68
 
@@ -68,7 +76,7 @@ kind: "package-reference"
68
76
 
69
77
  ### 设计理念
70
78
 
71
- 每次 spawn 都为信号发送与完全停稳选择同一个 owner。受支持的 Linux 普通命令与终端启动使用临时 user-systemd scope,受支持的 Windows 普通命令使用由 helper 持有、关闭时终止成员的 Job。macOS、旧版或不可用的 user-systemd,以及不可用的 Windows 原生支持使用既有 detached 进程组、`taskkill` 或终端 session 观察,并只告警一次。native 路径可能已经启动命令后,本提供方绝不会通过 fallback 重放该命令。
79
+ 每次 spawn 都为信号发送与完全停稳选择同一个 owner。受支持的 Linux 普通命令与终端启动使用临时 user-systemd scope,受支持的 Windows 普通命令使用由 helper 持有、关闭时终止成员的 Job。macOS、旧版或不可用的 user-systemd,以及不可用的 Windows 原生支持使用既有 detached 进程组、`taskkill` 或终端会话观察,并只告警一次。native 路径可能已经启动命令后,本提供方绝不会通过 fallback 重放该命令。
72
80
 
73
81
  ### 源码地图
74
82
 
@@ -82,15 +90,15 @@ kind: "package-reference"
82
90
  | [`src/windows-job.ts`](src/windows-job.ts) | Windows Job 能力检查与 helper 启动 |
83
91
  | [`src/runner-launch.ts`](src/runner-launch.ts) | source、built 与 packaged 私有 runner 选择 |
84
92
  | [`src/spawn-runner.ts`](src/spawn-runner.ts) | Linux 一次性 exec bootstrap 与 Windows Job runner |
85
- | [`src/runner-protocol.ts`](src/runner-protocol.ts) | 严格的 Linux 启动/错误文件与 Windows IPC 消息 |
93
+ | [`src/runner-protocol.ts`](src/runner-protocol.ts) | 严格定义的 Linux launch/startup 文件与 Windows IPC 消息 |
86
94
  | [`src/terminal.ts`](src/terminal.ts) | `node-pty` 终端句柄:Linux scope 绑定、前台检查与 fallback 清理 |
87
95
  | [`src/process-inspector.ts`](src/process-inspector.ts) | POSIX 进程树与会话检查 |
88
96
  | [`src/windows-inspector.ts`](src/windows-inspector.ts) | 经 koffi 的 Windows Toolhelp32 进程表检查 |
89
- | — | 不发布运行时不变式伴生入口;约定归 seam 所有。 |
97
+ | — | 不发布运行时不变式伴生入口;除所属 seam 强制执行的约定外,本包不公开独立的事件序列或可变数据关系。 |
90
98
 
91
99
  ### 主流程
92
100
 
93
- 一次 spawn 会同步校验最终 argv、cwd 与环境,在用户命令可能运行前选择 containment,并在目标身份保持私有的情况下返回句柄。Linux 普通命令与终端启动使用私有的一次性请求;scope 内的 bootstrap 会恢复目标 cwd 与环境、解析可执行文件、清除 fd 0 至 fd 2 的 close-on-exec 标记,再以原始 argv 进入 libc `execve()`。Windows 普通命令会隔离 runner 的 fd 0 至 fd 2、把 fd 3 留给 IPC,并用 fd 4 至 fd 6 承载 target stdio;runner 把这些 CRT 描述符解析成 OS handle,以 suspended 状态创建 target,将其加入 Job、恢复运行,再只关闭 carrier 描述符。`done` 会在 direct command 及其 stdio 屏障结算后完成,`waitForExit()` 则分别等待所选 scope、Job、进程组或已观察 session 变空。
101
+ 一次 spawn 会同步校验最终 argv、cwd 与环境,在用户命令可能运行前选择 containment,并在目标身份保持私有的情况下返回句柄。Linux 普通命令与终端启动使用私有的一次性请求;scope 内的 bootstrap 会恢复目标 cwd 与环境、解析可执行文件、清除 fd 0 至 fd 2 及可选控制 fd 7 的 close-on-exec 标记,再以原始 argv 进入 libc `execve()`。Windows 普通命令会隔离 runner 的 fd 0 至 fd 2、把 fd 3 留给 IPC,并用 fd 4 至 fd 6 承载 target stdio,在请求控制时还使用 fd 7;runner 把这些 CRT 描述符解析成 OS handle,以 suspended 状态创建 target,将其加入 Job、恢复运行,再关闭自身的标准流载体及可选 fd-7 载体。`done` 会在 direct command 及其 stdio 屏障结算后完成,`waitForExit()` 则分别等待所选 scope、Job、进程组或已观察会话变空。
94
102
 
95
103
  ### 安全不变式
96
104
 
@@ -113,6 +121,8 @@ spill 文件以 `0600` 权限、`O_EXCL` 与随机名称在 `0700` 每进程目
113
121
 
114
122
  -----
115
123
 
124
+ 终端分配通过 TERM 和 node-pty 使用调用者指定的 `terminalType`。动态 resize 更新已有 PTY。输出背压会暂停原生读取,待消费者排空后恢复;显式终止会恢复暂停的读取,以接收退出通知。
125
+
116
126
  <a id="model-experience"></a>
117
127
  ## 模型体验
118
128
 
@@ -129,11 +139,12 @@ spill 文件以 `0600` 权限、`O_EXCL` 与随机名称在 `0700` 每进程目
129
139
 
130
140
  这些限制说明本提供方何时不合适,或何时需要特别的运维注意。它们是当前包约束,不是通用平台对比或任务积压。
131
141
 
142
+ - **Linux 直接进程退出没有独立截止时间**——直接 `SIGKILL` 获得确认后,不可中断的内核 I/O 可能让 dispose 无限期等待;`graceMs` 和 scope 查询的轮询预算不限制此等待。
132
143
  - **native ownership 有明确宿主要求**——Linux 需要可读的 user manager 与 `systemd-run --expand-environment=no`;旧版 systemd 使用带告警的 PGID fallback。macOS 因没有受支持的公开 persistent owner,始终使用该 fallback。
133
- - **native 选择具有有界的每次 spawn 成本**——Linux 会重复检查 bootstrap 入口、libc `execve`/`fcntl` bindings、存活的 user manager 与 literal-argv scope 支持,直到这套完整探测首次成功;后续符合条件的普通命令或终端 spawn 只重新检查存活的 user manager。Windows 会在每次普通 spawn 前重新检查 runner 入口、bindings 与当前 Job 支持。Linux 深度探测的成功状态与 fallback 告警去重会在 provider 生命周期内持续保留。所有探测都会在用户命令可能运行前完成,子进程探测的超时为 5 秒。每次 Linux 启动都会创建私有请求目录,以 50 毫秒间隔检查尚未确定的 scope 建立状态;scope 已建立且仍 active 后,查询间隔按指数增长,最多为 5 秒。Windows 普通命令会保留一个 runner 与一条 IPC 通道,直到 Job 报告活动进程数为零。目标会直接继承标准句柄,不使用 named-pipe stdio 或结果文件。
144
+ - **native 选择具有有界的每次 spawn 成本**——Linux 会重复检查 bootstrap 入口、libc `execve`/`fcntl` bindings、存活的 user manager 与 literal-argv scope 支持,直到这套完整探测首次成功;后续符合条件的普通命令或终端 spawn 只重新检查存活的 user manager。Windows 会在每次普通 spawn 前重新检查 runner 入口、bindings 与当前 Job 支持。Linux 深度探测的成功状态与 fallback 告警去重会在提供方生命周期内持续保留。所有探测都会在用户命令可能运行前完成,子进程探测的超时为 5 秒。每次 Linux 启动都会创建私有请求目录,以 50 毫秒间隔检查尚未确定的 scope 建立状态;scope 已建立且仍 active 后,查询间隔按指数增长,最多为 5 秒。Windows 普通命令会保留一个 runner 与一条 IPC 通道,直到 Job 报告活动进程数为零。目标会直接继承标准句柄,不使用 named-pipe stdio 或结果文件。
134
145
  - **Windows Job inheritance 有明确排除项**——普通后代默认继承 Job,但 breakaway 进程不在保证范围。目标只在 Job 分配后启动;runner 若在 create-to-assignment 极窄区间遭外力终止,可能留下 suspended target。
135
146
  - **Windows 终端信号是控制台级的**——SIGINT 以 `\x03` Ctrl-C 输入写入投递,由 conhost 转为控制台级 CTRL_C 事件;SIGTSTP 与 SIGHUP 被拒绝(不可用);不带 `/F` 的 `taskkill` 无法终止控制台进程,因此拆卸的 TERM 档是 `/F` 升级前的宽限等待。Windows 就绪没有精确的 stdin-wait 档:prompt-marker 快路径把 shell pid 作为伪前台进程组比较,其余由静默与计时档覆盖。
136
- - **fallback 终端 ownership 仍依赖观察**——在 macOS 或缺少可用 user-systemd 的 Linux 上,子进程如果在任何前台检查快照之前重新设定父进程,或离开自有终端 session,就可能逃出进程表扫描。本地提供方不会新增持续进程表监视器;受支持的 Linux native 模式改由 scope membership 持有这些后代。
147
+ - **fallback 终端 ownership 仍依赖观察**——在 macOS 或缺少可用 user-systemd 的 Linux 上,子进程如果在任何前台检查快照之前重新设定父进程,或离开自有终端会话,就可能逃出进程表扫描。本地提供方不会新增持续进程表监视器;受支持的 Linux native 模式改由 scope membership 持有这些后代。
137
148
  - **进程内清理要求退出阶段仍能执行 JavaScript**——直接 `process.exit()`、默认未捕获异常和默认未处理 rejection 会发出 Node 同步 `exit` 事件。未安装 handler 时,`SIGTERM`、`SIGINT` 或 `SIGHUP` 的默认 OS 处置不会发出该事件;应用只有安装执行正常 dispose 或调用 `process.exit()` 的 handler 才能覆盖这些信号。`SIGKILL`、fatal OOM、`process.abort()`、native crash、断电,以及任何无法运行 JavaScript 的故障,都需要外部 supervisor、容器 init 或等价的 OS owner 负责。
138
149
  - **凭据清除依赖名称启发式规则**——只匹配 `*KEY*`/`*PASSWORD*`/`*SECRET*`/`*TOKEN*`;名称不同的 secret(例如 `*PASSPHRASE*`)会继续传递,对误删变量引入白名单属于已记录的后续工作。
139
150
  - **不会删除已完成的 spill 文件**——有界的完整输出恢复文件会在 OS tmpdir 下累积,直到外部机制进行清理;每进程私有 spill 目录仅在未持有任何已完成 spill 文件时于 JavaScript 可观察的退出阶段删除。