@deepseek-ai/dsh-subprocess-local 0.1.6-alpha.1 → 0.1.6-alpha.2

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: 5379aff31cdcf036614a6664ce59330d319bd459
6
- README.zh.md: f61596ac94b7f1270275cb93593c90a281c9ca90
5
+ README.md: 24480565de2f324a451dfdb40101eb50703b30b6
6
+ README.zh.md: c86a7eb930834d960fa25dd7b5d175af63d0b31a
package/README.md CHANGED
@@ -40,6 +40,8 @@ Load the provider in the same composition as its consumers. It has no config fie
40
40
 
41
41
  Absolute executable paths are verified; bare names resolve against the scrubbed PATH with platform-aware executable extensions (`.COM`/`.EXE`/`.BAT`/`.CMD` on Windows). Relative paths containing separators are rejected — provide an absolute path or a bare PATH name — and relative PATH entries resolve from the host process cwd.
42
42
 
43
+ Windows ordinary subprocesses start the private Job runner with `windowsHide` and request hidden initial windows for native targets. Standard streams and Job ownership remain independent of window visibility; commands that explicitly create their own windows are outside this guarantee.
44
+
43
45
  ### Collecting output
44
46
 
45
47
  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.
@@ -50,10 +52,15 @@ The `./output` export shares this collector and retained-spill storage with proc
50
52
 
51
53
  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
54
 
55
+ <a id="running-terminal-sessions"></a>
53
56
  ### Running terminal sessions
54
57
 
55
58
  `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.
56
59
 
60
+ With `shellActivity: true`, plain non-login `bash -i` and `zsh -i` launches install private lifecycle records while retaining user startup files and prompt configuration. Bash requires version 4.4 or later and writable prompt hooks; Zsh observes an empty top-level ZLE prompt, excluding `vared`, selection and continuation prompts. Input, shell transitions and changed process observations advance activity revisions. Foreground, background and stopped descendants block idle; native Linux also requires exactly one task in the systemd scope, including ownership beyond the process tree; incomplete process-table scans, custom traps and Zsh asynchronous descriptor handlers yield unknown. A failure to enumerate the process table rejects the observation; activity remains unknown and cleanup retains ownership until a readable table permits verification. Private files are removed after successful process cleanup. Other shells, Windows, custom arguments and sandbox-wrapped executables remain usable with unknown activity.
61
+
62
+ Opted-in root exit does not terminate surviving descendants. A confirmed empty Linux managed range or complete empty Linux session can authorize reclamation of its retained record; macOS cannot confirm an unobserved process range after root exit and keeps that record unknown. The existing fallback visibility limits still apply: shell lifecycle records do not make escaped, unobserved descendants discoverable. Lifecycle records coordinate ordinary shell behavior, not hostile same-user processes.
63
+
57
64
  ### Shutdown behavior
58
65
 
59
66
  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.
package/README.zh.md CHANGED
@@ -40,6 +40,8 @@ kind: "package-reference"
40
40
 
41
41
  绝对可执行文件路径会被验证;裸名称根据清理后的 PATH 并以平台感知的可执行文件扩展名(Windows 上为 `.COM`/`.EXE`/`.BAT`/`.CMD`)解析。含分隔符的相对路径会被拒绝——请提供绝对路径或裸 PATH 名称——相对 PATH 条目从宿主进程 cwd 解析。
42
42
 
43
+ Windows 普通子进程通过 `windowsHide` 启动私有 Job runner,并为原生目标请求隐藏初始窗口。标准流和 Job 归属不依赖窗口可见性;显式创建自身窗口的命令不在此保证范围内。
44
+
43
45
  ### 收集输出
44
46
 
45
47
  收集模式在内存中保留一条流的最后 `maxBytes`——错误与最终结果通常聚集在末尾——并在配置了 `spill` 上限时把完整流追加到 OS 临时目录下每进程目录中的私有文件(`0700` 目录、`0600` 随机命名文件)。某条流大于 spill 上限时,会丢弃不完整的 spill,只返回带截断标记的尾部。读取基于偏移量且从不消费,因此后台读取与批量读取在退出前后都可以共存。
@@ -50,10 +52,15 @@ kind: "package-reference"
50
52
 
51
53
  普通 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
54
 
55
+ <a id="running-terminal-sessions"></a>
53
56
  ### 运行终端会话
54
57
 
55
58
  `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 退出通知。
56
59
 
60
+ 使用 `shellActivity: true` 时,普通非登录的 `bash -i` 与 `zsh -i` 会安装私有生命周期记录,同时保留用户启动文件和提示符配置。Bash 需要 4.4 或更高版本,并允许写入提示符 hook;Zsh 观察空的顶层 ZLE 提示符,排除 `vared`、选择和续行提示符。输入、shell 状态迁移和进程观察变化都会推进活动 revision。前台、后台及停止的后代任务阻止空闲判断;原生 Linux 还要求 systemd scope 中恰好只有一个 task,覆盖进程树以外仍归其所有的工作;进程表扫描不完整、自定义 trap 和 Zsh 异步文件描述符 handler 会返回 unknown。进程表枚举失败会拒绝本次观察;活动保持 unknown,清理保留所有权,直到进程表可读并能完成验证。进程清理成功后删除私有文件。其他 shell、Windows、自定义参数以及经 sandbox 包装的可执行文件仍可使用,但活动为 unknown。
61
+
62
+ 启用后,根进程退出不会终止仍在运行的后代。明确为空的 Linux 受管范围或完整且为空的 Linux 会话可允许回收保留的记录;macOS 无法在根进程退出后确认未观察到的进程范围,会将该记录保持为 unknown。原有 fallback 可见性限制仍然适用:shell 生命周期记录不能让已逃逸且未观察到的后代变得可发现。这些记录协调普通 shell 行为,不用于防御同一用户的恶意进程。
63
+
57
64
  ### 关闭行为
58
65
 
59
66
  正常 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。
package/lib/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { C as bindManagedProcess, D as createProcessInspector, E as validateSubprocessSpec, O as controlPipe, S as loadLinuxExecve, T as spawnSubprocess, _ as parseWindowsRunnerResult, c as runnerStdio, d as cleanupLinuxLaunchFiles, l as spawnRunnerInvocation, m as deserializeRunnerError, n as WINDOWS_RUNNER_SELECTION, o as runnerEnvironment, p as createLinuxLaunchFiles, s as runnerInvocationAvailable, u as targetEnvironment, w as childEnv, y as readLinuxStartupError } from "./runner-launch-BnjugUhn.js";
1
+ import { C as bindManagedProcess, D as createProcessInspector, E as validateSubprocessSpec, O as controlPipe, S as loadLinuxExecve, T as spawnSubprocess, _ as parseWindowsRunnerResult, c as runnerStdio, d as cleanupLinuxLaunchFiles, l as spawnRunnerInvocation, m as deserializeRunnerError, n as WINDOWS_RUNNER_SELECTION, o as runnerEnvironment, p as createLinuxLaunchFiles, s as runnerInvocationAvailable, u as targetEnvironment, w as childEnv, y as readLinuxStartupError } from "./runner-launch-DGV26RBf.js";
2
2
  import { prepareManagedProcessBinding } from "./output.js";
3
- import { closeSync, constants, existsSync, openSync } from "node:fs";
3
+ import { closeSync, constants, existsSync, mkdtempSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { access, stat } from "node:fs/promises";
5
- import { constants as constants$1, devNull, userInfo } from "node:os";
6
- import { delimiter, extname, isAbsolute, resolve } from "node:path";
7
- import * as nodePty from "node-pty";
5
+ import { constants as constants$1, devNull, tmpdir, userInfo } from "node:os";
6
+ import { basename, delimiter, extname, isAbsolute, join, resolve } from "node:path";
7
+ import { createLazyRequire } from "@deepseek-ai/dsh-lazy-require";
8
8
  import { SubprocessExecutableNotFoundError, SubprocessRuntime } from "@deepseek-ai/dsh-subprocess";
9
9
  import { execFile, spawn, spawnSync } from "node:child_process";
10
10
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
@@ -159,6 +159,23 @@ var SystemdScopeOwner = class {
159
159
  this.query = query;
160
160
  this.sleep = sleep;
161
161
  }
162
+ inspectTaskCount() {
163
+ const result = this.runSync(this.systemctl, [
164
+ "--user",
165
+ "show",
166
+ "--property=LoadState",
167
+ "--property=ActiveState",
168
+ "--property=TasksCurrent",
169
+ this.unit
170
+ ], {
171
+ encoding: "utf8",
172
+ env: managerEnvironment(),
173
+ timeout: SYSTEMCTL_TIMEOUT_MS
174
+ });
175
+ if (result.error !== void 0 || result.status !== 0 || typeof result.stdout !== "string") return void 0;
176
+ const state = this.parseUnitState(result.stdout);
177
+ return state.loadState === "loaded" && state.activeState === "active" ? state.tasksCurrent : void 0;
178
+ }
162
179
  signal(signal) {
163
180
  if (this.stopped) return;
164
181
  this.terminationRequested = true;
@@ -580,6 +597,7 @@ function launchWindowsJob(spec, targetEnv, internals = {}) {
580
597
  ...spec.argv
581
598
  ], {
582
599
  cwd: process.cwd(),
600
+ windowsHide: true,
583
601
  env: runnerEnvironment(WINDOWS_RUNNER_SELECTION, invocation),
584
602
  stdio: runnerStdio(spec, true, ignoredStdinFd ?? "pipe")
585
603
  });
@@ -723,6 +741,9 @@ var LocalTerminalHandle = class {
723
741
  platform;
724
742
  managedOwner;
725
743
  resolveManagedOutcome;
744
+ shellActivity;
745
+ onQuiescence;
746
+ observeShellExit;
726
747
  pid;
727
748
  output = new PassThrough();
728
749
  done;
@@ -734,6 +755,10 @@ var LocalTerminalHandle = class {
734
755
  exited = false;
735
756
  outputPaused = false;
736
757
  trackedDescendants = [];
758
+ activityRevision = 0;
759
+ activityKey = "";
760
+ quiescent = false;
761
+ managedRangeEmpty = false;
737
762
  /** The spawned shell's start identity; scans stop adopting members once the root pid no longer carries it. */
738
763
  rootIdentity;
739
764
  /**
@@ -742,15 +767,22 @@ var LocalTerminalHandle = class {
742
767
  * @param graceMs - TERM-to-KILL and exit-wait grace.
743
768
  * @param platform - host platform; defaults to the running platform, injectable for deterministic tests.
744
769
  */
745
- constructor(terminal, inspector, graceMs, platform = process.platform, managedOwner, resolveManagedOutcome) {
770
+ constructor(terminal, inspector, graceMs, platform = process.platform, managedOwner, resolveManagedOutcome, shellActivity, onQuiescence, observeShellExit = false) {
746
771
  this.terminal = terminal;
747
772
  this.inspector = inspector;
748
773
  this.graceMs = graceMs;
749
774
  this.platform = platform;
750
775
  this.managedOwner = managedOwner;
751
776
  this.resolveManagedOutcome = resolveManagedOutcome;
777
+ this.shellActivity = shellActivity;
778
+ this.onQuiescence = onQuiescence;
779
+ this.observeShellExit = observeShellExit;
752
780
  this.pid = terminal.pid;
753
- this.rootIdentity = inspector.snapshot().tree(this.pid).find((member) => member.pid === this.pid);
781
+ try {
782
+ this.rootIdentity = inspector.snapshot().tree(this.pid).find((member) => member.pid === this.pid);
783
+ } catch (_rootIdentityUnavailable) {
784
+ this.rootIdentity = void 0;
785
+ }
754
786
  this.done = this.outcome.promise;
755
787
  const resume = () => {
756
788
  if (!this.outputPaused) return;
@@ -770,6 +802,9 @@ var LocalTerminalHandle = class {
770
802
  this.exitDisposable = terminal.onExit(({ exitCode, signal: exitSignal }) => {
771
803
  if (this.exited) return;
772
804
  this.exited = true;
805
+ if (this.managedOwner !== void 0 && this.observeShellExit) this.managedOwner.waitForExit().then(() => {
806
+ this.managedRangeEmpty = true;
807
+ }).catch(() => {});
773
808
  this.output.end();
774
809
  const outcome = {
775
810
  exitCode: exitSignal === void 0 || exitSignal === 0 ? exitCode : null,
@@ -788,6 +823,7 @@ var LocalTerminalHandle = class {
788
823
  }
789
824
  async write(data) {
790
825
  if (this.exited) throw new Error("terminal process has exited");
826
+ this.shellActivity?.invalidate();
791
827
  this.terminal.write(data);
792
828
  }
793
829
  async resize(cols, rows) {
@@ -803,7 +839,44 @@ var LocalTerminalHandle = class {
803
839
  inputWaiting: this.inspector.isStdinWaiting(processGroupId, this.pid)
804
840
  };
805
841
  }
842
+ async inspectActivity() {
843
+ let state = this.quiescent ? "idle" : "unknown";
844
+ let revision = 0;
845
+ if (!this.quiescent) try {
846
+ const shell = this.shellActivity?.inspect(this.pid) ?? {
847
+ state: "unknown",
848
+ revision: 0
849
+ };
850
+ revision = shell.revision;
851
+ const observed = this.inspector.snapshot();
852
+ const descendants = this.descendants(observed);
853
+ const root = observed.tree(this.pid).find((member) => member.pid === this.pid);
854
+ if (this.exited && this.managedRangeEmpty) state = "idle";
855
+ else if (descendants.length > 0) state = "busy";
856
+ else if (this.exited && this.managedOwner === void 0 && this.platform === "linux" && observed.complete === true && root === void 0) state = observed.session(this.pid).some((member) => observed.alive(member)) ? "busy" : "idle";
857
+ else if (observed.complete === true && root?.started === this.rootIdentity?.started && root !== void 0) {
858
+ const foreground = this.inspector.foregroundPgid(this.pid);
859
+ state = foreground === void 0 ? "unknown" : foreground === this.pid ? shell.state : "busy";
860
+ if (state === "idle" && this.managedOwner !== void 0) {
861
+ const tasks = this.managedOwner.inspectTaskCount?.();
862
+ state = tasks === void 0 || tasks < 1 ? "unknown" : tasks === 1 ? "idle" : "busy";
863
+ }
864
+ }
865
+ } catch (_incompleteActivityObservation) {
866
+ state = "unknown";
867
+ }
868
+ const key = `${revision}:${state}`;
869
+ if (key !== this.activityKey) {
870
+ this.activityKey = key;
871
+ this.activityRevision++;
872
+ }
873
+ return {
874
+ state,
875
+ revision: this.activityRevision
876
+ };
877
+ }
806
878
  async signalForeground(signal) {
879
+ this.shellActivity?.invalidate();
807
880
  const foreground = await this.inspectForeground();
808
881
  if (foreground === void 0) throw new Error(`cannot resolve foreground process group for terminal ${this.pid}`);
809
882
  if (signal === "SIGKILL" && foreground.processGroupId === this.pid) throw new Error("refusing to SIGKILL the terminal shell; terminate the terminal session instead");
@@ -823,7 +896,11 @@ var LocalTerminalHandle = class {
823
896
  this.outputPaused = false;
824
897
  this.terminal.resume();
825
898
  }
826
- const cleanup = this.closeOnce();
899
+ const cleanup = this.closeOnce().then(() => {
900
+ this.quiescent = true;
901
+ this.shellActivity?.dispose();
902
+ this.onQuiescence?.();
903
+ });
827
904
  this.cleanup = cleanup;
828
905
  cleanup.catch(() => {
829
906
  this.cleanup = void 0;
@@ -1013,6 +1090,164 @@ var LocalTerminalHandle = class {
1013
1090
  }
1014
1091
  };
1015
1092
  //#endregion
1093
+ //#region lib/types/shell-activity.js
1094
+ /** Private shell lifecycle files; ordinary output never authorizes terminal reclamation. */
1095
+ function quote(value) {
1096
+ return `'${value.replaceAll("'", "'\\''")}'`;
1097
+ }
1098
+ /** Opt-in startup integration and revision tracking for one ordinary interactive shell. */
1099
+ var ShellActivity = class {
1100
+ directory;
1101
+ argv;
1102
+ env;
1103
+ revision = 0;
1104
+ observed = "";
1105
+ invalidated;
1106
+ state = "unknown";
1107
+ /**
1108
+ * @param directory - private startup and status file directory.
1109
+ * @param argv - shell launch preserving supported user startup files.
1110
+ * @param env - environment with any temporary startup redirect.
1111
+ */
1112
+ constructor(directory, argv, env) {
1113
+ this.directory = directory;
1114
+ this.argv = argv;
1115
+ this.env = env;
1116
+ }
1117
+ /** Invalidate prompt evidence before delivering input or a foreground signal. */
1118
+ invalidate() {
1119
+ this.invalidated = this.read();
1120
+ this.revision++;
1121
+ this.state = "unknown";
1122
+ }
1123
+ /**
1124
+ * Read the latest top-level shell transition, fenced against input since that transition.
1125
+ * @param pid - original shell process id.
1126
+ * @returns lifecycle evidence; process ownership must be checked separately.
1127
+ */
1128
+ inspect(pid) {
1129
+ const record = this.read();
1130
+ if (record !== this.observed) {
1131
+ this.observed = record;
1132
+ this.revision++;
1133
+ }
1134
+ const match = /^(\d+):(\d+):(idle|busy)\n?$/u.exec(record);
1135
+ this.state = record === this.invalidated || match?.[1] !== String(pid) ? "unknown" : match[3];
1136
+ return {
1137
+ state: this.state,
1138
+ revision: this.revision
1139
+ };
1140
+ }
1141
+ /** Remove private startup and status files after process quiescence. */
1142
+ dispose() {
1143
+ rmSync(this.directory, {
1144
+ recursive: true,
1145
+ force: true
1146
+ });
1147
+ }
1148
+ read() {
1149
+ try {
1150
+ return readFileSync(join(this.directory, "state"), "utf8");
1151
+ } catch (_unavailableShellObservation) {
1152
+ return "";
1153
+ }
1154
+ }
1155
+ };
1156
+ /**
1157
+ * Prepare optional Bash or Zsh integration for a plain, non-login interactive launch.
1158
+ * @param spec - terminal request; custom arguments and wrapped executables remain unmodified.
1159
+ * @param env - scrubbed target environment.
1160
+ * @param platform - execution platform.
1161
+ * @returns private integration, or undefined for unsupported launches.
1162
+ */
1163
+ function prepareShellActivity(spec, env, platform) {
1164
+ if (spec.shellActivity !== true || platform === "win32" || spec.argv.length !== 2 || spec.argv[1] !== "-i") return void 0;
1165
+ const shell = basename(spec.argv[0]);
1166
+ if (shell !== "bash" && shell !== "zsh") return void 0;
1167
+ const directory = mkdtempSync(join(tmpdir(), "dsh-shell-"));
1168
+ const state = quote(join(directory, "state"));
1169
+ const guards = quote(join(directory, "guards"));
1170
+ try {
1171
+ if (shell === "bash") {
1172
+ const rc = join(directory, "bashrc");
1173
+ writeFileSync(rc, [
1174
+ "[[ ! -r ~/.bashrc ]] || builtin source ~/.bashrc",
1175
+ "if (( BASH_VERSINFO[0] > 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 4) )) && [[ ! $(declare -p PROMPT_COMMAND PS0 2>/dev/null) =~ declare\\ -[^[:space:]]*r ]]; then",
1176
+ " __dsh_shell_pid=$BASHPID; __dsh_shell_sequence=0",
1177
+ " __dsh_shell_idle() {",
1178
+ " local result=$?",
1179
+ " if [[ $BASHPID == \"$__dsh_shell_pid\" ]]; then",
1180
+ " (( ++__dsh_shell_sequence ))",
1181
+ " local activity=idle",
1182
+ ` builtin trap -p >| ${guards}`,
1183
+ ` [[ ! -s ${guards} ]] || activity=unknown`,
1184
+ ` builtin printf '%s:%s:%s\\n' "$BASHPID" "$__dsh_shell_sequence" "$activity" >| ${state}`,
1185
+ " fi",
1186
+ " return \"$result\"",
1187
+ " }",
1188
+ " if [[ $(declare -p PROMPT_COMMAND 2>/dev/null) == \"declare -a \"* ]]; then",
1189
+ " PROMPT_COMMAND+=(__dsh_shell_idle)",
1190
+ " else",
1191
+ " PROMPT_COMMAND=\"${PROMPT_COMMAND}\"$'\\n'\"__dsh_shell_idle\"",
1192
+ " fi",
1193
+ ` PS0+=${quote(`$(builtin printf '%s:%s:busy' "$__dsh_shell_pid" "$__dsh_shell_sequence" >| ${state})`)}`,
1194
+ "fi",
1195
+ ""
1196
+ ].join("\n"), {
1197
+ mode: 384,
1198
+ flag: "wx"
1199
+ });
1200
+ return new ShellActivity(directory, [
1201
+ spec.argv[0],
1202
+ "--rcfile",
1203
+ rc,
1204
+ "-i"
1205
+ ], env);
1206
+ }
1207
+ writeFileSync(join(directory, ".zshenv"), [
1208
+ env.ZDOTDIR === void 0 ? "unset ZDOTDIR" : `ZDOTDIR=${quote(env.ZDOTDIR)}`,
1209
+ "[[ ! -r ${ZDOTDIR:-$HOME}/.zshenv ]] || builtin source \"${ZDOTDIR:-$HOME}/.zshenv\"",
1210
+ "typeset -g __dsh_shell_pid=$$ __dsh_shell_sequence=0",
1211
+ "__dsh_shell_activity() {",
1212
+ " (( ZSH_SUBSHELL == 0 && $$ == __dsh_shell_pid )) || return",
1213
+ " (( ++__dsh_shell_sequence ))",
1214
+ ` builtin printf '%s:%s:%s\\n' "$$" "$__dsh_shell_sequence" "$1" >| ${state}`,
1215
+ " return 0",
1216
+ "}",
1217
+ "__dsh_shell_idle() {",
1218
+ ` { builtin trap; zle -F; } >| ${guards}`,
1219
+ " if [[ $CONTEXT != start || -n $BUFFER ]]; then __dsh_shell_activity busy",
1220
+ ` elif [[ -s ${guards} || -n \${(k)functions[(I)TRAP*]} ]]; then __dsh_shell_activity unknown`,
1221
+ " else __dsh_shell_activity idle; fi",
1222
+ "}",
1223
+ "__dsh_shell_busy() { __dsh_shell_activity busy }",
1224
+ "__dsh_shell_init() {",
1225
+ " autoload -Uz add-zle-hook-widget add-zsh-hook",
1226
+ " add-zle-hook-widget line-init __dsh_shell_idle",
1227
+ " add-zle-hook-widget line-finish __dsh_shell_busy",
1228
+ " add-zsh-hook preexec __dsh_shell_busy",
1229
+ " precmd_functions=(${precmd_functions:#__dsh_shell_init})",
1230
+ "}",
1231
+ "typeset -ga precmd_functions",
1232
+ "precmd_functions+=(__dsh_shell_init)",
1233
+ ""
1234
+ ].join("\n"), {
1235
+ mode: 384,
1236
+ flag: "wx"
1237
+ });
1238
+ return new ShellActivity(directory, spec.argv, {
1239
+ ...env,
1240
+ ZDOTDIR: directory
1241
+ });
1242
+ } catch (error) {
1243
+ rmSync(directory, {
1244
+ recursive: true,
1245
+ force: true
1246
+ });
1247
+ throw error;
1248
+ }
1249
+ }
1250
+ //#endregion
1016
1251
  //#region lib/types/index.js
1017
1252
  /**
1018
1253
  * Local Service Provider for the subprocess capability seam. Each spawn owns a
@@ -1023,6 +1258,7 @@ var LocalTerminalHandle = class {
1023
1258
  * stay with the caller's config (the bash executor's, the LSP host's, …).
1024
1259
  * @module @deepseek-ai/dsh-subprocess-local
1025
1260
  */
1261
+ const requireNodePty = createLazyRequire("node-pty", import.meta.url);
1026
1262
  /**
1027
1263
  * Local subprocess service: platform-selected managed ranges, Node-shaped stdio
1028
1264
  * dispositions (raw pipes, inherit, bounded tail-keep collection with spill
@@ -1174,32 +1410,41 @@ var LocalSubprocessRuntime = class extends SubprocessRuntime {
1174
1410
  const file = spec.argv[0];
1175
1411
  if (file === void 0 || file.length === 0) throw new Error("subprocess-local: terminal argv must contain a program");
1176
1412
  spec.signal?.throwIfAborted();
1413
+ const inspector = this.terminalInspector ?? createProcessInspector();
1414
+ const containmentMode = this.selectContainmentMode("terminal");
1177
1415
  const env = targetEnvironment(spec);
1416
+ const activity = prepareShellActivity(spec, env, this.internals.platform ?? process.platform);
1417
+ const launch = activity === void 0 ? spec : {
1418
+ ...spec,
1419
+ argv: activity.argv,
1420
+ env: activity.env
1421
+ };
1178
1422
  const options = {
1179
1423
  name: spec.terminalType,
1180
1424
  rows: spec.rows,
1181
1425
  cols: spec.cols,
1182
1426
  cwd: spec.cwd,
1183
1427
  env: {
1184
- ...env,
1428
+ ...activity?.env ?? env,
1185
1429
  TERM: spec.terminalType
1186
1430
  }
1187
1431
  };
1188
- const inspector = this.terminalInspector ?? createProcessInspector();
1189
- const scope = this.selectContainmentMode("terminal") === "linux-scope" ? prepareLinuxTerminalScope(spec, {
1190
- ...env,
1191
- PWD: spec.cwd,
1192
- TERM: spec.terminalType
1193
- }) : void 0;
1194
- if (scope !== void 0) {
1195
- options.cwd = scope.cwd;
1196
- options.env = scope.env;
1197
- }
1432
+ let scope;
1198
1433
  let terminal;
1199
1434
  try {
1200
- terminal = nodePty.spawn(scope?.command ?? file, scope?.args ?? [...spec.argv.slice(1)], options);
1435
+ scope = containmentMode === "linux-scope" ? prepareLinuxTerminalScope(launch, {
1436
+ ...activity?.env ?? env,
1437
+ PWD: spec.cwd,
1438
+ TERM: spec.terminalType
1439
+ }) : void 0;
1440
+ if (scope !== void 0) {
1441
+ options.cwd = scope.cwd;
1442
+ options.env = scope.env;
1443
+ }
1444
+ terminal = requireNodePty().spawn(scope?.command ?? file, scope?.args ?? [...launch.argv.slice(1)], options);
1201
1445
  } catch (error) {
1202
1446
  scope?.cleanup();
1447
+ activity?.dispose();
1203
1448
  throw error;
1204
1449
  }
1205
1450
  let handle;
@@ -1209,10 +1454,13 @@ var LocalSubprocessRuntime = class extends SubprocessRuntime {
1209
1454
  settled: directSettlement.promise,
1210
1455
  signal: (signal) => signalLinuxDirectProcess(terminal.pid, () => process.kill(terminal.pid, signal))
1211
1456
  });
1212
- handle = new LocalTerminalHandle(terminal, inspector, spec.graceMs, this.internals.platform ?? process.platform, owner, scope?.resolveOutcome);
1457
+ handle = new LocalTerminalHandle(terminal, inspector, spec.graceMs, this.internals.platform ?? process.platform, owner, scope?.resolveOutcome, activity, () => {
1458
+ this.terminals.delete(handle);
1459
+ }, spec.shellActivity === true);
1213
1460
  this.terminals.add(handle);
1214
1461
  const release = async () => {
1215
1462
  directSettlement.resolve();
1463
+ if (spec.shellActivity === true) return;
1216
1464
  await handle.terminate();
1217
1465
  this.terminals.delete(handle);
1218
1466
  };
@@ -2,12 +2,12 @@ import { OutputCollector, prepareManagedProcessBinding } from "./output.js";
2
2
  import { accessSync, chmodSync, closeSync, constants, existsSync, lstatSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { basename, dirname, extname, isAbsolute, join } from "node:path";
5
+ import { createLazyRequire } from "@deepseek-ai/dsh-lazy-require";
5
6
  import { scrubbedParentEnv } from "@deepseek-ai/dsh-subprocess";
6
7
  import { execFileSync, spawn, spawnSync } from "node:child_process";
7
8
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
8
9
  import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
9
10
  import { SUBPROCESS_CONTROL_ENV, SUBPROCESS_CONTROL_FD } from "@deepseek-ai/dsh-subprocess/control";
10
- import koffi from "koffi";
11
11
  import { getSystemErrorMessage, getSystemErrorName, inspect } from "node:util";
12
12
  import { fileURLToPath } from "node:url";
13
13
  //#region lib/types/managed-owner.js
@@ -74,6 +74,7 @@ function controlEnvironment(env, control) {
74
74
  * an injectable internals boundary so suites can pin it on any host.
75
75
  * @module dsh-subprocess-local/windows-inspector
76
76
  */
77
+ const requireKoffi$1 = createLazyRequire("koffi", import.meta.url);
77
78
  /**
78
79
  * Walk a process table from one root in children-first order, retaining only
79
80
  * members whose start identity is readable (unreadable members are detector
@@ -175,7 +176,6 @@ function isInvalidHandle(value) {
175
176
  const asBigInt = value;
176
177
  return asBigInt === 0n || asBigInt === 18446744073709551615n || asBigInt === -1n;
177
178
  }
178
- const PVOID = koffi.pointer("void");
179
179
  /**
180
180
  * Resolve the koffi Win32 struct types once. Registration is lazy and cached
181
181
  * because koffi's type registry is global per process: test runners that
@@ -184,6 +184,8 @@ const PVOID = koffi.pointer("void");
184
184
  */
185
185
  function win32Structs() {
186
186
  if (cachedStructs !== void 0) return cachedStructs;
187
+ const koffi = requireKoffi$1();
188
+ const PVOID = koffi.pointer("void");
187
189
  const PROCESSENTRY32W = koffi.struct("PROCESSENTRY32W", {
188
190
  dwSize: "uint32",
189
191
  cntUsage: "uint32",
@@ -204,6 +206,7 @@ function win32Structs() {
204
206
  if (PROCESSENTRY32W.size !== 568) throw new Error(`PROCESSENTRY32W layout mismatch: koffi computed ${PROCESSENTRY32W.size}, Windows headers say 568`);
205
207
  /* v8 ignore stop */
206
208
  cachedStructs = {
209
+ PVOID,
207
210
  PROCESSENTRY32W,
208
211
  FILETIME
209
212
  };
@@ -220,7 +223,8 @@ let cachedBindings;
220
223
  */
221
224
  function win32Bindings() {
222
225
  if (cachedBindings !== void 0) return cachedBindings;
223
- const { PROCESSENTRY32W, FILETIME } = win32Structs();
226
+ const koffi = requireKoffi$1();
227
+ const { PVOID, PROCESSENTRY32W, FILETIME } = win32Structs();
224
228
  const kernel32 = koffi.load("kernel32.dll");
225
229
  const bind = (name, result, args) => kernel32.func("__stdcall", name, result, args);
226
230
  cachedBindings = {
@@ -252,10 +256,11 @@ function win32Bindings() {
252
256
  * @returns the branded allocation pointer.
253
257
  */
254
258
  function allocNative(type, count) {
255
- return koffi.alloc(type, count);
259
+ return requireKoffi$1().alloc(type, count);
256
260
  }
257
261
  /** Enumerate the current process table through Toolhelp32. */
258
262
  function snapshotWindowsProcesses(bindings) {
263
+ const koffi = requireKoffi$1();
259
264
  const { PROCESSENTRY32W } = win32Structs();
260
265
  const snapshot = bindings.createToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
261
266
  /* v8 ignore next -- an invalid snapshot for the process flag is not producible through the public API;
@@ -281,6 +286,7 @@ function snapshotWindowsProcesses(bindings) {
281
286
  }
282
287
  /** Read one process's creation identity and current wait state. */
283
288
  function windowsProcessState(bindings, pid) {
289
+ const koffi = requireKoffi$1();
284
290
  const { FILETIME } = win32Structs();
285
291
  const handle = bindings.openProcess(1052672, 0, pid);
286
292
  if (isInvalidHandle(handle)) return void 0;
@@ -417,7 +423,7 @@ function numericEntries(internals, path) {
417
423
  try {
418
424
  return internals.readDir(path).filter((entry) => /^\d+$/.test(entry)).map(Number);
419
425
  } catch (_unreadableProcDirectory) {
420
- return [];
426
+ return;
421
427
  }
422
428
  }
423
429
  function readSyscall(internals, pid, tid) {
@@ -516,9 +522,11 @@ function quiescent(state) {
516
522
  }
517
523
  var PosixProcessSnapshot = class {
518
524
  rows;
525
+ complete;
519
526
  byPid;
520
- constructor(rows) {
527
+ constructor(rows, complete) {
521
528
  this.rows = rows;
529
+ this.complete = complete;
522
530
  this.byPid = new Map(rows.map((row) => [row.pid, row]));
523
531
  }
524
532
  tree(rootPid) {
@@ -575,10 +583,10 @@ var LinuxProcessInspector = class extends PosixProcessInspector {
575
583
  if (shell === void 0) return false;
576
584
  const terminalDevice = readLinuxTerminalDevice(this.internals, shellPid, shell.ttyDevice);
577
585
  if (terminalDevice === void 0) return false;
578
- for (const pid of numericEntries(this.internals, "/proc")) {
586
+ for (const pid of numericEntries(this.internals, "/proc") ?? []) {
579
587
  const process = readLinuxStat(this.internals, pid);
580
588
  if (process?.pgrp !== pgid) continue;
581
- for (const tid of numericEntries(this.internals, `/proc/${pid}/task`)) {
589
+ for (const tid of numericEntries(this.internals, `/proc/${pid}/task`) ?? []) {
582
590
  const syscall = readSyscall(this.internals, pid, tid);
583
591
  if (syscall !== void 0 && syscallWaitsOnStdin(this.internals, pid, tid, syscall, tables) && readLinuxTerminalDevice(this.internals, pid, process.ttyDevice, tid) === terminalDevice) return true;
584
592
  }
@@ -590,8 +598,12 @@ var LinuxProcessInspector = class extends PosixProcessInspector {
590
598
  return stat?.started === identity.started && !quiescent(stat.state);
591
599
  }
592
600
  snapshot() {
593
- return new PosixProcessSnapshot(numericEntries(this.internals, "/proc").flatMap((pid) => {
601
+ const pids = numericEntries(this.internals, "/proc");
602
+ if (pids === void 0) throw new Error("Cannot inspect processes: /proc directory is unreadable");
603
+ let complete = true;
604
+ return new PosixProcessSnapshot(pids.flatMap((pid) => {
594
605
  const stat = readLinuxStat(this.internals, pid);
606
+ if (stat === void 0) complete = false;
595
607
  return stat === void 0 ? [] : [{
596
608
  pid,
597
609
  parentPid: stat.parentPid,
@@ -599,21 +611,28 @@ var LinuxProcessInspector = class extends PosixProcessInspector {
599
611
  session: stat.session,
600
612
  state: stat.state
601
613
  }];
602
- }));
614
+ }), complete);
603
615
  }
604
616
  };
605
617
  function macProcessTable(internals) {
606
- return internals.exec("/bin/ps", ["-axo", "pid=,ppid=,lstart="]).split("\n").flatMap((line) => {
607
- const match = /^\s*(\d+)\s+(\d+)\s+(.+?)\s*$/.exec(line);
608
- if (match?.[1] === void 0 || match[2] === void 0 || match[3] === void 0) return [];
609
- return [{
610
- pid: Number(match[1]),
611
- parentPid: Number(match[2]),
612
- started: match[3],
613
- session: void 0,
614
- state: void 0
615
- }];
616
- });
618
+ let complete = true;
619
+ return {
620
+ rows: internals.exec("/bin/ps", ["-axo", "pid=,ppid=,lstart="]).split("\n").flatMap((line) => {
621
+ const match = /^\s*(\d+)\s+(\d+)\s+(.+?)\s*$/.exec(line);
622
+ if (match?.[1] === void 0 || match[2] === void 0 || match[3] === void 0) {
623
+ if (line.trim().length > 0) complete = false;
624
+ return [];
625
+ }
626
+ return [{
627
+ pid: Number(match[1]),
628
+ parentPid: Number(match[2]),
629
+ started: match[3],
630
+ session: void 0,
631
+ state: void 0
632
+ }];
633
+ }),
634
+ complete
635
+ };
617
636
  }
618
637
  var MacProcessInspector = class extends PosixProcessInspector {
619
638
  foregroundPgid(shellPid) {
@@ -633,10 +652,11 @@ var MacProcessInspector = class extends PosixProcessInspector {
633
652
  return false;
634
653
  }
635
654
  isAlive(identity) {
636
- return macProcessTable(this.internals).some((entry) => entry.pid === identity.pid && entry.started === identity.started);
655
+ return macProcessTable(this.internals).rows.some((entry) => entry.pid === identity.pid && entry.started === identity.started);
637
656
  }
638
657
  snapshot() {
639
- return new PosixProcessSnapshot(macProcessTable(this.internals));
658
+ const table = macProcessTable(this.internals);
659
+ return new PosixProcessSnapshot(table.rows, table.complete);
640
660
  }
641
661
  };
642
662
  /**
@@ -1028,6 +1048,7 @@ function spawnSubprocess(spec, internals = {}) {
1028
1048
  //#endregion
1029
1049
  //#region lib/types/linux-execve.js
1030
1050
  /** Lazy libc execve and descriptor bindings used by the one-shot Linux bootstrap. */
1051
+ const requireKoffi = createLazyRequire("koffi", import.meta.url);
1031
1052
  const STANDARD_FILE_DESCRIPTORS = [
1032
1053
  0,
1033
1054
  1,
@@ -1055,6 +1076,7 @@ function systemError(errno, syscall, path) {
1055
1076
  */
1056
1077
  function loadLinuxExecve() {
1057
1078
  if (cachedExecve !== void 0) return cachedExecve;
1079
+ const koffi = requireKoffi();
1058
1080
  const libc = koffi.load(null);
1059
1081
  const nativeExecve = libc.func("int execve(const char *pathname, const char **argv, const char **envp)");
1060
1082
  const nativeFcntl = libc.func("int fcntl(int fd, int cmd, int arg)");
package/lib/runner.js CHANGED
@@ -1,4 +1,4 @@
1
- import { S as loadLinuxExecve, a as resolveWindowsExecutable, b as serializeRunnerError, f as consumeLinuxLaunchRequest, g as linuxLaunchFilesFromLocator, h as isWindowsTerminateRequest, i as parseRunnerTargetArgv, r as consumeRunnerSelection, t as SUBPROCESS_RUNNER_ENV, v as parseWindowsStartRequest, x as writeLinuxStartupError } from "./runner-launch-BnjugUhn.js";
1
+ import { S as loadLinuxExecve, a as resolveWindowsExecutable, b as serializeRunnerError, f as consumeLinuxLaunchRequest, g as linuxLaunchFilesFromLocator, h as isWindowsTerminateRequest, i as parseRunnerTargetArgv, r as consumeRunnerSelection, t as SUBPROCESS_RUNNER_ENV, v as parseWindowsStartRequest, x as writeLinuxStartupError } from "./runner-launch-DGV26RBf.js";
2
2
  import { closeSync } from "node:fs";
3
3
  import { SUBPROCESS_CONTROL_FD } from "@deepseek-ai/dsh-subprocess/control";
4
4
  import { Win32Error, closeHandleChecked, isJobEmpty, loadWin32ProcessBindings, pollProcessExit, spawnCurrentTokenJobProcess, terminateJob } from "@deepseek-ai/dsh-win32-process";
@@ -7,6 +7,11 @@ export interface BoundProcessOwner {
7
7
  signal(signal: 'SIGTERM' | 'SIGKILL', cancellationReason?: unknown): void;
8
8
  /** Wait for the same managed range to become empty; reject when it cannot be observed. */
9
9
  waitForExit(): Promise<void>;
10
+ /**
11
+ * Count tasks in the native range, including descendants outside the observable process tree.
12
+ * @returns the current count, or undefined when observation is unavailable.
13
+ */
14
+ inspectTaskCount?(): number | undefined;
10
15
  /** Synchronously force final termination during JavaScript-observable host exit. */
11
16
  terminateForHostExit(): void;
12
17
  /** Release provider-private protocol artifacts after outcome and range settlement. */
@@ -25,6 +25,8 @@ interface FileStatus {
25
25
  * is the fence a signal takes, because it reads current state instead.
26
26
  */
27
27
  export interface ProcessSnapshot {
28
+ /** Whether the process-table scan omitted no unreadable rows; absent means unverified. */
29
+ readonly complete?: boolean;
28
30
  /**
29
31
  * Return the root and its transitive descendants as observed, children first.
30
32
  * @param rootPid - tree root to descend from.
@@ -58,6 +60,7 @@ export interface ProcessInspector {
58
60
  /**
59
61
  * Read the process table once and answer tree, session, and liveness from it.
60
62
  * @returns A process-table observation whose reads are shared.
63
+ * @throws when the platform process table cannot be enumerated.
61
64
  */
62
65
  snapshot(): ProcessSnapshot;
63
66
  /**
@@ -0,0 +1,37 @@
1
+ import type { SubprocessTerminalActivity, SubprocessTerminalSpawnSpec } from '@deepseek-ai/dsh-subprocess';
2
+ /** Opt-in startup integration and revision tracking for one ordinary interactive shell. */
3
+ export declare class ShellActivity {
4
+ private readonly directory;
5
+ readonly argv: readonly string[];
6
+ readonly env: Record<string, string>;
7
+ private revision;
8
+ private observed;
9
+ private invalidated;
10
+ private state;
11
+ /**
12
+ * @param directory - private startup and status file directory.
13
+ * @param argv - shell launch preserving supported user startup files.
14
+ * @param env - environment with any temporary startup redirect.
15
+ */
16
+ constructor(directory: string, argv: readonly string[], env: Record<string, string>);
17
+ /** Invalidate prompt evidence before delivering input or a foreground signal. */
18
+ invalidate(): void;
19
+ /**
20
+ * Read the latest top-level shell transition, fenced against input since that transition.
21
+ * @param pid - original shell process id.
22
+ * @returns lifecycle evidence; process ownership must be checked separately.
23
+ */
24
+ inspect(pid: number): SubprocessTerminalActivity;
25
+ /** Remove private startup and status files after process quiescence. */
26
+ dispose(): void;
27
+ private read;
28
+ }
29
+ /**
30
+ * Prepare optional Bash or Zsh integration for a plain, non-login interactive launch.
31
+ * @param spec - terminal request; custom arguments and wrapped executables remain unmodified.
32
+ * @param env - scrubbed target environment.
33
+ * @param platform - execution platform.
34
+ * @returns private integration, or undefined for unsupported launches.
35
+ */
36
+ export declare function prepareShellActivity(spec: SubprocessTerminalSpawnSpec, env: Record<string, string>, platform: NodeJS.Platform): ShellActivity | undefined;
37
+ //# sourceMappingURL=shell-activity.d.ts.map
@@ -1,9 +1,10 @@
1
1
  /** Local node-pty terminal-process implementation for the subprocess seam. */
2
2
  import { PassThrough } from 'node:stream';
3
3
  import type { IPty } from 'node-pty';
4
- import type { SubprocessOutcome, SubprocessTerminalForeground, SubprocessTerminalHandle, SubprocessTerminalSignal } from '@deepseek-ai/dsh-subprocess';
4
+ import type { SubprocessOutcome, SubprocessTerminalActivity, SubprocessTerminalForeground, SubprocessTerminalHandle, SubprocessTerminalSignal } from '@deepseek-ai/dsh-subprocess';
5
5
  import type { BoundProcessOwner } from './managed-owner.ts';
6
6
  import type { ProcessInspector } from './process-inspector.ts';
7
+ import type { ShellActivity } from './shell-activity.ts';
7
8
  /**
8
9
  * A local terminal whose native managed range or fallback process-session
9
10
  * ownership stays below the PTY backend.
@@ -20,6 +21,9 @@ export declare class LocalTerminalHandle implements SubprocessTerminalHandle {
20
21
  private readonly platform;
21
22
  private readonly managedOwner?;
22
23
  private readonly resolveManagedOutcome?;
24
+ private readonly shellActivity?;
25
+ private readonly onQuiescence?;
26
+ private readonly observeShellExit;
23
27
  readonly pid: number;
24
28
  readonly output: PassThrough;
25
29
  readonly done: Promise<SubprocessOutcome>;
@@ -31,6 +35,10 @@ export declare class LocalTerminalHandle implements SubprocessTerminalHandle {
31
35
  private exited;
32
36
  private outputPaused;
33
37
  private trackedDescendants;
38
+ private activityRevision;
39
+ private activityKey;
40
+ private quiescent;
41
+ private managedRangeEmpty;
34
42
  /** The spawned shell's start identity; scans stop adopting members once the root pid no longer carries it. */
35
43
  private readonly rootIdentity;
36
44
  /**
@@ -39,12 +47,13 @@ export declare class LocalTerminalHandle implements SubprocessTerminalHandle {
39
47
  * @param graceMs - TERM-to-KILL and exit-wait grace.
40
48
  * @param platform - host platform; defaults to the running platform, injectable for deterministic tests.
41
49
  */
42
- constructor(terminal: IPty, inspector: ProcessInspector, graceMs: number, platform?: NodeJS.Platform, managedOwner?: BoundProcessOwner | undefined, resolveManagedOutcome?: ((outcome: SubprocessOutcome) => SubprocessOutcome) | undefined);
50
+ constructor(terminal: IPty, inspector: ProcessInspector, graceMs: number, platform?: NodeJS.Platform, managedOwner?: BoundProcessOwner | undefined, resolveManagedOutcome?: ((outcome: SubprocessOutcome) => SubprocessOutcome) | undefined, shellActivity?: Pick<ShellActivity, "inspect" | "invalidate" | "dispose"> | undefined, onQuiescence?: (() => void) | undefined, observeShellExit?: boolean);
43
51
  /** Whether node-pty has not yet published the top-level exit event. */
44
52
  get running(): boolean;
45
53
  write(data: string): Promise<void>;
46
54
  resize(cols: number, rows: number): Promise<void>;
47
55
  inspectForeground(): Promise<SubprocessTerminalForeground | undefined>;
56
+ inspectActivity(): Promise<SubprocessTerminalActivity>;
48
57
  signalForeground(signal: SubprocessTerminalSignal): Promise<number>;
49
58
  terminate(): Promise<void>;
50
59
  /**
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.1.6-alpha.1",
4
+ "version": "0.1.6-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -39,19 +39,20 @@
39
39
  ],
40
40
  "license": "MIT",
41
41
  "peerDependencies": {
42
- "@deepseek-ai/dsh-subprocess": "^0.1.6-alpha.1",
43
- "@deepseek-ai/dsh-timeout": "^0.1.6-alpha.1",
44
- "@deepseek-ai/cordis": "^4.0.2"
42
+ "@deepseek-ai/dsh-subprocess": "^0.1.6-alpha.2",
43
+ "@deepseek-ai/cordis": "^4.0.2",
44
+ "@deepseek-ai/dsh-timeout": "^0.1.6-alpha.2"
45
45
  },
46
46
  "dependencies": {
47
47
  "koffi": "^3.1.0",
48
48
  "node-pty": "1.2.0-beta.15",
49
- "@deepseek-ai/dsh-win32-process": "^0.1.6-alpha.1"
49
+ "@deepseek-ai/dsh-lazy-require": "^0.1.6-alpha.2",
50
+ "@deepseek-ai/dsh-win32-process": "^0.1.6-alpha.2"
50
51
  },
51
52
  "devDependencies": {
52
- "@deepseek-ai/dsh-loader-smoke": "^0.1.6-alpha.1",
53
- "@deepseek-ai/dsh-subprocess": "^0.1.6-alpha.1",
54
- "@deepseek-ai/dsh-timeout": "^0.1.6-alpha.1",
53
+ "@deepseek-ai/dsh-subprocess": "^0.1.6-alpha.2",
54
+ "@deepseek-ai/dsh-loader-smoke": "^0.1.6-alpha.2",
55
+ "@deepseek-ai/dsh-timeout": "^0.1.6-alpha.2",
55
56
  "@deepseek-ai/cordis": "^4.0.2"
56
57
  },
57
58
  "scripts": {