@nowcrew/daemon 0.5.18 → 0.5.20

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.
Files changed (41) hide show
  1. package/README.md +23 -0
  2. package/dist/attachments.js +196 -0
  3. package/dist/computer-cli.js +72 -12
  4. package/dist/computer-profile-lock.js +395 -0
  5. package/dist/computer-profile.js +189 -20
  6. package/dist/config.js +2 -1
  7. package/dist/console.js +175 -9
  8. package/dist/execution-event-limit.js +1 -1
  9. package/dist/execution-journal-lock.js +199 -40
  10. package/dist/execution-journal.js +42 -4
  11. package/dist/execution-protocol.js +21 -1
  12. package/dist/execution-recovery.js +71 -0
  13. package/dist/execution-runner.js +68 -77
  14. package/dist/execution-supervisor.js +79 -31
  15. package/dist/external-output.js +114 -0
  16. package/dist/i18n.js +5 -5
  17. package/dist/list-models.js +41 -5
  18. package/dist/local-executor.js +103 -14
  19. package/dist/machine-info.js +6 -1
  20. package/dist/main.js +23 -8
  21. package/dist/origin-decision.js +3 -1
  22. package/dist/prompt.js +4 -1
  23. package/dist/runner.js +14 -9
  24. package/dist/runtime-cancellation.js +74 -0
  25. package/dist/runtime-capabilities.js +38 -0
  26. package/dist/runtime-path.js +60 -0
  27. package/dist/runtimes/claude.js +9 -4
  28. package/dist/runtimes/codex-app-server-runner.js +340 -0
  29. package/dist/runtimes/codex.js +10 -4
  30. package/dist/runtimes/kimi-acp-runner.js +117 -17
  31. package/dist/runtimes/kimi.js +2 -0
  32. package/dist/runtimes/progress-watchdog.js +26 -0
  33. package/dist/serve-lifecycle.js +82 -0
  34. package/dist/serve.js +212 -212
  35. package/dist/session.js +1 -1
  36. package/dist/shared-execution-slots.js +68 -0
  37. package/dist/shutdown-deadline.js +32 -0
  38. package/dist/slog.js +34 -20
  39. package/dist/supervised-runtime.js +104 -0
  40. package/dist/websocket-shutdown.js +53 -0
  41. package/package.json +3 -3
@@ -558,7 +558,7 @@ export function createExecutionJournal(agentsRoot, options = {}) {
558
558
  return { kind: "created", ...entry };
559
559
  });
560
560
  },
561
- startGuarded: async (executionId, processStartedAt, startDormant) => {
561
+ startGuarded: async (executionId, processStartedAt, startDormant, hooks) => {
562
562
  validateExecutionId(executionId);
563
563
  TimestampSchema.parse(processStartedAt);
564
564
  return serialized(async () => {
@@ -567,9 +567,21 @@ export function createExecutionJournal(agentsRoot, options = {}) {
567
567
  return { kind: "existing", entry: await confirmDurable(entry) };
568
568
  }
569
569
  const dormant = await startDormant();
570
+ let abortPromise = null;
571
+ const abort = () => {
572
+ if (abortPromise === null) {
573
+ try {
574
+ abortPromise = Promise.resolve(dormant.abort());
575
+ }
576
+ catch (error) {
577
+ abortPromise = Promise.reject(error);
578
+ }
579
+ }
580
+ return abortPromise;
581
+ };
570
582
  const abortWith = async (error) => {
571
583
  try {
572
- await dormant.abort();
584
+ await abort();
573
585
  }
574
586
  catch (abortError) {
575
587
  throw new AggregateError([error, abortError], "Dormant runtime abort failed");
@@ -591,6 +603,9 @@ export function createExecutionJournal(agentsRoot, options = {}) {
591
603
  });
592
604
  await writeRecord(updated);
593
605
  try {
606
+ const gate = hooks?.beforeRelease?.({ entry: updated, handle: dormant, abort });
607
+ if (gate !== undefined)
608
+ await gate;
594
609
  await dormant.release();
595
610
  }
596
611
  catch (error) {
@@ -671,8 +686,31 @@ export function createExecutionJournal(agentsRoot, options = {}) {
671
686
  }
672
687
  }),
673
688
  prune: async () => serialized(pruneInternal),
674
- close: async () => {
675
- await runSerialized(directory, lease.close);
689
+ close: async (closeOptions = {}) => {
690
+ const signal = closeOptions.signal;
691
+ let started = false;
692
+ const closing = runSerialized(directory, async () => {
693
+ started = true;
694
+ if (signal?.aborted)
695
+ throw signal.reason ?? new Error("Journal close aborted");
696
+ await lease.close(closeOptions);
697
+ });
698
+ if (signal === undefined)
699
+ return closing;
700
+ await new Promise((resolveClose, rejectClose) => {
701
+ const settle = (settler) => {
702
+ signal.removeEventListener("abort", onAbort);
703
+ settler();
704
+ };
705
+ const onAbort = () => {
706
+ if (!started)
707
+ settle(() => rejectClose(signal.reason ?? new Error("Journal close aborted")));
708
+ };
709
+ signal.addEventListener("abort", onAbort, { once: true });
710
+ if (signal.aborted)
711
+ onAbort();
712
+ closing.then(() => settle(resolveClose), (error) => settle(() => rejectClose(error)));
713
+ });
676
714
  },
677
715
  };
678
716
  }
@@ -48,7 +48,7 @@ export const LegacyAgentStartSchema = z.object({
48
48
  content: z.string().optional(),
49
49
  senderHandle: z.string().optional(),
50
50
  threadId: z.string().optional(),
51
- origin: z.enum(["wecom"]).optional(),
51
+ origin: z.enum(["wecom", "feishu"]).optional(),
52
52
  }).passthrough().optional(),
53
53
  scheduledRun: z.object({
54
54
  jobId: z.string().min(1),
@@ -85,6 +85,12 @@ export const ConsoleStreamSchema = z.enum([
85
85
  "result",
86
86
  "error",
87
87
  ]);
88
+ export const ExecutionAttachmentSchema = z.object({
89
+ id: z.string().min(1).max(200),
90
+ filename: z.string().min(1).max(255),
91
+ mime: z.string().min(1).max(200),
92
+ sizeBytes: z.number().int().nonnegative().max(25 * 1024 * 1024),
93
+ }).strict();
88
94
  export const ExecutionStartSchema = z.object({
89
95
  type: z.literal("execution:start"),
90
96
  protocolVersion: ProtocolVersionSchema,
@@ -115,6 +121,9 @@ export const ExecutionStartSchema = z.object({
115
121
  channelId: z.string().min(1),
116
122
  threadId: z.string().min(1).optional(),
117
123
  wakeMessageId: z.string().min(1).optional(),
124
+ externalResponseSessionId: ExecutionIdSchema.optional(),
125
+ answerStream: z.boolean().optional(),
126
+ attachments: z.array(ExecutionAttachmentSchema).max(20).optional(),
118
127
  }).strict(),
119
128
  reporting: z.object({
120
129
  captureFinal: z.boolean(),
@@ -185,6 +194,15 @@ export const ExecutionConsoleSchema = z.object({
185
194
  seq: SequenceSchema,
186
195
  at: TimestampSchema,
187
196
  }).strict();
197
+ export const ExecutionOutputSchema = z.object({
198
+ type: z.literal("execution:output"),
199
+ protocolVersion: ProtocolVersionSchema,
200
+ executionId: ExecutionIdSchema,
201
+ channel: z.literal("external_answer"),
202
+ text: z.string().min(1),
203
+ seq: SequenceSchema,
204
+ at: TimestampSchema,
205
+ }).strict();
188
206
  export const ExecutionUsageSchema = z.object({
189
207
  inputTokens: TokenCountSchema,
190
208
  outputTokens: TokenCountSchema,
@@ -205,6 +223,7 @@ const RawExecutionCompletedSchema = z.object({
205
223
  model: z.string().optional(),
206
224
  resumed: z.boolean(),
207
225
  finalText: z.string().optional(),
226
+ externalAnswer: z.string().min(1).optional(),
208
227
  boundImDecision: z.enum(["notify", "silent"]).optional(),
209
228
  usage: ExecutionUsageSchema.optional(),
210
229
  startedAt: TimestampSchema,
@@ -312,6 +331,7 @@ const RawDaemonToServerExecutionFrameSchema = z.discriminatedUnion("type", [
312
331
  ExecutionStartedSchema,
313
332
  ExecutionActivitySchema,
314
333
  ExecutionConsoleSchema,
334
+ ExecutionOutputSchema,
315
335
  RawExecutionCompletedSchema,
316
336
  ExecutionSnapshotSchema,
317
337
  ]);
@@ -0,0 +1,71 @@
1
+ import { join, resolve } from "node:path";
2
+ export async function reconcileExecutionJournal(journal, dependencies) {
3
+ try {
4
+ await journal.reconcileAfterRestart();
5
+ }
6
+ catch (error) {
7
+ const agentsRoot = resolve(dependencies.agentsRoot);
8
+ const errorRecord = typeof error === "object" && error !== null
9
+ ? error
10
+ : {};
11
+ const journalPath = typeof errorRecord.journalPath === "string"
12
+ ? resolve(errorRecord.journalPath)
13
+ : join(agentsRoot, ".crew", "executions");
14
+ const ownerPid = typeof errorRecord.ownerPid === "number" ? errorRecord.ownerPid : undefined;
15
+ const errorType = error instanceof Error ? error.name : typeof error;
16
+ const errorMessage = error instanceof Error ? error.message : String(error);
17
+ const diagnostics = {
18
+ server_url: dependencies.serverUrl,
19
+ agents_root: agentsRoot,
20
+ journal_path: journalPath,
21
+ owner_pid: ownerPid,
22
+ error_type: errorType,
23
+ error_message: errorMessage,
24
+ };
25
+ dependencies.log("execution.recovery_failed", "execution journal 恢复失败", {
26
+ level: "ERROR",
27
+ ...diagnostics,
28
+ });
29
+ dependencies.writeStderr(`${JSON.stringify({
30
+ level: "ERROR",
31
+ event_type: "execution.recovery_failed",
32
+ message: "execution journal 恢复失败",
33
+ ...diagnostics,
34
+ })}\n`);
35
+ const reportCleanupFailure = (stage, cleanupError) => {
36
+ const cleanupErrorType = cleanupError instanceof Error ? cleanupError.name : typeof cleanupError;
37
+ const cleanupErrorMessage = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
38
+ const cleanupDiagnostics = {
39
+ server_url: dependencies.serverUrl,
40
+ agents_root: agentsRoot,
41
+ journal_path: journalPath,
42
+ owner_pid: ownerPid,
43
+ cleanup_stage: stage,
44
+ error_type: cleanupErrorType,
45
+ error_message: cleanupErrorMessage,
46
+ recovery_error_type: errorType,
47
+ recovery_error_message: errorMessage,
48
+ };
49
+ dependencies.log("execution.recovery_cleanup_failed", "execution journal 恢复失败后的清理失败", { level: "ERROR", ...cleanupDiagnostics });
50
+ dependencies.writeStderr(`${JSON.stringify({
51
+ level: "ERROR",
52
+ event_type: "execution.recovery_cleanup_failed",
53
+ message: "execution journal 恢复失败后的清理失败",
54
+ ...cleanupDiagnostics,
55
+ })}\n`);
56
+ };
57
+ try {
58
+ await dependencies.flush();
59
+ }
60
+ catch (cleanupError) {
61
+ reportCleanupFailure("slog_flush", cleanupError);
62
+ }
63
+ try {
64
+ await journal.close();
65
+ }
66
+ catch (cleanupError) {
67
+ reportCleanupFailure("journal_close", cleanupError);
68
+ }
69
+ throw error;
70
+ }
71
+ }
@@ -1,17 +1,19 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { join } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
3
  import { DaemonToServerExecutionFrameSchema, ExecutionCompletedSchema, ExecutionRejectedSchema, ExecutionStartSchema, } from "./execution-protocol.js";
5
4
  import { JournalConflictError } from "./execution-journal.js";
6
5
  import { boundExecutionFrame } from "./execution-event-limit.js";
7
6
  import { mintAgentToken } from "./token.js";
8
7
  import { executeLocal, withLocalExecutionFacts, } from "./local-executor.js";
9
8
  import { startDormantSupervisor, } from "./execution-supervisor.js";
10
- import { buildClaudeArgs, CLAUDE_EFFORT_LEVELS } from "./runtimes/claude.js";
11
- import { buildCodexArgs, CODEX_EFFORT_LEVELS } from "./runtimes/codex.js";
9
+ import { CLAUDE_EFFORT_LEVELS } from "./runtimes/claude.js";
10
+ import { CODEX_EFFORT_LEVELS } from "./runtimes/codex.js";
12
11
  import { KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
13
12
  import { executionBackendCapability } from "./execution-backend.js";
14
13
  import { readBoundImDecisionFile, resetBoundImDecisionFile } from "./bound-im-decision.js";
14
+ import { RuntimeCancelledError } from "./runtime-cancellation.js";
15
+ import { supervisorLaunch } from "./supervised-runtime.js";
16
+ export { supervisorLaunch } from "./supervised-runtime.js";
15
17
  const ACTIVITY_KIND = {
16
18
  init: "working",
17
19
  text: "thinking",
@@ -183,56 +185,6 @@ function launchProviderConfig(config) {
183
185
  ...(config.description === undefined ? {} : { description: config.description }),
184
186
  };
185
187
  }
186
- function supervisorLaunch(request) {
187
- const common = {
188
- wakePrompt: request.wakePrompt,
189
- dangerous: request.effectivePermission === "full_access",
190
- effectivePermission: request.effectivePermission,
191
- ...(request.model === undefined ? {} : { model: request.model }),
192
- ...(request.reasoning === undefined ? {} : { reasoning: request.reasoning }),
193
- };
194
- if (request.runtime === "claude") {
195
- return {
196
- command: request.bin,
197
- args: buildClaudeArgs({
198
- ...common,
199
- bin: request.bin,
200
- cwd: request.cwd,
201
- env: request.env,
202
- systemPromptPath: request.systemPromptPath,
203
- ...(request.sessionId === undefined ? {} : {
204
- sessionId: request.sessionId,
205
- resume: request.resume,
206
- }),
207
- }),
208
- cwd: request.cwd,
209
- env: request.env,
210
- };
211
- }
212
- if (request.runtime === "codex") {
213
- return {
214
- command: request.bin,
215
- args: buildCodexArgs(common),
216
- cwd: request.cwd,
217
- env: request.env,
218
- stdinText: `${request.systemPrompt}\n\n${request.wakePrompt}`,
219
- };
220
- }
221
- if (request.effectivePermission !== "full_access") {
222
- throw new Error(`Kimi ACP cannot enforce ${request.effectivePermission} permission`);
223
- }
224
- return {
225
- command: process.execPath,
226
- args: [
227
- fileURLToPath(new URL("./runtimes/kimi-acp-runner.js", import.meta.url)),
228
- "--bin", request.bin,
229
- ...(request.model === undefined ? [] : ["--model", request.model]),
230
- ],
231
- cwd: request.cwd,
232
- env: request.env,
233
- stdinText: `${request.systemPrompt}\n\n${request.wakePrompt}`,
234
- };
235
- }
236
188
  function rejection(executionId, reason, message, at) {
237
189
  return ExecutionRejectedSchema.parse({
238
190
  type: "execution:rejected",
@@ -442,6 +394,7 @@ export async function runExecution(config, input, dependencies) {
442
394
  const providerConfig = launchProviderConfig(credential.config);
443
395
  let activitySequence = 0;
444
396
  let consoleSequence = 0;
397
+ let externalOutputSequence = 0;
445
398
  const callbacks = {
446
399
  ...(spec.reporting.streamActivity ? {
447
400
  onActivity: (activity) => {
@@ -479,8 +432,26 @@ export async function runExecution(config, input, dependencies) {
479
432
  catch { /* best-effort console omitted when its envelope cannot fit */ }
480
433
  },
481
434
  } : {}),
435
+ ...(spec.context.externalResponseSessionId || spec.context.answerStream ? {
436
+ onExternalOutput: (text) => {
437
+ const frame = DaemonToServerExecutionFrameSchema.parse({
438
+ type: "execution:output",
439
+ protocolVersion: 1,
440
+ executionId: spec.executionId,
441
+ channel: "external_answer",
442
+ text,
443
+ seq: externalOutputSequence++,
444
+ at: now().toISOString(),
445
+ });
446
+ try {
447
+ telemetry.enqueue(boundExecutionFrame(frame, config.executionLimits.maxEventBytes));
448
+ }
449
+ catch { /* final completion remains the authoritative repair */ }
450
+ },
451
+ } : {}),
482
452
  };
483
453
  const localDependencies = {
454
+ ...(dependencies.cancellation === undefined ? {} : { cancellation: dependencies.cancellation }),
484
455
  launchRuntime: async (request) => {
485
456
  if (launchClosed || dependencies.cancellation?.isRequested())
486
457
  throw new ExecutionCancelledError();
@@ -489,27 +460,42 @@ export async function runExecution(config, input, dependencies) {
489
460
  launchAttempts.add(launchSettled);
490
461
  try {
491
462
  const processStartedAt = now().toISOString();
492
- const guarded = await dependencies.journal.startGuarded(spec.executionId, processStartedAt, () => startSupervisor(supervisorLaunch(request)));
463
+ const launchControl = { cancel: null };
464
+ const guarded = await dependencies.journal.startGuarded(spec.executionId, processStartedAt, () => startSupervisor(supervisorLaunch(request)), {
465
+ beforeRelease: ({ entry, handle, abort }) => {
466
+ supervisorState.active = handle;
467
+ let stopPromise = null;
468
+ let releaseStarted = false;
469
+ const stopOnce = (operation) => {
470
+ if (stopPromise === null) {
471
+ try {
472
+ stopPromise = Promise.resolve(operation());
473
+ }
474
+ catch (error) {
475
+ stopPromise = Promise.reject(error);
476
+ }
477
+ }
478
+ return stopPromise;
479
+ };
480
+ launchControl.cancel = () => stopOnce(releaseStarted ? handle.cancel : abort);
481
+ supervisorState.abortOnce = () => stopOnce(abort);
482
+ dependencies.cancellation?.register(launchControl.cancel);
483
+ startedAt = entry.processStartedAt ?? processStartedAt;
484
+ if (dependencies.cancellation?.isRequested()) {
485
+ return dependencies.cancellation.waitForStop().then(() => {
486
+ throw new ExecutionCancelledError();
487
+ });
488
+ }
489
+ releaseStarted = true;
490
+ },
491
+ });
493
492
  if (guarded.kind !== "started") {
494
493
  throw new Error(`Execution became ${guarded.entry.state} before local launch`);
495
494
  }
496
- supervisorState.active = guarded.handle;
497
- let stopPromise = null;
498
- const stopOnce = (operation) => {
499
- if (stopPromise === null) {
500
- try {
501
- stopPromise = Promise.resolve(operation());
502
- }
503
- catch (error) {
504
- stopPromise = Promise.reject(error);
505
- }
506
- }
507
- return stopPromise;
508
- };
509
- const cancelOnce = () => stopOnce(guarded.handle.cancel);
510
- supervisorState.abortOnce = () => stopOnce(guarded.handle.abort);
511
- dependencies.cancellation?.register(cancelOnce);
512
- startedAt = guarded.entry.processStartedAt ?? processStartedAt;
495
+ if (launchControl.cancel === null) {
496
+ throw new Error("Execution launch cancellation gate was not installed");
497
+ }
498
+ const installedCancel = launchControl.cancel;
513
499
  await reportBestEffort(dependencies.report, boundExecutionFrame(DaemonToServerExecutionFrameSchema.parse({
514
500
  type: "execution:started",
515
501
  protocolVersion: 1,
@@ -520,14 +506,14 @@ export async function runExecution(config, input, dependencies) {
520
506
  timeout = setTimeout(() => {
521
507
  timedOut = true;
522
508
  try {
523
- void cancelOnce().catch(rejectCancellationFailure);
509
+ void installedCancel().catch(rejectCancellationFailure);
524
510
  }
525
511
  catch (error) {
526
512
  rejectCancellationFailure(error);
527
513
  }
528
514
  }, effectiveTimeoutMs);
529
515
  }
530
- return guarded.handle;
516
+ return { ...guarded.handle, cancel: installedCancel };
531
517
  }
532
518
  finally {
533
519
  launchAttempts.delete(launchSettled);
@@ -543,6 +529,7 @@ export async function runExecution(config, input, dependencies) {
543
529
  taskKey: spec.workspace.taskKey,
544
530
  ...(spec.workspace.resumeKey === undefined ? {} : { resumeKey: spec.workspace.resumeKey }),
545
531
  ...(spec.context.wakeMessageId === undefined ? {} : { wakeMessageId: spec.context.wakeMessageId }),
532
+ ...(spec.context.attachments === undefined ? {} : { attachments: spec.context.attachments }),
546
533
  systemPrompt: withLocalExecutionFacts(spec.instructions.systemPrompt, config.executionLimits.maxPromptBytes
547
534
  - Buffer.byteLength(spec.instructions.wakePrompt, "utf8")),
548
535
  wakePrompt: spec.instructions.wakePrompt,
@@ -575,10 +562,10 @@ export async function runExecution(config, input, dependencies) {
575
562
  maxTurns: config.sessionMaxTurns,
576
563
  },
577
564
  };
578
- const result = await cancellable(Promise.race([
565
+ const result = await Promise.race([
579
566
  execute(localInput, callbacks, localDependencies),
580
567
  cancellationFailure,
581
- ]), dependencies.cancellation);
568
+ ]);
582
569
  if (timeout !== undefined)
583
570
  clearTimeout(timeout);
584
571
  const finishedAt = now().toISOString();
@@ -619,6 +606,9 @@ export async function runExecution(config, input, dependencies) {
619
606
  ...(!spec.reporting.captureFinal || result.finalText === null
620
607
  ? {}
621
608
  : { finalText: result.finalText }),
609
+ ...(spec.context.answerStream && result.exitCode === 0 && result.externalAnswer
610
+ ? { externalAnswer: result.externalAnswer }
611
+ : {}),
622
612
  ...(result.usage === undefined ? {} : { usage: result.usage }),
623
613
  startedAt,
624
614
  finishedAt,
@@ -627,7 +617,8 @@ export async function runExecution(config, input, dependencies) {
627
617
  catch (error) {
628
618
  if (timeout !== undefined)
629
619
  clearTimeout(timeout);
630
- if (error instanceof ExecutionCancelledError) {
620
+ const cancelled = error instanceof ExecutionCancelledError || error instanceof RuntimeCancelledError;
621
+ if (cancelled) {
631
622
  await closeLaunchGate();
632
623
  await dependencies.cancellation?.waitForStop();
633
624
  }
@@ -640,7 +631,7 @@ export async function runExecution(config, input, dependencies) {
640
631
  throw new AggregateError([error, abortError], `Failed to stop the execution supervisor: ${detail}`);
641
632
  }
642
633
  }
643
- completion = error instanceof ExecutionCancelledError
634
+ completion = cancelled
644
635
  ? ExecutionCompletedSchema.parse({
645
636
  type: "execution:completed",
646
637
  protocolVersion: 1,
@@ -44,6 +44,55 @@ async function waitForProcessGroupExit(pid, timeoutMs) {
44
44
  await new Promise((resolve) => setTimeout(resolve, PROCESS_GROUP_POLL_MS));
45
45
  }
46
46
  }
47
+ async function processGroupExists(pid) {
48
+ try {
49
+ process.kill(-pid, 0);
50
+ return true;
51
+ }
52
+ catch (error) {
53
+ const code = error.code;
54
+ if (code === "ESRCH")
55
+ return false;
56
+ if (code === "EPERM")
57
+ return true;
58
+ throw error;
59
+ }
60
+ }
61
+ async function signalOwnedTreeIfPresent(pid, signal, platform, signalTree) {
62
+ try {
63
+ await signalTree(pid, signal, platform);
64
+ }
65
+ catch (error) {
66
+ if (error.code !== "ESRCH")
67
+ throw error;
68
+ }
69
+ }
70
+ async function terminateAndConfirmOwnedTree(pid, platform, timeoutMs, supervisorClosed, signalTree) {
71
+ const waitUntilStopped = () => platform === "win32"
72
+ ? waitForExit(supervisorClosed.then(() => ({ exitCode: 0 })), timeoutMs, pid)
73
+ : waitForProcessGroupExit(pid, timeoutMs);
74
+ let termError;
75
+ try {
76
+ await signalOwnedTreeIfPresent(pid, "SIGTERM", platform, signalTree);
77
+ await waitUntilStopped();
78
+ return;
79
+ }
80
+ catch (error) {
81
+ termError = error;
82
+ }
83
+ try {
84
+ await signalOwnedTreeIfPresent(pid, "SIGKILL", platform, signalTree);
85
+ await waitUntilStopped();
86
+ }
87
+ catch (killError) {
88
+ throw new AggregateError([termError, killError], "Supervisor process-tree termination failed");
89
+ }
90
+ }
91
+ async function confirmOrTerminateOwnedTree(pid, platform, timeoutMs, supervisorClosed, signalTree) {
92
+ if (platform !== "win32" && !(await processGroupExists(pid)))
93
+ return;
94
+ await terminateAndConfirmOwnedTree(pid, platform, timeoutMs, supervisorClosed, signalTree);
95
+ }
47
96
  async function withTimeout(promise, timeoutMs, phase) {
48
97
  let timer;
49
98
  try {
@@ -86,9 +135,13 @@ export async function signalSupervisorTree(pid, signal, platform = process.platf
86
135
  }
87
136
  export async function startDormantSupervisor(launch, options = {}) {
88
137
  const platform = options.platform ?? process.platform;
89
- const backend = executionBackendCapability(platform);
90
- if (!backend.supported)
91
- throw new Error(backend.reason);
138
+ const ownershipMode = options.ownershipMode ?? "durable";
139
+ if (ownershipMode === "durable") {
140
+ const backend = executionBackendCapability(platform);
141
+ if (!backend.supported)
142
+ throw new Error(backend.reason);
143
+ }
144
+ const signalTree = options.signalTree ?? signalSupervisorTree;
92
145
  const childEntry = options.childEntry
93
146
  ?? fileURLToPath(new URL("./execution-supervisor-child.js", import.meta.url));
94
147
  const abortTimeoutMs = options.abortTimeoutMs ?? DEFAULT_ABORT_TIMEOUT_MS;
@@ -118,12 +171,16 @@ export async function startDormantSupervisor(launch, options = {}) {
118
171
  ...(supervisorSpawnError === undefined && signal !== null ? { terminationSignal: signal } : {}),
119
172
  }));
120
173
  });
174
+ const supervisorClosed = new Promise((resolve) => child.once("close", () => resolve()));
175
+ let treeStopPromise = null;
176
+ const ensureTreeStopped = () => {
177
+ treeStopPromise ??= confirmOrTerminateOwnedTree(pid, platform, abortTimeoutMs, supervisorClosed, signalTree);
178
+ return treeStopPromise;
179
+ };
121
180
  const exit = supervisorExit.then(async (result) => {
122
- if (platform !== "win32")
123
- await waitForProcessGroupExit(pid, abortTimeoutMs);
181
+ await ensureTreeStopped();
124
182
  return result;
125
183
  });
126
- const supervisorClosed = new Promise((resolve) => child.once("close", () => resolve()));
127
184
  let readyResolve;
128
185
  let readyReject;
129
186
  const ready = new Promise((resolve, reject) => {
@@ -161,41 +218,30 @@ export async function startDormantSupervisor(launch, options = {}) {
161
218
  const abort = async () => {
162
219
  if (child.exitCode !== null || child.signalCode !== null) {
163
220
  await supervisorClosed;
221
+ await ensureTreeStopped();
164
222
  return;
165
223
  }
166
- try {
167
- await signalSupervisorTree(pid, "SIGTERM", platform);
168
- }
169
- catch (error) {
170
- const code = error instanceof Error && "code" in error ? error.code : undefined;
171
- if (code !== "ESRCH")
172
- throw error;
173
- }
174
- try {
175
- await waitForExit(supervisorClosed.then(() => ({ exitCode: 0 })), abortTimeoutMs, pid);
176
- }
177
- catch (error) {
178
- try {
179
- await signalSupervisorTree(pid, "SIGKILL", platform);
180
- }
181
- catch (killError) {
182
- const code = killError instanceof Error && "code" in killError ? killError.code : undefined;
183
- if (code !== "ESRCH")
184
- throw new AggregateError([error, killError], "Supervisor abort failed");
185
- }
186
- await waitForExit(supervisorClosed.then(() => ({ exitCode: 0 })), abortTimeoutMs, pid);
187
- }
224
+ await ensureTreeStopped();
225
+ await waitForExit(supervisorClosed.then(() => ({ exitCode: 0 })), abortTimeoutMs, pid);
226
+ await ensureTreeStopped();
188
227
  };
189
228
  const cancel = async () => {
190
229
  if (child.exitCode !== null || child.signalCode !== null) {
191
230
  await supervisorClosed;
231
+ await ensureTreeStopped();
192
232
  return;
193
233
  }
194
234
  try {
195
235
  await new Promise((resolve, reject) => {
196
236
  child.send({ type: "abort" }, (error) => error === null ? resolve() : reject(error));
197
237
  });
198
- await waitForExit(supervisorClosed.then(() => ({ exitCode: 0 })), abortTimeoutMs, pid);
238
+ if (platform === "win32") {
239
+ await ensureTreeStopped();
240
+ }
241
+ else {
242
+ await waitForExit(supervisorClosed.then(() => ({ exitCode: 0 })), abortTimeoutMs, pid);
243
+ await ensureTreeStopped();
244
+ }
199
245
  }
200
246
  catch {
201
247
  await abort();
@@ -218,9 +264,8 @@ export async function startDormantSupervisor(launch, options = {}) {
218
264
  });
219
265
  throw error;
220
266
  }
221
- return {
267
+ const handle = {
222
268
  pid,
223
- parentExitGuard: "pipe-eof",
224
269
  stdout: child.stdout,
225
270
  stderr: child.stderr,
226
271
  exit,
@@ -249,4 +294,7 @@ export async function startDormantSupervisor(launch, options = {}) {
249
294
  abort,
250
295
  cancel,
251
296
  };
297
+ return ownershipMode === "durable"
298
+ ? { ...handle, ownershipMode, parentExitGuard: "pipe-eof" }
299
+ : { ...handle, ownershipMode };
252
300
  }