@deepseek-ai/dsh-subprocess-local 0.0.1-rc.2 → 0.0.1-rc.5

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: 2817e02861db6caad89cad258d14a90c34afcbaf
6
- README.zh.md: 251b994a35e8bd7c84827957a548a1f352583ac6
5
+ README.md: 645f05a0daf92d64c438f8aaae3b0e115f253a64
6
+ README.zh.md: cd8c04071f00b298b2d242624c2ad1327758e16e
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  English | [中文](README.zh.md)
4
4
 
5
- Local Service provider for the [`@deepseek-ai/dsh-subprocess`](../subprocess/README.md) seam. `LocalSubprocessService` resolves local executables, spawns ordinary detached process trees with explicit stdio, and implements terminal processes through `node-pty` plus platform process inspection. It has no config: every disposition, limit, terminal dimension, grace, and directory arrives from the calling capability seams ([`dsh-bash-local`](../../bash/bash-local/README.md), [`dsh-lsp-local`](../../lsp/lsp-local/README.md), and [`dsh-pty-local`](../../pty/pty-local/README.md)).
5
+ Local Service provider for the [`@deepseek-ai/dsh-subprocess`](../subprocess/README.md) seam. `LocalSubprocessRuntime` resolves local executables, spawns ordinary detached process trees with explicit stdio, and implements terminal processes through `node-pty` plus platform process inspection. It has no config: every disposition, limit, terminal dimension, grace, and directory arrives from the calling capability seams ([`dsh-bash-local`](../../shell/bash-local/README.md), [`dsh-lsp-stdio`](../../lsp/lsp-stdio/README.md), and [`dsh-terminal-bash`](../../terminal/terminal-bash/README.md)).
6
6
 
7
7
  ## Behavior
8
8
 
@@ -12,7 +12,8 @@ Local Service provider for the [`@deepseek-ai/dsh-subprocess`](../subprocess/REA
12
12
  - **Offset-based reads** — collect-mode readers return deltas in whole-stream byte coordinates; the service never holds a cursor, so consumer-owned cursors (the bash background read path) and full-stream re-reads coexist, before and after settlement.
13
13
  - **Executable lookup** — `resolveExecutable` checks absolute files or searches the scrubbed effective PATH with platform-aware executable extensions; relative paths containing separators are rejected at the seam, and relative PATH entries resolve from the host process cwd.
14
14
  - **Terminal-process ownership** — `spawnTerminal` allocates `node-pty`, bridges UTF-8 terminal text, inspects and signals the current foreground process group, and exposes one awaited termination operation that sweeps descendants before and after terminating the top-level shell. Each foreground inspection retains exact identities from the rooted tree; Linux also enumerates the POSIX session after its leader exits. A previously observed macOS descendant and any same-session Linux member therefore remain fenced after reparenting, while pid/start identity prevents cleanup from following PID reuse. The higher PTY backend owns prompt readiness, buffers, and model-facing operations.
15
- - **Terminate-and-join disposal** — the service retains live handles only so its own disposal can escalate every running tree and await its exit; settled and spawn-failed handles leave the live set on settlement.
15
+ - **Terminate-and-join disposal** — the service retains live handles so its own disposal can escalate every running tree and await its exit; quiescent and spawn-failed handles leave the live set after whole-tree or terminal-session cleanup finishes.
16
+ - **Synchronous host-exit finalization** — while the service effect is active, a Node `exit` listener force-terminates every ordinary tree and observable terminal session still in the same live sets. The local-only operations send POSIX SIGKILL to the managed group, run Windows `taskkill /T /F`, and synchronously signal captured/current terminal identities around the PTY root kill; they create no promise or timer, preserve the host's exit code and diagnostic, contain each target's failure, and do not claim quiescence. Normal disposal keeps the awaited graceful path above. See the [host-exit cleanup decision](../../../.agents/notes/implemented/bug-fix/2026-08-11-synchronous-subprocess-exit-cleanup.md).
16
17
 
17
18
  ## Model Experience
18
19
 
@@ -27,6 +28,7 @@ No direct invalidation; the named consumers own any request-prefix changes.
27
28
  - **Windows tree support is best-effort** — termination routes through `taskkill /PID <pid> /T /F` with all outcomes contained (absent tree, races, missing binary), and liveness falls back to the direct-child boundary.
28
29
  - **Terminal process inspection is Linux/macOS only** — the terminal primitive fails when its inspector has no supported platform implementation; Linux exact probes cover x64 and arm64, while macOS uses `ps` snapshots.
29
30
  - **A daemonized terminal descendant can still escape the observable boundary** — on macOS, a child that reparents before any foreground-inspection snapshot is no longer discoverable from the `node-pty` root; on Linux, a child that calls `setsid` leaves both the tree and owned terminal session. The local provider does not add a continuous process-table monitor.
31
+ - **In-process cleanup requires a JavaScript-observable exit** — direct `process.exit()`, default uncaught exceptions, and default unhandled rejections emit Node's synchronous `exit` event. The default OS disposition for an unhandled `SIGTERM`, `SIGINT`, or `SIGHUP` bypasses that event; an application covers those signals only by installing a handler that performs normal disposal or calls `process.exit()`. `SIGKILL`, fatal OOM, `process.abort()`, native crashes, power loss, and any failure that cannot run JavaScript require an external supervisor, container init, or equivalent OS owner.
30
32
  - **The credential scrub is a name heuristic** — `*KEY*`/`*PASSWORD*`/`*SECRET*`/`*TOKEN*` only; differently-named secrets (e.g. `*PASSPHRASE*`) pass through, and a whitelist for over-scrubbed vars is noted future work.
31
33
  - **Completed spill files are not deleted** — bounded full-output recovery files (and the private per-process spill dir) accumulate under the OS tmpdir until something external cleans them; oversize incomplete spills are discarded and deletion is attempted immediately, but a cleanup failure can leave a bounded file behind.
32
34
 
package/README.zh.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README.md) | 中文
4
4
 
5
- [`@deepseek-ai/dsh-subprocess`](../subprocess/README.md) seam 的本地 Service provider。`LocalSubprocessService` 解析本地可执行文件,以显式 stdio spawn 普通 detached 进程树,并通过 `node-pty` 加平台进程检查实现终端进程。该实现没有任何配置:每项处置方式、限制、终端尺寸、宽限期与目录都来自调用方能力 seam([`dsh-bash-local`](../../bash/bash-local/README.md)、[`dsh-lsp-local`](../../lsp/lsp-local/README.md) 和 [`dsh-pty-local`](../../pty/pty-local/README.md))。
5
+ [`@deepseek-ai/dsh-subprocess`](../subprocess/README.md) seam 的本地 Service provider。`LocalSubprocessRuntime` 解析本地可执行文件,以显式 stdio spawn 普通 detached 进程树,并通过 `node-pty` 加平台进程检查实现终端进程。该实现没有任何配置:每项处置方式、限制、终端尺寸、宽限期与目录都来自调用方能力 seam([`dsh-bash-local`](../../shell/bash-local/README.md)、[`dsh-lsp-stdio`](../../lsp/lsp-stdio/README.md) 和 [`dsh-terminal-bash`](../../terminal/terminal-bash/README.md))。
6
6
 
7
7
  ## 行为
8
8
 
@@ -10,9 +10,10 @@
10
10
  - **按流划分的处置方式**:`'pipe'` 把原始流原样交给调用方(协议分帧仍归消费方所有);`'inherit'` 直通父进程的描述符;收集模式(collect)在输出超过上限后于内存中保留尾部(错误与结果通常聚集在末尾,沿用 pi/OpenCode 的理由),并在配置了 spill 上限时把完整流追加到一个私有临时文件;省略 `spill` 则只保留用于诊断的尾部。某条流大于 spill 上限时,会丢弃已不完整的 spill,仅返回带截断标记的尾部;spill 文件描述符在结算时封存,最终关闭失败时则不公布路径,以免声称存在不完整的文件。spill 文件权限为 `0600`、名称随机,位于按需创建、权限为 `0700` 的每进程目录之下。
11
11
  - **凭据清除 + 显式合并**:以 `process.env` 为基础,移除形似凭据的变量(`*KEY*`/`*PASSWORD*`/`*SECRET*`/`*TOKEN*`)和所有环境中已有的 `DSH_*` 名称;spec 的显式 `env` 在该清除之后合并且不做命名空间校验,因此有意提供的凭据或当前 `DSH_*` 事实会胜出,而陈旧的嵌套 harness 身份无法从环境中隐式漏入。提供的 stdin 会被写入后关闭;否则 fd 0 指向 `/dev/null`。参见 [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.md)与[受管环境 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md)。
12
12
  - **基于偏移量的读取**:收集模式的读取器按完整流的字节坐标返回增量;服务自身从不持有游标,因此消费方自有的游标(bash 的后台读取路径)与完整流重读可以共存,结算前后皆然。
13
- - **可执行文件查找**:`resolveExecutable` 检查绝对文件,或根据平台可执行文件扩展名在清理后的有效 PATH 中搜索;含分隔符的相对路径在该能力入口被拒绝,相对 PATH 条目从宿主进程 cwd 解析。
13
+ - **可执行文件查找**:`resolveExecutable` 检查绝对文件,或根据平台可执行文件扩展名在清理后的有效 PATH 中搜索;含分隔符的相对路径在该 seam 处被拒绝,相对 PATH 条目从宿主进程 cwd 解析。
14
14
  - **终端进程所有权**:`spawnTerminal` 分配 `node-pty`,桥接 UTF-8 终端文本,检查当前前台进程组并向其发送信号,还会公开一项须等待的终止操作,在终止顶层 shell 前后清理后代进程。每次前台检查都会保留根进程树中的精确身份;Linux 还会在 POSIX 会话 leader 退出后枚举该会话。因此,之前观察到的 macOS 后代以及同会话 Linux 成员在重新设定父进程后仍受围栏保护,pid/start 身份则防止清理跟随 PID 复用。上层 PTY 后端负责提示符就绪、缓冲区与面向模型的操作。
15
- - **先终止再等待退出的 dispose(资源释放)**:服务保留存活句柄,只为让自身的 dispose 能对每个仍在运行的进程树执行升级并等待其退出;已结算与 spawn 失败的句柄在结算时即离开存活集合。
15
+ - **先终止再等待退出的 dispose(资源释放)**:服务保留存活句柄,使自身的 dispose 能对每个仍在运行的进程树执行升级并等待其退出;完全停稳与 spawn 失败的句柄会在整棵进程树或 terminal session 清理完成后离开存活集合。
16
+ - **同步宿主退出最终清理**:服务 effect 仍有效时,Node `exit` listener 会强制终止同一组存活集合中仍存在的每棵普通进程树和可观察 terminal session。这些仅供本地实现使用的操作会向受管 POSIX 进程组发送 SIGKILL、在 Windows 运行 `taskkill /T /F`,并在终止 PTY root 前后同步向已捕获及当前可观察的 terminal 身份发送信号;它们不会创建 Promise 或 timer,不改变宿主退出码与诊断,会分别包含每个目标的失败,也不会声称已经完全停稳。正常 dispose 仍使用上面的须等待温和路径。参见[宿主退出清理决策](../../../.agents/notes/implemented/bug-fix/2026-08-11-synchronous-subprocess-exit-cleanup.md)。
16
17
 
17
18
  ## 模型体验
18
19
 
@@ -27,6 +28,7 @@
27
28
  - **Windows 进程树支持仅为尽力而为**:终止经由 `taskkill /PID <pid> /T /F` 完成,所有结果都被就地吸收,不向外抛出(进程树已不存在、竞态、二进制缺失),存活探测则回退到直接子进程边界。
28
29
  - **终端进程检查仅支持 Linux/macOS**:检查器没有受支持的平台实现时,终端原语会失败;Linux 精确探针覆盖 x64 与 arm64,macOS 则使用 `ps` 快照。
29
30
  - **守护化的终端后代仍可能逃出可观察边界**:在 macOS 上,子进程如果在任何前台检查快照之前重新设定父进程,将无法再从 `node-pty` 根进程发现;在 Linux 上,调用 `setsid` 的子进程会同时离开进程树与自有终端会话。本地提供方不会新增持续进程表监视器。
31
+ - **进程内清理要求退出阶段仍能执行 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 所有者负责。
30
32
  - **凭据清除依赖名称启发式规则**:只匹配 `*KEY*`/`*PASSWORD*`/`*SECRET*`/`*TOKEN*`;名称不同的 secret(例如 `*PASSPHRASE*`)会继续传递,对误删变量引入白名单属于已记录的后续工作。
31
33
  - **不会删除已完成的 spill 文件**:有界的完整输出恢复文件(以及每个进程的私有 spill 目录)会在 OS tmpdir 下累积,直到外部机制进行清理;超大的不完整 spill 会被丢弃并立即尝试删除,但清理失败可能留下一个有界文件。
32
34
 
package/lib/index.js CHANGED
@@ -2,7 +2,7 @@ import { closeSync, constants, mkdtempSync, openSync, readFileSync, readSync, re
2
2
  import { access, stat } from "node:fs/promises";
3
3
  import { delimiter, extname, isAbsolute, join, resolve } from "node:path";
4
4
  import * as nodePty from "node-pty";
5
- import { SubprocessService, scrubbedParentEnv } from "@deepseek-ai/dsh-subprocess";
5
+ import { SubprocessRuntime, scrubbedParentEnv } from "@deepseek-ai/dsh-subprocess";
6
6
  import { execFileSync, spawn, spawnSync } from "node:child_process";
7
7
  import { randomBytes } from "node:crypto";
8
8
  import { constants as constants$1, tmpdir } from "node:os";
@@ -621,6 +621,9 @@ function spawnSubprocess(spec, internals = {}) {
621
621
  kill("SIGKILL");
622
622
  }, spec.graceMs);
623
623
  };
624
+ const terminateForHostExit = () => {
625
+ kill("SIGKILL");
626
+ };
624
627
  const onAbort = () => {
625
628
  terminate();
626
629
  };
@@ -694,6 +697,7 @@ function spawnSubprocess(spec, internals = {}) {
694
697
  },
695
698
  done,
696
699
  terminate,
700
+ terminateForHostExit,
697
701
  waitForExit
698
702
  };
699
703
  }
@@ -785,6 +789,27 @@ var LocalTerminalHandle = class {
785
789
  });
786
790
  return cleanup;
787
791
  }
792
+ /**
793
+ * Force-terminate the observable session synchronously during Node's exit
794
+ * event. This does not claim quiescence and does not replace terminate().
795
+ */
796
+ terminateForHostExit() {
797
+ this.forceStopDescendants();
798
+ this.forceStopShell();
799
+ this.forceStopDescendants();
800
+ }
801
+ forceStopShell() {
802
+ if (this.exited) return;
803
+ if (this.rootIdentity !== void 0) {
804
+ try {
805
+ this.inspector.signalProcess(this.rootIdentity, "SIGKILL");
806
+ } catch (_rootExitedDuringHostExit) {}
807
+ return;
808
+ }
809
+ try {
810
+ this.terminal.kill("SIGKILL");
811
+ } catch (_unidentifiedShellExitedDuringHostExit) {}
812
+ }
788
813
  survivors(members) {
789
814
  return members.filter((member) => this.inspector.isAlive(member));
790
815
  }
@@ -809,6 +834,13 @@ var LocalTerminalHandle = class {
809
834
  this.inspector.signalProcess(member, signal);
810
835
  } catch (_alreadyExitedDuringSignal) {}
811
836
  }
837
+ forceStopDescendants() {
838
+ let members = this.trackedDescendants;
839
+ try {
840
+ members = this.descendants();
841
+ } catch (_processTableUnavailableDuringHostExit) {}
842
+ this.signalMembers(members, "SIGKILL");
843
+ }
812
844
  unionMembers(...groups) {
813
845
  const members = [];
814
846
  const seen = /* @__PURE__ */ new Set();
@@ -858,8 +890,9 @@ var LocalTerminalHandle = class {
858
890
  //#region lib/types/index.js
859
891
  /**
860
892
  * Local Service provider for the subprocess capability seam. Each spawn is a detached
861
- * process tree with the spec's per-stream stdio dispositions; disposal
862
- * terminates and joins live trees. It has no config: every disposition and
893
+ * process tree with the spec's per-stream stdio dispositions. Normal disposal
894
+ * terminates and joins live trees; Node's synchronous exit phase force-stops
895
+ * any trees the service still owns. It has no config: every disposition and
863
896
  * limit arrives on the spec, so the deployment-varying choices stay with the
864
897
  * caller's config (the bash executor's, the LSP host's, …).
865
898
  * @module @deepseek-ai/dsh-subprocess-local
@@ -868,12 +901,13 @@ var LocalTerminalHandle = class {
868
901
  * Local subprocess service: detached process trees, Node-shaped stdio
869
902
  * dispositions (raw pipes, inherit, bounded tail-keep collection with spill
870
903
  * files), credential-scrubbed environment, and tree-scoped signalling with
871
- * SIGTERM→grace→SIGKILL escalation.
904
+ * SIGTERM→grace→SIGKILL escalation, plus synchronous final termination during
905
+ * JavaScript-observable host exit.
872
906
  */
873
- var LocalSubprocessService = class extends SubprocessService {
874
- /** Live handles retained only so disposal can terminate and join them. */
907
+ var LocalSubprocessRuntime = class extends SubprocessRuntime {
908
+ /** Live handles retained for normal disposal and synchronous host-exit finalization. */
875
909
  live = /* @__PURE__ */ new Set();
876
- /** Live terminal sessions retained through whole-session quiescence. */
910
+ /** Live terminals retained through normal quiescence or host-exit finalization. */
877
911
  terminals = /* @__PURE__ */ new Set();
878
912
  /** Test hook: spill and platform knobs forwarded to spawnSubprocess. */
879
913
  internals = {};
@@ -881,20 +915,42 @@ var LocalSubprocessService = class extends SubprocessService {
881
915
  terminalInspector;
882
916
  constructor(ctx) {
883
917
  super(ctx);
884
- ctx.effect(() => async () => {
885
- const pending = [];
886
- for (const handle of this.live) {
887
- handle.terminate();
888
- pending.push(handle.done.catch(() => {}).then(() => handle.waitForExit()));
889
- }
890
- for (const terminal of this.terminals) pending.push(terminal.terminate());
891
- this.live.clear();
892
- this.terminals.clear();
893
- const failures = (await Promise.allSettled(pending)).flatMap((outcome) => outcome.status === "rejected" ? [outcome.reason] : []);
894
- if (failures.length === 1) throw failures[0];
895
- if (failures.length > 1) throw new AggregateError(failures, "local subprocess teardown failed");
918
+ ctx.effect(() => {
919
+ const onHostExit = () => {
920
+ this.terminateForHostExit();
921
+ };
922
+ process.prependListener("exit", onHostExit);
923
+ return async () => {
924
+ try {
925
+ await this.disposeManagedProcesses();
926
+ } finally {
927
+ process.off("exit", onHostExit);
928
+ }
929
+ };
896
930
  }, "local subprocess teardown");
897
931
  }
932
+ terminateForHostExit() {
933
+ for (const handle of this.live) try {
934
+ handle.terminateForHostExit();
935
+ } catch (_ordinaryTreeTerminationFailed) {}
936
+ for (const terminal of this.terminals) try {
937
+ terminal.terminateForHostExit();
938
+ } catch (_terminalTerminationFailed) {}
939
+ }
940
+ async disposeManagedProcesses() {
941
+ const pending = [];
942
+ for (const handle of this.live) {
943
+ handle.terminate();
944
+ pending.push(handle.done.catch(() => {}).then(() => handle.waitForExit()));
945
+ }
946
+ for (const terminal of this.terminals) pending.push(terminal.terminate());
947
+ const failures = (await Promise.allSettled(pending)).flatMap((outcome) => outcome.status === "rejected" ? [outcome.reason] : []);
948
+ if (failures.length > 0) this.terminateForHostExit();
949
+ this.live.clear();
950
+ this.terminals.clear();
951
+ if (failures.length === 1) throw failures[0];
952
+ if (failures.length > 1) throw new AggregateError(failures, "local subprocess teardown failed");
953
+ }
898
954
  async resolveExecutable(command, env, signal) {
899
955
  if (command.length === 0) throw new Error("subprocess-local: executable must be non-empty");
900
956
  signal?.throwIfAborted();
@@ -958,4 +1014,4 @@ function environmentValue(env, name) {
958
1014
  return Object.entries(env).find(([key]) => key.toUpperCase() === normalized)?.[1];
959
1015
  }
960
1016
  //#endregion
961
- export { LocalSubprocessService, LocalSubprocessService as default };
1017
+ export { LocalSubprocessRuntime, LocalSubprocessRuntime as default };
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * Local Service provider for the subprocess capability seam. Each spawn is a detached
3
- * process tree with the spec's per-stream stdio dispositions; disposal
4
- * terminates and joins live trees. It has no config: every disposition and
3
+ * process tree with the spec's per-stream stdio dispositions. Normal disposal
4
+ * terminates and joins live trees; Node's synchronous exit phase force-stops
5
+ * any trees the service still owns. It has no config: every disposition and
5
6
  * limit arrives on the spec, so the deployment-varying choices stay with the
6
7
  * caller's config (the bash executor's, the LSP host's, …).
7
8
  * @module @deepseek-ai/dsh-subprocess-local
8
9
  */
9
10
  import { Context } from '@deepseek-ai/cordis';
10
- import { SubprocessService } from '@deepseek-ai/dsh-subprocess';
11
+ import { SubprocessRuntime } from '@deepseek-ai/dsh-subprocess';
11
12
  import type { SubprocessHandle, SubprocessSpawnSpec, SubprocessTerminalHandle, SubprocessTerminalSpawnSpec } from '@deepseek-ai/dsh-subprocess';
12
13
  import type { SpawnInternals } from './spawn.ts';
13
14
  import type { ProcessInspector } from './process-inspector.ts';
@@ -15,22 +16,25 @@ import type { ProcessInspector } from './process-inspector.ts';
15
16
  * Local subprocess service: detached process trees, Node-shaped stdio
16
17
  * dispositions (raw pipes, inherit, bounded tail-keep collection with spill
17
18
  * files), credential-scrubbed environment, and tree-scoped signalling with
18
- * SIGTERM→grace→SIGKILL escalation.
19
+ * SIGTERM→grace→SIGKILL escalation, plus synchronous final termination during
20
+ * JavaScript-observable host exit.
19
21
  */
20
- export declare class LocalSubprocessService extends SubprocessService {
21
- /** Live handles retained only so disposal can terminate and join them. */
22
+ export declare class LocalSubprocessRuntime extends SubprocessRuntime {
23
+ /** Live handles retained for normal disposal and synchronous host-exit finalization. */
22
24
  private live;
23
- /** Live terminal sessions retained through whole-session quiescence. */
25
+ /** Live terminals retained through normal quiescence or host-exit finalization. */
24
26
  private terminals;
25
27
  /** Test hook: spill and platform knobs forwarded to spawnSubprocess. */
26
28
  internals: SpawnInternals;
27
29
  /** Test hook for platform process inspection; production resolves lazily on terminal spawn. */
28
30
  terminalInspector: ProcessInspector | undefined;
29
31
  constructor(ctx: Context);
32
+ private terminateForHostExit;
33
+ private disposeManagedProcesses;
30
34
  resolveExecutable(command: string, env?: Readonly<Record<string, string>>, signal?: AbortSignal): Promise<string>;
31
35
  private executableCandidates;
32
36
  spawn(spec: SubprocessSpawnSpec): SubprocessHandle;
33
37
  spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise<SubprocessTerminalHandle>;
34
38
  }
35
- export default LocalSubprocessService;
39
+ export default LocalSubprocessRuntime;
36
40
  //# sourceMappingURL=index.d.ts.map
@@ -27,6 +27,15 @@ export interface SpawnInternals {
27
27
  /** Linux process-group member probe (defaults to `/proc` inspection). */
28
28
  linuxProcessGroupHasLiveMembers?: (processGroupId: number) => boolean | undefined;
29
29
  }
30
+ /**
31
+ * Local-only synchronous final termination used by the owning service during
32
+ * host exit and as the last fallback after failed normal disposal. It is
33
+ * intentionally absent from the public subprocess seam.
34
+ */
35
+ export interface LocalSubprocessHandle extends SubprocessHandle {
36
+ /** Force-terminate the current tree synchronously without starting timers or waits. */
37
+ terminateForHostExit(): void;
38
+ }
30
39
  /**
31
40
  * Collects one stream with a bounded in-memory tail. With a spill cap, on
32
41
  * first overflow a spill file is created and every chunk (including those
@@ -117,5 +126,5 @@ export declare function taskkillProcessTree(pid: number): void;
117
126
  * @returns live subprocess handle.
118
127
  * @throws when `graceMs` cannot be represented by one Node timer.
119
128
  */
120
- export declare function spawnSubprocess(spec: SubprocessSpawnSpec, internals?: SpawnInternals): SubprocessHandle;
129
+ export declare function spawnSubprocess(spec: SubprocessSpawnSpec, internals?: SpawnInternals): LocalSubprocessHandle;
121
130
  //# sourceMappingURL=spawn.d.ts.map
@@ -36,10 +36,17 @@ export declare class LocalTerminalHandle implements SubprocessTerminalHandle {
36
36
  inspectForeground(): Promise<SubprocessTerminalForeground | undefined>;
37
37
  signalForeground(signal: SubprocessTerminalSignal): Promise<number>;
38
38
  terminate(): Promise<void>;
39
+ /**
40
+ * Force-terminate the observable session synchronously during Node's exit
41
+ * event. This does not claim quiescence and does not replace terminate().
42
+ */
43
+ terminateForHostExit(): void;
44
+ private forceStopShell;
39
45
  private survivors;
40
46
  private descendants;
41
47
  private waitForMembers;
42
48
  private signalMembers;
49
+ private forceStopDescendants;
43
50
  private unionMembers;
44
51
  private stopDescendants;
45
52
  private stopShell;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-subprocess-local",
3
3
  "description": "Local-subprocess implementation of the DeepSeek Harness subprocess seam",
4
- "version": "0.0.1-rc.2",
4
+ "version": "0.0.1-rc.5",
5
5
  "publishConfig": {
6
6
  "access": "restricted"
7
7
  },
@@ -33,19 +33,20 @@
33
33
  ],
34
34
  "license": "BSD-3-Clause",
35
35
  "peerDependencies": {
36
- "@deepseek-ai/dsh-invariants": "^0.0.1-rc.2",
37
- "@deepseek-ai/dsh-subprocess": "^0.0.1-rc.2",
38
- "@deepseek-ai/dsh-timeout": "^0.0.1-rc.2",
39
- "@deepseek-ai/cordis": "^4.0.1-rc.1"
36
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.5",
37
+ "@deepseek-ai/dsh-subprocess": "^0.0.1-rc.5",
38
+ "@deepseek-ai/cordis": "^4.0.1-rc.4",
39
+ "@deepseek-ai/dsh-timeout": "^0.0.1-rc.5"
40
40
  },
41
41
  "dependencies": {
42
42
  "node-pty": "^1.1.0"
43
43
  },
44
44
  "devDependencies": {
45
- "@deepseek-ai/dsh-invariants": "^0.0.1-rc.2",
46
- "@deepseek-ai/dsh-subprocess": "^0.0.1-rc.2",
47
- "@deepseek-ai/dsh-timeout": "^0.0.1-rc.2",
48
- "@deepseek-ai/cordis": "^4.0.1-rc.1"
45
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.5",
46
+ "@deepseek-ai/dsh-loader-smoke": "^0.0.1-rc.5",
47
+ "@deepseek-ai/dsh-subprocess": "^0.0.1-rc.5",
48
+ "@deepseek-ai/dsh-timeout": "^0.0.1-rc.5",
49
+ "@deepseek-ai/cordis": "^4.0.1-rc.4"
49
50
  },
50
51
  "scripts": {
51
52
  "postinstall": "node scripts/ensure-spawn-helper.mjs"