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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1,13 +1,14 @@
1
- import { C as bindManagedProcess, D as validateSubprocessSpec, E as spawnSubprocess, O as createProcessInspector, S as loadLinuxExecve, T as prepareManagedProcessBinding, _ 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-COYGu0Dl.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-BnjugUhn.js";
2
+ import { prepareManagedProcessBinding } from "./output.js";
2
3
  import { closeSync, constants, existsSync, openSync } from "node:fs";
3
4
  import { access, stat } from "node:fs/promises";
5
+ import { constants as constants$1, devNull, userInfo } from "node:os";
4
6
  import { delimiter, extname, isAbsolute, resolve } from "node:path";
5
7
  import * as nodePty from "node-pty";
6
- import { SubprocessRuntime } from "@deepseek-ai/dsh-subprocess";
8
+ import { SubprocessExecutableNotFoundError, SubprocessRuntime } from "@deepseek-ai/dsh-subprocess";
7
9
  import { execFile, spawn, spawnSync } from "node:child_process";
8
- import { randomBytes } from "node:crypto";
9
- import { constants as constants$1, devNull } from "node:os";
10
10
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
11
+ import { randomBytes } from "node:crypto";
11
12
  import { loadWin32ProcessBindings, probeCurrentTokenJobSupport } from "@deepseek-ai/dsh-win32-process";
12
13
  import { Buffer } from "node:buffer";
13
14
  import { PassThrough } from "node:stream";
@@ -118,9 +119,24 @@ function probeLinuxManager(internals = {}) {
118
119
  function probeLinuxNative(internals = {}) {
119
120
  return probeLinuxBootstrap(internals) && probeLinuxScope(internals);
120
121
  }
122
+ var LinuxScopeStartup = class {
123
+ files;
124
+ kind;
125
+ terminationSignals = /* @__PURE__ */ new Set();
126
+ constructor(files, kind) {
127
+ this.files = files;
128
+ this.kind = kind;
129
+ }
130
+ resolveOutcome(outcome) {
131
+ const startup = readLinuxStartupError(this.files.startupErrorPath);
132
+ if (startup !== void 0) throw deserializeRunnerError(startup.error);
133
+ if (existsSync(this.files.requestPath) && !(outcome.signal !== null && this.terminationSignals.has(outcome.signal))) throw new Error(`${this.kind} scope exited before its bootstrap consumed the launch request`);
134
+ return outcome;
135
+ }
136
+ };
121
137
  var SystemdScopeOwner = class {
122
138
  unit;
123
- files;
139
+ startup;
124
140
  direct;
125
141
  systemctl;
126
142
  runSync;
@@ -128,13 +144,15 @@ var SystemdScopeOwner = class {
128
144
  sleep;
129
145
  establishment = "pending";
130
146
  stopped = false;
147
+ terminationRequested = false;
131
148
  observation;
132
149
  killFailure;
150
+ directKillSettlement;
133
151
  wakeGeneration = 0;
134
152
  wakeWaiter;
135
- constructor(unit, files, direct, systemctl, runSync, query, sleep) {
153
+ constructor(unit, startup, direct, systemctl, runSync, query, sleep) {
136
154
  this.unit = unit;
137
- this.files = files;
155
+ this.startup = startup;
138
156
  this.direct = direct;
139
157
  this.systemctl = systemctl;
140
158
  this.runSync = runSync;
@@ -143,9 +161,12 @@ var SystemdScopeOwner = class {
143
161
  }
144
162
  signal(signal) {
145
163
  if (this.stopped) return;
164
+ this.terminationRequested = true;
165
+ if (this.direct.running()) this.startup.terminationSignals.add(signal);
146
166
  this.observeRequestConsumption();
147
167
  const directFallbackRequired = this.establishment === "pending";
148
- if (directFallbackRequired && this.direct.running()) this.direct.signal(signal);
168
+ let directSignalled = false;
169
+ if (directFallbackRequired && this.direct.running()) directSignalled = this.direct.signal(signal);
149
170
  const result = this.runSync(this.systemctl, [
150
171
  "--user",
151
172
  "kill",
@@ -159,13 +180,19 @@ var SystemdScopeOwner = class {
159
180
  });
160
181
  this.wakeObservation();
161
182
  if (result.error === void 0 && result.status === 0) {
162
- if (signal === "SIGKILL") this.killFailure = void 0;
183
+ if (signal === "SIGKILL") {
184
+ this.killFailure = void 0;
185
+ this.directKillSettlement = void 0;
186
+ }
163
187
  return;
164
188
  }
165
- if (!directFallbackRequired && this.direct.running()) this.direct.signal(signal);
189
+ if (!directFallbackRequired && this.direct.running()) directSignalled = this.direct.signal(signal);
166
190
  if (signal === "SIGKILL") {
167
191
  const output = `${result.stdout}\n${result.stderr}`;
168
- if (!MISSING_UNIT.test(output)) this.killFailure = result.error ?? /* @__PURE__ */ new Error(`systemctl could not signal ${this.unit}: ${output.trim() || `exit ${String(result.status)}`}`);
192
+ if (!MISSING_UNIT.test(output)) {
193
+ this.killFailure = result.error ?? /* @__PURE__ */ new Error(`systemctl could not signal ${this.unit}: ${output.trim() || `exit ${String(result.status)}`}`);
194
+ this.directKillSettlement = directSignalled ? this.direct.settled.then(() => {}, () => {}) : void 0;
195
+ }
169
196
  }
170
197
  }
171
198
  terminateForHostExit() {
@@ -188,15 +215,40 @@ var SystemdScopeOwner = class {
188
215
  } catch {}
189
216
  }
190
217
  observeRequestConsumption() {
191
- if (this.establishment === "pending" && !existsSync(this.files.requestPath)) this.establishment = "established";
218
+ if (this.establishment === "pending" && !existsSync(this.startup.files.requestPath)) this.establishment = "established";
192
219
  }
193
220
  absentUnit() {
194
221
  this.observeRequestConsumption();
195
222
  if (this.establishment === "established") return false;
196
- if (!this.direct.running() && existsSync(this.files.requestPath)) return false;
223
+ if (!this.direct.running() && existsSync(this.startup.files.requestPath)) return false;
197
224
  if (this.killFailure !== void 0) throw this.killFailure;
198
225
  return true;
199
226
  }
227
+ /**
228
+ * Prove an active unit with no processes is the empty managed range rather
229
+ * than a launch still placing its payload. systemd ends a scope only on the
230
+ * populated-to-empty transition, so a payload killed before it entered the
231
+ * cgroup leaves the unit active forever. A departed client cannot add another
232
+ * payload; a consumed request proves the payload already entered the scope,
233
+ * even while its direct-process exit notification is pending.
234
+ */
235
+ emptyRange(tasksCurrent) {
236
+ return this.terminationRequested && tasksCurrent === 0 && (!this.direct.running() || !existsSync(this.startup.files.requestPath));
237
+ }
238
+ /** Release a leftover empty scope so the transient unit is collected and cannot accumulate. */
239
+ releaseEmptyRange() {
240
+ try {
241
+ this.runSync(this.systemctl, [
242
+ "--user",
243
+ "stop",
244
+ this.unit
245
+ ], {
246
+ env: managerEnvironment(),
247
+ stdio: "ignore",
248
+ timeout: SYSTEMCTL_TIMEOUT_MS
249
+ });
250
+ } catch {}
251
+ }
200
252
  parseUnitState(stdout) {
201
253
  const values = /* @__PURE__ */ new Map();
202
254
  for (const line of stdout.split(/\r?\n/u)) {
@@ -209,24 +261,32 @@ var SystemdScopeOwner = class {
209
261
  }
210
262
  const loadState = values.get("LoadState");
211
263
  const activeState = values.get("ActiveState");
212
- if (values.size !== 2 || loadState === void 0 || activeState === void 0) throw new Error(`systemctl returned incomplete state for ${this.unit}: ${JSON.stringify(stdout.trim())}`);
264
+ const reportedTasks = values.get("TasksCurrent");
265
+ const tasksCurrent = reportedTasks === "[not set]" ? void 0 : reportedTasks;
266
+ if (values.size !== (reportedTasks === void 0 ? 2 : 3) || loadState === void 0 || activeState === void 0) throw new Error(`systemctl returned incomplete state for ${this.unit}: ${JSON.stringify(stdout.trim())}`);
267
+ if (tasksCurrent !== void 0 && !/^\d+$/u.test(tasksCurrent)) throw new Error(`systemctl returned a non-numeric TasksCurrent for ${this.unit}: ${JSON.stringify(tasksCurrent)}`);
213
268
  return {
214
269
  loadState,
215
- activeState
270
+ activeState,
271
+ tasksCurrent: tasksCurrent === void 0 ? void 0 : Number(tasksCurrent)
216
272
  };
217
273
  }
218
274
  async rangeActive() {
219
275
  this.observeRequestConsumption();
276
+ const generation = this.wakeGeneration;
277
+ const directRunning = this.direct.running();
220
278
  const result = await this.query(this.systemctl, [
221
279
  "--user",
222
280
  "show",
223
281
  this.unit,
224
282
  "--property=LoadState",
225
- "--property=ActiveState"
283
+ "--property=ActiveState",
284
+ "--property=TasksCurrent"
226
285
  ]);
286
+ if (generation !== this.wakeGeneration) return true;
227
287
  const output = `${result.stdout}\n${result.stderr}`;
228
288
  if (result.status === 0) {
229
- const { loadState, activeState } = this.parseUnitState(result.stdout);
289
+ const { loadState, activeState, tasksCurrent } = this.parseUnitState(result.stdout);
230
290
  if (loadState === "not-found" && activeState === "inactive") return this.absentUnit();
231
291
  if (loadState !== "loaded") throw new Error(`systemctl returned unknown state for ${this.unit}: ${JSON.stringify({
232
292
  loadState,
@@ -240,7 +300,19 @@ var SystemdScopeOwner = class {
240
300
  "reloading",
241
301
  "deactivating"
242
302
  ].includes(activeState)) throw new Error(`systemctl returned unknown ActiveState for ${this.unit}: ${JSON.stringify(activeState)}`);
243
- if (this.killFailure !== void 0) throw this.killFailure;
303
+ if (this.emptyRange(tasksCurrent)) {
304
+ this.releaseEmptyRange();
305
+ return false;
306
+ }
307
+ if (this.killFailure !== void 0) {
308
+ if (directRunning && this.directKillSettlement !== void 0) {
309
+ const settlement = this.directKillSettlement;
310
+ this.directKillSettlement = void 0;
311
+ await settlement;
312
+ return this.rangeActive();
313
+ }
314
+ throw this.killFailure;
315
+ }
244
316
  return true;
245
317
  }
246
318
  if (!MISSING_UNIT.test(output)) {
@@ -288,7 +360,7 @@ var SystemdScopeOwner = class {
288
360
  await this.observation;
289
361
  }
290
362
  cleanup() {
291
- cleanupLinuxLaunchFiles(this.files);
363
+ cleanupLinuxLaunchFiles(this.startup.files);
292
364
  }
293
365
  };
294
366
  function scopeArgs(unitBase, invocation, argv) {
@@ -305,7 +377,7 @@ function scopeArgs(unitBase, invocation, argv) {
305
377
  ...argv
306
378
  ];
307
379
  }
308
- function directOutcome(child, files) {
380
+ function directOutcome(child, startup) {
309
381
  return new Promise((resolveOutcome, rejectOutcome) => {
310
382
  let settled = false;
311
383
  child.once("error", (error) => {
@@ -317,40 +389,47 @@ function directOutcome(child, files) {
317
389
  if (settled) return;
318
390
  settled = true;
319
391
  try {
320
- const startup = readLinuxStartupError(files.startupErrorPath);
321
- if (startup !== void 0) {
322
- rejectOutcome(deserializeRunnerError(startup.error));
323
- return;
324
- }
325
- if (existsSync(files.requestPath)) {
326
- rejectOutcome(/* @__PURE__ */ new Error("subprocess scope exited before its bootstrap consumed the launch request"));
327
- return;
328
- }
329
- resolveOutcome({
392
+ resolveOutcome(startup.resolveOutcome({
330
393
  exitCode,
331
394
  signal
332
- });
395
+ }));
333
396
  } catch (error) {
334
397
  rejectOutcome(error instanceof Error ? error : new Error(String(error)));
335
398
  }
336
399
  });
337
400
  });
338
401
  }
339
- function signalChildGroup(child, signal) {
402
+ /**
403
+ * Send a direct-process signal, distinguishing an absent PID from failed delivery.
404
+ * @param pid - owned direct-process identity whose exit notification can still be pending.
405
+ * @param send - platform signal operation; true means the signal was submitted.
406
+ * @returns whether the signal was submitted or the owned PID is already absent.
407
+ */
408
+ function signalLinuxDirectProcess(pid, send) {
340
409
  try {
341
- process.kill(-child.pid, signal);
342
- } catch {
343
- try {
344
- child.kill(signal);
345
- } catch {}
410
+ if (send()) return true;
411
+ } catch {}
412
+ try {
413
+ process.kill(pid, 0);
414
+ return false;
415
+ } catch (error) {
416
+ return error.code === "ESRCH";
346
417
  }
347
418
  }
419
+ function signalChildGroup(child, signal) {
420
+ let groupSignalled = false;
421
+ try {
422
+ groupSignalled = process.kill(-child.pid, signal);
423
+ } catch {}
424
+ if (groupSignalled && signal === "SIGTERM") return true;
425
+ return signalLinuxDirectProcess(child.pid, () => process.kill(child.pid, signal));
426
+ }
348
427
  /**
349
428
  * Prepare one Linux PTY scope using the same launch request and bootstrap core.
350
429
  * @param spec - terminal target request.
351
430
  * @param targetEnv - validated complete target environment.
352
431
  * @param internals - optional runner and systemd seams used by tests.
353
- * @returns invocation facts and ownership callbacks for node-pty.
432
+ * @returns invocation and ownership callbacks; requested termination preserves the observed signal even before bootstrap consumption.
354
433
  */
355
434
  function prepareLinuxTerminalScope(spec, targetEnv, internals = {}) {
356
435
  const invocation = internals.runnerInvocation ?? spawnRunnerInvocation();
@@ -358,19 +437,15 @@ function prepareLinuxTerminalScope(spec, targetEnv, internals = {}) {
358
437
  cwd: spec.cwd,
359
438
  env: targetEnv
360
439
  });
440
+ const startup = new LinuxScopeStartup(files, "terminal");
361
441
  const unitBase = unitStem("dsh-terminal");
362
442
  return {
363
443
  command: internals.systemdRun ?? "systemd-run",
364
444
  args: scopeArgs(unitBase, invocation, spec.argv),
365
445
  cwd: process.cwd(),
366
446
  env: runnerEnvironment(files.requestPath, invocation),
367
- bindOwner: (direct) => new SystemdScopeOwner(`${unitBase}.scope`, files, direct, internals.systemctl ?? "systemctl", internals.spawnSync ?? spawnSync, internals.systemctlQuery ?? querySystemctl, internals.sleep ?? sleepWithAbort),
368
- resolveOutcome: (outcome) => {
369
- const startup = readLinuxStartupError(files.startupErrorPath);
370
- if (startup !== void 0) throw deserializeRunnerError(startup.error);
371
- if (existsSync(files.requestPath)) throw new Error("terminal scope exited before its bootstrap consumed the launch request");
372
- return outcome;
373
- },
447
+ bindOwner: (direct) => new SystemdScopeOwner(`${unitBase}.scope`, startup, direct, internals.systemctl ?? "systemctl", internals.spawnSync ?? spawnSync, internals.systemctlQuery ?? querySystemctl, internals.sleep ?? sleepWithAbort),
448
+ resolveOutcome: (outcome) => startup.resolveOutcome(outcome),
374
449
  cleanup: () => {
375
450
  cleanupLinuxLaunchFiles(files);
376
451
  }
@@ -381,14 +456,16 @@ function prepareLinuxTerminalScope(spec, targetEnv, internals = {}) {
381
456
  * @param spec - ordinary target request.
382
457
  * @param targetEnv - validated complete target environment.
383
458
  * @param internals - optional runner and systemd seams used by tests.
384
- * @returns direct streams, result, and managed-scope owner.
459
+ * @returns streams, result, and scope owner; requested termination preserves the observed signal even before bootstrap consumption.
385
460
  */
386
461
  function launchLinuxScope(spec, targetEnv, internals = {}) {
387
462
  const invocation = internals.runnerInvocation ?? spawnRunnerInvocation();
388
463
  const files = createLinuxLaunchFiles({
389
464
  cwd: spec.cwd,
390
- env: targetEnv
465
+ env: targetEnv,
466
+ ...spec.stdio.control === void 0 ? {} : { control: spec.stdio.control }
391
467
  });
468
+ const startup = new LinuxScopeStartup(files, "subprocess");
392
469
  const unitBase = unitStem("dsh-subprocess");
393
470
  let child;
394
471
  try {
@@ -402,17 +479,18 @@ function launchLinuxScope(spec, targetEnv, internals = {}) {
402
479
  cleanupLinuxLaunchFiles(files);
403
480
  throw error;
404
481
  }
405
- const owner = new SystemdScopeOwner(`${unitBase}.scope`, files, {
482
+ const direct = directOutcome(child, startup);
483
+ const owner = new SystemdScopeOwner(`${unitBase}.scope`, startup, {
406
484
  running: () => child.pid !== void 0 && child.exitCode === null && child.signalCode === null,
407
- signal: (signal) => {
408
- signalChildGroup(child, signal);
409
- }
485
+ signal: (signal) => signalChildGroup(child, signal),
486
+ settled: direct
410
487
  }, internals.systemctl ?? "systemctl", internals.spawnSync ?? spawnSync, internals.systemctlQuery ?? querySystemctl, internals.sleep ?? sleepWithAbort);
411
488
  return {
412
489
  stdin: child.stdin,
413
490
  stdout: child.stdout,
414
491
  stderr: child.stderr,
415
- direct: directOutcome(child, files),
492
+ control: controlPipe(child, spec.stdio.control),
493
+ direct,
416
494
  owner
417
495
  };
418
496
  }
@@ -513,10 +591,25 @@ function launchWindowsJob(spec, targetEnv, internals = {}) {
513
591
  const rangeExit = Promise.withResolvers();
514
592
  let directResultType;
515
593
  let runnerSpawned = false;
594
+ let runnerExit;
595
+ let ipcDisconnected = false;
516
596
  const failInfrastructure = (error) => {
517
597
  direct.reject(error);
518
598
  rangeExit.reject(error);
519
599
  };
600
+ const settleRange = () => {
601
+ if (!runnerSpawned || runnerExit === void 0) return;
602
+ const { exitCode, signal } = runnerExit;
603
+ if (exitCode === 0 && signal === null) {
604
+ if (directResultType !== void 0) {
605
+ rangeExit.resolve();
606
+ return;
607
+ }
608
+ if (!ipcDisconnected) return;
609
+ }
610
+ const status = signal !== null ? `signal ${signal}` : exitCode === null ? "without an exit status" : `exit code ${String(exitCode)}`;
611
+ failInfrastructure(/* @__PURE__ */ new Error(`subprocess-local: Windows Job runner exited with ${status} before proving its managed range empty`));
612
+ };
520
613
  const owner = new WindowsJobOwner(child, rangeExit.promise, () => directResultType, failInfrastructure);
521
614
  child.on("message", (value) => {
522
615
  if (directResultType !== void 0) {
@@ -538,6 +631,7 @@ function launchWindowsJob(spec, targetEnv, internals = {}) {
538
631
  signal: null
539
632
  });
540
633
  else direct.reject(owner.mapStartFailure(deserializeRunnerError(result.error), result.error));
634
+ settleRange();
541
635
  });
542
636
  child.once("spawn", () => {
543
637
  runnerSpawned = true;
@@ -546,7 +640,8 @@ function launchWindowsJob(spec, targetEnv, internals = {}) {
546
640
  child.send({
547
641
  type: "start",
548
642
  cwd: spec.cwd,
549
- env: targetEnv
643
+ env: targetEnv,
644
+ ...spec.stdio.control === void 0 ? {} : { control: spec.stdio.control }
550
645
  }, (error) => {
551
646
  if (error === null) return;
552
647
  failInfrastructure(error);
@@ -565,19 +660,22 @@ function launchWindowsJob(spec, targetEnv, internals = {}) {
565
660
  }
566
661
  failInfrastructure(error);
567
662
  });
568
- child.once("close", (exitCode, signal) => {
569
- if (!runnerSpawned) return;
570
- if (exitCode === 0 && signal === null && directResultType !== void 0) {
571
- rangeExit.resolve();
572
- return;
573
- }
574
- const status = signal !== null ? `signal ${signal}` : exitCode === null ? "without an exit status" : `exit code ${String(exitCode)}`;
575
- failInfrastructure(/* @__PURE__ */ new Error(`subprocess-local: Windows Job runner exited with ${status} before proving its managed range empty`));
663
+ child.once("exit", (exitCode, signal) => {
664
+ runnerExit = {
665
+ exitCode,
666
+ signal
667
+ };
668
+ settleRange();
669
+ });
670
+ child.once("disconnect", () => {
671
+ ipcDisconnected = true;
672
+ settleRange();
576
673
  });
577
674
  return {
578
675
  stdin: spec.stdio.stdin === "ignore" ? null : targetStdin,
579
676
  stdout: child.stdio[5],
580
677
  stderr: child.stdio[6],
678
+ control: controlPipe(child, spec.stdio.control),
581
679
  direct: direct.promise,
582
680
  owner
583
681
  };
@@ -634,6 +732,7 @@ var LocalTerminalHandle = class {
634
732
  cleanup;
635
733
  managedOwnerCleaned = false;
636
734
  exited = false;
735
+ outputPaused = false;
637
736
  trackedDescendants = [];
638
737
  /** The spawned shell's start identity; scans stop adopting members once the root pid no longer carries it. */
639
738
  rootIdentity;
@@ -653,8 +752,20 @@ var LocalTerminalHandle = class {
653
752
  this.pid = terminal.pid;
654
753
  this.rootIdentity = inspector.snapshot().tree(this.pid).find((member) => member.pid === this.pid);
655
754
  this.done = this.outcome.promise;
755
+ const resume = () => {
756
+ if (!this.outputPaused) return;
757
+ this.outputPaused = false;
758
+ if (!this.exited) terminal.resume();
759
+ };
760
+ this.output.on("drain", resume);
761
+ this.output.once("close", () => {
762
+ this.output.off("drain", resume);
763
+ });
656
764
  this.dataDisposable = terminal.onData((data) => {
657
- this.output.write(Buffer.from(data, "utf8"));
765
+ if (!this.output.write(Buffer.from(data, "utf8")) && this.cleanup === void 0 && !this.outputPaused) {
766
+ this.outputPaused = true;
767
+ terminal.pause();
768
+ }
658
769
  });
659
770
  this.exitDisposable = terminal.onExit(({ exitCode, signal: exitSignal }) => {
660
771
  if (this.exited) return;
@@ -679,6 +790,10 @@ var LocalTerminalHandle = class {
679
790
  if (this.exited) throw new Error("terminal process has exited");
680
791
  this.terminal.write(data);
681
792
  }
793
+ async resize(cols, rows) {
794
+ if (this.exited) throw new Error("terminal process has exited");
795
+ this.terminal.resize(cols, rows);
796
+ }
682
797
  async inspectForeground() {
683
798
  this.descendants(this.inspector.snapshot());
684
799
  const processGroupId = this.inspector.foregroundPgid(this.pid);
@@ -704,6 +819,10 @@ var LocalTerminalHandle = class {
704
819
  }
705
820
  terminate() {
706
821
  if (this.cleanup !== void 0) return this.cleanup;
822
+ if (this.outputPaused) {
823
+ this.outputPaused = false;
824
+ this.terminal.resume();
825
+ }
707
826
  const cleanup = this.closeOnce();
708
827
  this.cleanup = cleanup;
709
828
  cleanup.catch(() => {
@@ -916,6 +1035,8 @@ var LocalSubprocessRuntime = class extends SubprocessRuntime {
916
1035
  live = /* @__PURE__ */ new Set();
917
1036
  /** Live terminals retained through normal quiescence or host-exit finalization. */
918
1037
  terminals = /* @__PURE__ */ new Set();
1038
+ /** Caller endpoints retained until close, independently of managed process lifetime. */
1039
+ controlChannels = /* @__PURE__ */ new Set();
919
1040
  /** Test hook: process, spill, and platform operations forwarded to spawnSubprocess. */
920
1041
  internals = {};
921
1042
  /** Provider-lifetime latch suppressing repeated weaker-containment warnings. */
@@ -957,6 +1078,13 @@ var LocalSubprocessRuntime = class extends SubprocessRuntime {
957
1078
  this.terminals.delete(terminal);
958
1079
  }));
959
1080
  const outcomes = await Promise.allSettled(pending);
1081
+ await Promise.all([...this.controlChannels].map((control) => new Promise((resolveClose) => {
1082
+ control.once("close", () => {
1083
+ resolveClose();
1084
+ });
1085
+ control.destroy();
1086
+ })));
1087
+ this.controlChannels.clear();
960
1088
  const failures = [];
961
1089
  for (const outcome of outcomes) if (outcome.status === "rejected") failures.push(outcome.reason);
962
1090
  if (failures.length > 0) this.terminateForHostExit();
@@ -980,7 +1108,7 @@ var LocalSubprocessRuntime = class extends SubprocessRuntime {
980
1108
  } catch {}
981
1109
  }
982
1110
  signal?.throwIfAborted();
983
- throw new Error(absolute ? `subprocess-local: command ${JSON.stringify(command)} is not an executable file` : `subprocess-local: command ${JSON.stringify(command)} was not found on PATH`);
1111
+ throw new SubprocessExecutableNotFoundError(absolute ? `subprocess-local: command ${JSON.stringify(command)} is not an executable file` : `subprocess-local: command ${JSON.stringify(command)} was not found on PATH`);
984
1112
  }
985
1113
  executableCandidates(command, env) {
986
1114
  const path = environmentValue(env, "PATH") ?? "";
@@ -998,6 +1126,13 @@ var LocalSubprocessRuntime = class extends SubprocessRuntime {
998
1126
  handle = bindManagedProcess(spec, containmentMode === "linux-scope" ? launchLinuxScope(spec, env) : launchWindowsJob(spec, env), binding);
999
1127
  }
1000
1128
  this.live.add(handle);
1129
+ const control = handle.control;
1130
+ if (control !== void 0) {
1131
+ this.controlChannels.add(control);
1132
+ control.once("close", () => {
1133
+ this.controlChannels.delete(control);
1134
+ });
1135
+ }
1001
1136
  const release = () => handle.waitForExit().then(() => {
1002
1137
  this.live.delete(handle);
1003
1138
  });
@@ -1025,23 +1160,36 @@ var LocalSubprocessRuntime = class extends SubprocessRuntime {
1025
1160
  const reason = selectedReason ?? (platform === "darwin" ? "macOS has no supported persistent process-range owner" : platform === "win32" ? kind === "terminal" ? "Windows ConPTY remains outside Job containment" : "the Win32 Job runner is unavailable" : `platform ${platform} has no native managed range`);
1026
1161
  this.ctx.logger.warn(`subprocess-local is using weaker process-tree containment because ${reason}; descendants that escape the process group or direct-parent tree are not guaranteed to terminate or delay waitForExit()`);
1027
1162
  }
1163
+ /** @inheritdoc */
1164
+ async terminalEnvironment(signal) {
1165
+ signal?.throwIfAborted();
1166
+ const platform = process.platform === "win32" ? "windows" : "posix";
1167
+ const defaultShell = platform === "windows" ? process.env.ComSpec || void 0 : process.env.SHELL || userInfo().shell || void 0;
1168
+ return {
1169
+ platform,
1170
+ ...defaultShell === void 0 ? {} : { defaultShell }
1171
+ };
1172
+ }
1028
1173
  async spawnTerminal(spec) {
1029
1174
  const file = spec.argv[0];
1030
1175
  if (file === void 0 || file.length === 0) throw new Error("subprocess-local: terminal argv must contain a program");
1031
1176
  spec.signal?.throwIfAborted();
1032
1177
  const env = targetEnvironment(spec);
1033
1178
  const options = {
1034
- name: "dumb",
1179
+ name: spec.terminalType,
1035
1180
  rows: spec.rows,
1036
1181
  cols: spec.cols,
1037
1182
  cwd: spec.cwd,
1038
- env
1183
+ env: {
1184
+ ...env,
1185
+ TERM: spec.terminalType
1186
+ }
1039
1187
  };
1040
1188
  const inspector = this.terminalInspector ?? createProcessInspector();
1041
1189
  const scope = this.selectContainmentMode("terminal") === "linux-scope" ? prepareLinuxTerminalScope(spec, {
1042
1190
  ...env,
1043
1191
  PWD: spec.cwd,
1044
- TERM: "dumb"
1192
+ TERM: spec.terminalType
1045
1193
  }) : void 0;
1046
1194
  if (scope !== void 0) {
1047
1195
  options.cwd = scope.cwd;
@@ -1055,17 +1203,16 @@ var LocalSubprocessRuntime = class extends SubprocessRuntime {
1055
1203
  throw error;
1056
1204
  }
1057
1205
  let handle;
1206
+ const directSettlement = Promise.withResolvers();
1058
1207
  const owner = scope?.bindOwner({
1059
1208
  running: () => handle?.running ?? true,
1060
- signal: (signal) => {
1061
- try {
1062
- terminal.kill(signal);
1063
- } catch {}
1064
- }
1209
+ settled: directSettlement.promise,
1210
+ signal: (signal) => signalLinuxDirectProcess(terminal.pid, () => process.kill(terminal.pid, signal))
1065
1211
  });
1066
1212
  handle = new LocalTerminalHandle(terminal, inspector, spec.graceMs, this.internals.platform ?? process.platform, owner, scope?.resolveOutcome);
1067
1213
  this.terminals.add(handle);
1068
1214
  const release = async () => {
1215
+ directSettlement.resolve();
1069
1216
  await handle.terminate();
1070
1217
  this.terminals.delete(handle);
1071
1218
  };