ソースを参照

fix(subprocess): keep shared handle semantics provider-neutral

pku-xht 1 ヶ月 前
親
コミット
bf5a8e42a3

+ 2 - 2
docs/subsystems/subprocess.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/subprocess.md
-subprocess.md: 410bf5f25bcb8d15e826d66a0be1e1625cf524ce
-subprocess.zh.md: 16b207db40adf22e4b4e345c566fc29e63c4240e
+subprocess.md: 240788ab2b7e3d996093073c086bbcc847b5eebd
+subprocess.zh.md: 73720cde04c7acc49dd1a734cdf71422ff401b44

+ 7 - 7
docs/subsystems/subprocess.md

@@ -131,19 +131,19 @@ interface SubprocessSpawnSpec {
 
 ## Handles: streams, readers, and managed-range termination
 
-A spawn returns a live handle synchronously after any provider-specific setup needed to publish its target pid. Collect-mode readers take whole-stream byte offsets and never consume, so independent readers cannot steal one another's deltas; piped streams belong to the caller. `terminate()` and `waitForExit()` use one managed range: supported local Linux and Windows providers use an OS-owned scope or Job, while weaker fallbacks are disclosed. The only termination verb escalates SIGTERM→grace→SIGKILL, so a consumer can build its own teardown ladder (the ACP backend's stdin-EOF-first `disposeAcpChild` is the template).
+A spawn returns a live handle synchronously; the provider may publish its process identity later. Collect-mode readers take whole-stream byte offsets and never consume, so independent readers cannot steal one another's deltas; piped streams belong to the caller. `terminate()` and `waitForExit()` use one provider-managed range whose identity, observation limits, and weaker fallbacks belong to the provider. The only termination verb escalates SIGTERM→grace→SIGKILL, so a consumer can build its own teardown ladder (the ACP backend's stdin-EOF-first `disposeAcpChild` is the template).
 
 ```ts type-equiv
 /**
- * A live direct child and its provider-managed process range. Collected output
+ * A live subprocess and its provider-managed process range. Collected output
  * remains readable after exit; piped streams belong to the caller.
  *
  * Termination and {@link SubprocessHandle.waitForExit} use the same managed
- * range. Supported Linux and Windows hosts use an OS-owned scope or Job;
- * weaker platform fallbacks are disclosed by the provider.
+ * range. Each provider documents the process identity and range it can
+ * observe.
  */
 interface SubprocessHandle {
-  /** Direct target process id; -1 when the spawn itself failed. */
+  /** Provider-published process identifier; -1 while unavailable or after startup fails. */
   readonly pid: number
   /** The child's stdin, present iff spawned with `stdin: 'pipe'`. */
   readonly stdin: Writable | undefined
@@ -282,9 +282,9 @@ Abstract subprocess service. Subclass, implement spawn, and load the subclass as
 Implementations must honor these semantics:
 
 - Executable paths belong to one execution world shared with the mounted filesystem provider.
-- spawn returns a live handle synchronously after provider-specific setup needed to publish its target pid. `done` resolves with direct-process exit facts and may reject for spawn or selected provider-runner failures.
+- spawn returns a live handle synchronously. Its pid is provider-owned and may remain unavailable during asynchronous startup. `done` resolves with the spawned command's exit facts and may reject for spawn or selected provider-runner failures.
 - Collect-mode readers are offset-based and non-consuming, so independent readers never consume one another's output; lossy reads report truncation and the spill file holding the complete stream when one exists. Piped streams are handed to the caller raw and never buffered here.
-- SubprocessHandle.terminate (and the spec's abort signal) escalates SIGTERM→grace→SIGKILL — the only termination verb — against the provider's managed range. Supported local Linux and Windows providers use an OS-owned scope or Job; weaker fallbacks use a detached process group or direct-parent tree. SubprocessHandle.waitForExit observes that same range so a consumer-owned teardown ladder can hold each tier on real quiescence.
+- SubprocessHandle.terminate (and the spec's abort signal) escalates SIGTERM→grace→SIGKILL — the only termination verb — against the provider's managed range. SubprocessHandle.waitForExit observes that same range so a consumer-owned teardown ladder can hold each tier on real quiescence; each provider documents its identity and observability limits.
 - Disposal of the service terminates all still-running managed processes and awaits their exit.
 - spawnTerminal owns terminal allocation, text transport, foreground groups, signalling, and whole-session quiescence behind one awaited termination method; readiness and persistent-shell policy stay in the PTY consumer. Its output stream ends after queued terminal output when the top-level process exits.
 

+ 7 - 7
docs/subsystems/subprocess.zh.md

@@ -131,19 +131,19 @@ interface SubprocessSpawnSpec {
 
 ## 句柄:流、读取器与 managed-range 终止
 
-spawn 会在完成发布 target pid 所需的 provider-specific setup 后同步返回活动句柄。收集模式的读取器接受全流字节偏移量且从不消费,因此独立读取器不会抢走彼此的增量;管道化的流归调用方所有。`terminate()` 与 `waitForExit()` 使用同一个 managed range:受支持的本地 Linux 与 Windows provider 使用 OS-owned scope 或 Job,并明确披露较弱 fallback。唯一的终止动词执行 SIGTERM→宽限期→SIGKILL 升级,因此消费方可以构建自己的分级清理流程;ACP 后端先关闭 stdin 的 `disposeAcpChild` 是参考实现。
+spawn 会同步返回活动句柄;provider 可以稍后发布其进程标识。收集模式的读取器接受全流字节偏移量且从不消费,因此独立读取器不会抢走彼此的增量;管道化的流归调用方所有。`terminate()` 与 `waitForExit()` 使用同一个 provider-managed range,其标识、观察限制与较弱 fallback 均归 provider 定义。唯一的终止动词执行 SIGTERM→宽限期→SIGKILL 升级,因此消费方可以构建自己的分级清理流程;ACP 后端先关闭 stdin 的 `disposeAcpChild` 是参考实现。
 
 ```ts type-equiv
 /**
- * A live direct child and its provider-managed process range. Collected output
+ * A live subprocess and its provider-managed process range. Collected output
  * remains readable after exit; piped streams belong to the caller.
  *
  * Termination and {@link SubprocessHandle.waitForExit} use the same managed
- * range. Supported Linux and Windows hosts use an OS-owned scope or Job;
- * weaker platform fallbacks are disclosed by the provider.
+ * range. Each provider documents the process identity and range it can
+ * observe.
  */
 interface SubprocessHandle {
-  /** Direct target process id; -1 when the spawn itself failed. */
+  /** Provider-published process identifier; -1 while unavailable or after startup fails. */
   readonly pid: number
   /** The child's stdin, present iff spawned with `stdin: 'pipe'`. */
   readonly stdin: Writable | undefined
@@ -282,9 +282,9 @@ Abstract subprocess service. Subclass, implement spawn, and load the subclass as
 Implementations must honor these semantics:
 
 - Executable paths belong to one execution world shared with the mounted filesystem provider.
-- spawn returns a live handle synchronously after provider-specific setup needed to publish its target pid. `done` resolves with direct-process exit facts and may reject for spawn or selected provider-runner failures.
+- spawn returns a live handle synchronously. Its pid is provider-owned and may remain unavailable during asynchronous startup. `done` resolves with the spawned command's exit facts and may reject for spawn or selected provider-runner failures.
 - Collect-mode readers are offset-based and non-consuming, so independent readers never consume one another's output; lossy reads report truncation and the spill file holding the complete stream when one exists. Piped streams are handed to the caller raw and never buffered here.
-- SubprocessHandle.terminate (and the spec's abort signal) escalates SIGTERM→grace→SIGKILL — the only termination verb — against the provider's managed range. Supported local Linux and Windows providers use an OS-owned scope or Job; weaker fallbacks use a detached process group or direct-parent tree. SubprocessHandle.waitForExit observes that same range so a consumer-owned teardown ladder can hold each tier on real quiescence.
+- SubprocessHandle.terminate (and the spec's abort signal) escalates SIGTERM→grace→SIGKILL — the only termination verb — against the provider's managed range. SubprocessHandle.waitForExit observes that same range so a consumer-owned teardown ladder can hold each tier on real quiescence; each provider documents its identity and observability limits.
 - Disposal of the service terminates all still-running managed processes and awaits their exit.
 - spawnTerminal owns terminal allocation, text transport, foreground groups, signalling, and whole-session quiescence behind one awaited termination method; readiness and persistent-shell policy stay in the PTY consumer. Its output stream ends after queued terminal output when the top-level process exits.
 

+ 1 - 1
packages/extensions/tool-cordis/src/api-catalog.ts

@@ -1866,7 +1866,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
   {
     key: 'subprocess',
     summary: 'Abstract subprocess service.',
-    description: 'Abstract subprocess service. Subclass, implement spawn, and load the subclass as a plugin — it registers as `ctx.subprocess` (one implementation per context; loading a second throws, which is cordis\' standard duplicate-service behavior).\n\nImplementations must honor these semantics:\n\n- Executable paths belong to one execution world shared with the mounted filesystem provider.\n- spawn returns a live handle synchronously after provider-specific setup needed to publish its target pid. `done` resolves with direct-process exit facts and may reject for spawn or selected provider-runner failures.\n- Collect-mode readers are offset-based and non-consuming, so independent readers never consume one another\'s output; lossy reads report truncation and the spill file holding the complete stream when one exists. Piped streams are handed to the caller raw and never buffered here.\n- SubprocessHandle.terminate (and the spec\'s abort signal) escalates SIGTERM→grace→SIGKILL — the only termination verb — against the provider\'s managed range. Supported local Linux and Windows providers use an OS-owned scope or Job; weaker fallbacks use a detached process group or direct-parent tree. SubprocessHandle.waitForExit observes that same range so a consumer-owned teardown ladder can hold each tier on real quiescence.\n- Disposal of the service terminates all still-running managed processes and awaits their exit.\n- spawnTerminal owns terminal allocation, text transport, foreground groups, signalling, and whole-session quiescence behind one awaited termination method; readiness and persistent-shell policy stay in the PTY consumer. Its output stream ends after queued terminal output when the top-level process exits.',
+    description: 'Abstract subprocess service. Subclass, implement spawn, and load the subclass as a plugin — it registers as `ctx.subprocess` (one implementation per context; loading a second throws, which is cordis\' standard duplicate-service behavior).\n\nImplementations must honor these semantics:\n\n- Executable paths belong to one execution world shared with the mounted filesystem provider.\n- spawn returns a live handle synchronously. Its pid is provider-owned and may remain unavailable during asynchronous startup. `done` resolves with the spawned command\'s exit facts and may reject for spawn or selected provider-runner failures.\n- Collect-mode readers are offset-based and non-consuming, so independent readers never consume one another\'s output; lossy reads report truncation and the spill file holding the complete stream when one exists. Piped streams are handed to the caller raw and never buffered here.\n- SubprocessHandle.terminate (and the spec\'s abort signal) escalates SIGTERM→grace→SIGKILL — the only termination verb — against the provider\'s managed range. SubprocessHandle.waitForExit observes that same range so a consumer-owned teardown ladder can hold each tier on real quiescence; each provider documents its identity and observability limits.\n- Disposal of the service terminates all still-running managed processes and awaits their exit.\n- spawnTerminal owns terminal allocation, text transport, foreground groups, signalling, and whole-session quiescence behind one awaited termination method; readiness and persistent-shell policy stay in the PTY consumer. Its output stream ends after queued terminal output when the top-level process exits.',
     methods: [
       {
         signature: 'abstract resolveExecutable( command: string, env?: Readonly<Record<string, string>>, signal?: AbortSignal, ): Promise<string>',

+ 8 - 8
packages/subprocess/subprocess-local/tests/native-windows.spec.ts

@@ -109,16 +109,16 @@ describe.skipIf(!windowsNative)('Windows Job native containment', () => {
       stdio: { stdin: 'ignore', stdout: 'pipe', stderr: 'inherit' } as const,
     }
     const handle = bindManagedProcess(request, launchWindowsJob(request))
+    if (handle.stdout === undefined) throw new Error('expected piped stdout')
+    const stdoutEnded = Promise.race([
+      new Promise<boolean>((resolve, reject) => {
+        handle.stdout?.once('end', () => { resolve(true) })
+        handle.stdout?.once('error', reject)
+      }),
+      new Promise<boolean>(resolve => setTimeout(() => { resolve(false) }, 1_000)),
+    ])
     const descendant = await waitForPid(pidFile)
     try {
-      if (handle.stdout === undefined) throw new Error('expected piped stdout')
-      const stdoutEnded = Promise.race([
-        new Promise<boolean>((resolve, reject) => {
-          handle.stdout?.once('end', () => { resolve(true) })
-          handle.stdout?.once('error', reject)
-        }),
-        new Promise<boolean>(resolve => setTimeout(() => { resolve(false) }, 1_000)),
-      ])
       await expect(handle.done).resolves.toEqual({ exitCode: 42, signal: null })
       await expect(stdoutEnded).resolves.toBe(true)
       expect(readFileSync(factsFile, 'utf8')).toBe(JSON.stringify({

+ 2 - 2
packages/subprocess/subprocess/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/subprocess/subprocess/README.md
-README.md: 174e9732d4e7fc2e2589d8f653c5f866873d277c
-README.zh.md: 6336600888d2c4eb324d21a118b95d7ddb447811
+README.md: 56b5a7e05013e7e57319e4a336dcd22f3f23747d
+README.zh.md: 895f4cce14fdaa67ad08cf4e3e95a6fa783a5afe

+ 4 - 4
packages/subprocess/subprocess/README.md

@@ -2,15 +2,15 @@
 
 English | [中文](README.zh.md)
 
-The subprocess seam (`ctx.subprocess`) is the process half of one execution world. The abstract `SubprocessRuntime` exposes executable lookup, ordinary managed `spawn`, and one terminal-process primitive; its vocabulary covers raw/collected stdio, process and terminal handles, exit facts, tree/session cleanup, and the managed `DSH_*` environment namespace. The local implementation lives in [`dsh-subprocess-local`](../subprocess-local/README.md).
+The subprocess seam (`ctx.subprocess`) is the process half of one execution world. The abstract `SubprocessRuntime` exposes executable lookup, ordinary managed `spawn`, and one terminal-process primitive; its vocabulary covers raw/collected stdio, process and terminal handles, exit facts, managed-range/session cleanup, and the managed `DSH_*` environment namespace. The local implementation lives in [`dsh-subprocess-local`](../subprocess-local/README.md).
 
 ## Contract
 
-- `spawn(spec)` returns a live handle synchronously; a native provider may first complete its bounded setup handshake so the handle exposes the target pid. `done` resolves with direct-process exit facts (`SubprocessOutcome` carries no output and no cause classification) and rejects for spawn-level or selected native-runner failures.
+- `spawn(spec)` returns a live handle synchronously. The provider owns the meaning and publication timing of `pid`, which remains `-1` while unavailable or after startup fails. `done` resolves with the spawned command's exit facts (`SubprocessOutcome` carries no output and no cause classification) and rejects for spawn-level or selected provider-runner failures.
 - Spawn working directories and executable paths belong to the provider's execution world. `resolveExecutable(command, env?, signal?)` verifies absolute commands or resolves bare names against that world's scrubbed PATH plus explicit overrides.
 - The spec is fully explicit — argv, cwd, per-stream stdio dispositions, grace — because deployment-varying defaults belong to the caller's config, not to a hidden subprocess-service default (the `dsh-shell` request/spec split is the owning template). `argv` is never shell-interpreted; a consumer that wants a shell passes `['bash', '-c', command]` itself.
 - Stdio is Node-shaped per stream: `'pipe'` hands the caller the raw stream for its own protocol framing (LSP JSON-RPC, ACP ndjson), `'inherit'` passes the parent descriptor through for diagnostics, and collect mode (`{ maxBytes, spill? }`) buffers a bounded tail with an optional full-stream spill file. Collect readers take whole-stream byte offsets and never consume, so independent readers cannot steal one another's deltas; a read whose offset slid out of the in-memory tail is `lossy` and points at the spill file when one exists. Collected output stays readable after settlement.
-- Termination and waiting use one provider-managed range. Supported local Linux and Windows providers use a user-systemd scope or kill-on-close Job; weaker fallbacks use a detached POSIX process group or `taskkill /T`. `terminate()` — the only termination verb — escalates SIGTERM→grace→SIGKILL (idempotent, driven by the spec's abort signal too, and a no-op once the range is empty), while `waitForExit(signal?)` observes that same range so a consumer-owned teardown ladder holds each tier on real quiescence. The wait rejects when a selected native owner can no longer observe its range; the manager reacts but never classifies why (callers own deadlines, teardown ladders, and cause classification).
+- Termination and waiting use one provider-managed range. `terminate()` — the only termination verb — escalates SIGTERM→grace→SIGKILL (idempotent, driven by the spec's abort signal too, and a no-op once the range is empty), while `waitForExit(signal?)` observes that same range so a consumer-owned teardown ladder holds each tier on real quiescence. Each provider documents how it defines and observes the range, including weaker fallbacks; the [local provider](../subprocess-local/README.md) owns its systemd, Job, process-group, and `taskkill` details. The wait rejects when a selected owner can no longer observe its range; the manager reacts but never classifies why (callers own deadlines, teardown ladders, and cause classification).
 - `spawnTerminal(spec)` is the only non-pipe primitive. Its handle owns a real PTY, UTF-8 text I/O, foreground-process-group inspection/signalling, and one awaited `terminate()` operation that reaches quiescence for every session member the provider can still observe and settles in-flight handle calls; providers document substrate-specific observability limits. The spec signal cancels allocation only; the published handle owns its lifetime. The output stream ends after queued output when the top-level process exits, and a live transport failure rejects `done`. These operations remain one substrate primitive because ordinary pipes cannot allocate a controlling terminal or clean terminal-session members; readiness, scrollback, and owner policy remain in the PTY consumer.
 - `scrubbedParentEnv()` / `SENSITIVE_ENV_PATTERN` are the one shared scrub definition: ambient credential-shaped and `DSH_*` names are dropped, and explicit `env` merges after the scrub. The local ordinary and terminal spawns both apply it; SDK-managed transports that own their spawn may import it directly.
 - Disposal of the service terminates all still-running managed processes and awaits their exit.
@@ -28,4 +28,4 @@ No direct invalidation; the named consumers own any request-prefix changes.
 ## Known Limitations and Deferred Work
 
 - **SDK-managed spawns remain outside** — an SDK transport that owns its internal spawn cannot route that call through this service; it can still import `scrubbedParentEnv` so environment policy stays single-sourced.
-- **Teardown ladders are consumer-owned** — the seam ships signalling verbs and the tree-liveness wait, not a canned quiesce sequence; each out-of-process consumer encodes its child's cooperation shape itself (the ACP backend's stdin-EOF-first ladder is the in-repo template).
+- **Teardown ladders are consumer-owned** — the seam ships signalling verbs and the managed-range wait, not a canned quiesce sequence; each out-of-process consumer encodes its child's cooperation shape itself (the ACP backend's stdin-EOF-first ladder is the in-repo template).

+ 4 - 4
packages/subprocess/subprocess/README.zh.md

@@ -2,15 +2,15 @@
 
 [English](README.md) | 中文
 
-子进程 seam(`ctx.subprocess`)是一个执行世界的进程部分。抽象的 `SubprocessRuntime` 公开可执行文件查找、普通受管 `spawn` 和一项终端进程原语;其词汇涵盖原始/收集式 stdio、进程与终端句柄、退出事实、进程树/会话清理,以及受管的 `DSH_*` 环境命名空间。本地实现位于 [`dsh-subprocess-local`](../subprocess-local/README.zh.md)。
+子进程 seam(`ctx.subprocess`)是一个执行世界的进程部分。抽象的 `SubprocessRuntime` 公开可执行文件查找、普通受管 `spawn` 和一项终端进程原语;其词汇涵盖原始/收集式 stdio、进程与终端句柄、退出事实、受管范围/会话清理,以及受管的 `DSH_*` 环境命名空间。本地实现位于 [`dsh-subprocess-local`](../subprocess-local/README.zh.md)。
 
 ## 约定
 
-- `spawn(spec)` 同步返回活动句柄;native provider 可以先完成有界 setup handshake,使该句柄公开 target pid。`done` 以 direct-process exit facts resolve(`SubprocessOutcome` 不携带输出,也不携带原因分类),并在 spawn 层面或所选 native runner 失败时 reject。
+- `spawn(spec)` 同步返回活动句柄。`pid` 的含义和发布时间由 provider 拥有;尚不可用或启动失败后,其值为 `-1`。`done` 以已启动命令的退出事实 resolve(`SubprocessOutcome` 不携带输出,也不携带原因分类),并在 spawn 层面或所选 provider runner 失败时 reject。
 - spawn 工作目录和可执行文件路径属于提供方的执行世界。`resolveExecutable(command, env?, signal?)` 验证绝对命令,或根据该执行世界清理后的 PATH 加显式覆盖来解析裸名称。
 - spec 完全显式(argv、cwd、按流划分的 stdio 处置方式(disposition)、宽限期),因为随部署变化的默认值属于调用方的配置,而不属于某个隐藏的子进程服务默认值(`dsh-shell` 的 request/spec 拆分是这条规则的所属模板)。`argv` 绝不经过 shell 解释;需要 shell 的消费方自行传入 `['bash', '-c', command]`。
 - stdio 按流采用 Node 风格:`'pipe'` 把原始流交给调用方做自己的协议分帧(LSP 的 JSON-RPC、ACP(Agent Client Protocol)的 ndjson),`'inherit'` 直通父进程描述符以承载诊断输出,收集模式(collect)`{ maxBytes, spill? }` 则缓冲一段有界尾部,外加可选的完整流 spill 文件。收集模式的读取器接受全流字节偏移量且从不消费,因此独立的读取器不会抢走彼此的增量;偏移量滑出内存尾部窗口的读取标记为 `lossy`,并在 spill 文件存在时指向它。收集到的输出在结算后仍可读取。
-- 终止与等待使用同一个 provider-managed range。受支持的本地 Linux 与 Windows provider 使用 user-systemd scope 或 kill-on-close Job;较弱 fallback 使用 detached POSIX 进程组或 `taskkill /T`。`terminate()`(唯一的终止动词)执行 SIGTERM→宽限期→SIGKILL 升级(幂等,也由 spec 的 abort 信号驱动,并在范围为空后成为空操作);`waitForExit(signal?)` 观察同一范围,使消费方自有的拆卸阶梯能在真正完全停稳后才进入下一层。所选 native owner 不再能观察范围时,该等待会 reject;管理器只响应中止,但绝不判定原因(deadline、拆卸阶梯与原因分类归调用方所有)。
+- 终止与等待使用同一个 provider-managed range。`terminate()`(唯一的终止动词)执行 SIGTERM→宽限期→SIGKILL 升级(幂等,也由 spec 的 abort 信号驱动,并在范围为空后成为空操作);`waitForExit(signal?)` 观察同一范围,使消费方自有的拆卸阶梯能在真正完全停稳后才进入下一层。每个 provider 记录该范围的定义、观察方式与较弱 fallback;[本地 provider](../subprocess-local/README.zh.md)拥有 systemd、Job、进程组与 `taskkill` 的具体说明。所选 owner 不再能观察范围时,该等待会 reject;管理器只响应中止,但绝不判定原因(deadline、拆卸阶梯与原因分类归调用方所有)。
 - `spawnTerminal(spec)` 是唯一的非管道原语。其句柄负责真实 PTY、UTF-8 文本 I/O、前台进程组检查/信号发送,以及一项须等待的 `terminate()` 操作;该操作会使提供方仍可观察到的每个会话成员完全停稳,并结算在途句柄调用;提供方会记录执行基底特有的可观察性限制。spec 信号只取消分配;句柄一经发布,便负责自身生命周期。顶层进程退出时,输出流在已排队输出之后结束;仍处于活动状态的传输若发生故障,会使 `done` 拒绝。这些操作保留为一项执行基底原语,因为普通管道无法分配控制终端或清理终端会话成员;就绪状态、scrollback 和所有者策略仍归 PTY 消费方所有。
 - `scrubbedParentEnv()` / `SENSITIVE_ENV_PATTERN` 是唯一一份共享的环境清理定义:环境中形似凭据的名称与 `DSH_*` 名称都会被丢弃,显式 `env` 在清除之后合并。本地的普通 spawn 与终端 spawn 都应用该定义;拥有自身 spawn 的 SDK 管理传输可直接导入它。
 - 服务自身的 dispose(资源释放)会终止所有仍在运行的受管进程并等待其退出。
@@ -28,4 +28,4 @@
 ## 已知限制与暂缓事项
 
 - **由 SDK 管理的 spawn 仍在服务之外**:拥有内部 spawn 的 SDK 传输无法把该调用路由到本服务;它仍可导入 `scrubbedParentEnv`,使环境策略保持单一来源。
-- **拆卸阶梯归消费方所有**:该 seam 只提供信号动词与进程树存活等待,不提供现成的停稳序列;每个进程外消费方自行编码其子进程的配合方式(ACP 后端以 stdin EOF 打头的阶梯是仓库内模板)。
+- **拆卸阶梯归消费方所有**:该 seam 只提供信号动词与受管范围等待,不提供现成的停稳序列;每个进程外消费方自行编码其子进程的配合方式(ACP 后端以 stdin EOF 打头的阶梯是仓库内模板)。

+ 7 - 7
packages/subprocess/subprocess/src/index.ts

@@ -80,19 +80,19 @@ declare module '@deepseek-ai/cordis' {
  * Implementations must honor these semantics:
  * - Executable paths belong to one execution world shared with the mounted
  *   filesystem provider.
- * - {@link spawn} returns a live handle synchronously after provider-specific
- *   setup needed to publish its target pid. `done` resolves with direct-process
- *   exit facts and may reject for spawn or selected provider-runner failures.
+ * - {@link spawn} returns a live handle synchronously. Its pid is provider-owned
+ *   and may remain unavailable during asynchronous startup. `done` resolves with
+ *   the spawned command's exit facts and may reject for spawn or selected
+ *   provider-runner failures.
  * - Collect-mode readers are offset-based and non-consuming, so independent
  *   readers never consume one another's output; lossy reads report truncation
  *   and the spill file holding the complete stream when one exists. Piped
  *   streams are handed to the caller raw and never buffered here.
  * - {@link SubprocessHandle.terminate} (and the spec's abort signal) escalates
  *   SIGTERM→grace→SIGKILL — the only termination verb — against the provider's
- *   managed range. Supported local Linux and Windows providers use an OS-owned
- *   scope or Job; weaker fallbacks use a detached process group or direct-parent
- *   tree. {@link SubprocessHandle.waitForExit} observes that same range so a
- *   consumer-owned teardown ladder can hold each tier on real quiescence.
+ *   managed range. {@link SubprocessHandle.waitForExit} observes that same range
+ *   so a consumer-owned teardown ladder can hold each tier on real quiescence;
+ *   each provider documents its identity and observability limits.
  * - Disposal of the service terminates all still-running managed processes
  *   and awaits their exit.
  * - {@link spawnTerminal} owns terminal allocation, text transport,

+ 4 - 4
packages/subprocess/subprocess/src/types.ts

@@ -156,15 +156,15 @@ export interface SubprocessCollectedOutputs {
 }
 
 /**
- * A live direct child and its provider-managed process range. Collected output
+ * A live subprocess and its provider-managed process range. Collected output
  * remains readable after exit; piped streams belong to the caller.
  *
  * Termination and {@link SubprocessHandle.waitForExit} use the same managed
- * range. Supported Linux and Windows hosts use an OS-owned scope or Job;
- * weaker platform fallbacks are disclosed by the provider.
+ * range. Each provider documents the process identity and range it can
+ * observe.
  */
 export interface SubprocessHandle {
-  /** Direct target process id; -1 when the spawn itself failed. */
+  /** Provider-published process identifier; -1 while unavailable or after startup fails. */
   readonly pid: number
   /** The child's stdin, present iff spawned with `stdin: 'pipe'`. */
   readonly stdin: Writable | undefined