@fieldwangai/agentflow 0.1.98 → 0.1.100

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.
@@ -31,6 +31,17 @@ function childEnv(options = {}, extra = {}) {
31
31
  return { ...process.env, ...readMergedEnvObject(userId), ...optEnv, ...extra };
32
32
  }
33
33
 
34
+ function notifyPromptAgentChild(options, child) {
35
+ if (typeof options?.onChild !== "function") return;
36
+ try {
37
+ options.onChild(child || null, {
38
+ processGroup: Boolean(options.detached) && process.platform !== "win32",
39
+ });
40
+ } catch {
41
+ // Child tracking must not interfere with the runner.
42
+ }
43
+ }
44
+
34
45
  function cursorAttemptOptions(options = {}) {
35
46
  const baseEnv = childEnv(options);
36
47
  const attempts = Array.isArray(options._agentflowCursorApiKeyAttempts)
@@ -1330,7 +1341,9 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
1330
1341
  stdio: ["ignore", "pipe", useStderrInherit ? "inherit" : "pipe"],
1331
1342
  shell: false,
1332
1343
  env: childEnv(options, cursorApiKeyEnv(cursorSelection)),
1344
+ detached: Boolean(options.detached) && process.platform !== "win32",
1333
1345
  });
1346
+ notifyPromptAgentChild(options, child);
1334
1347
 
1335
1348
  let lastResult = null;
1336
1349
  let hadError = false;
@@ -1551,6 +1564,7 @@ export function runOpenCodeAgentWithPrompt(cliWorkspace, promptText, options = {
1551
1564
  stdio: ["ignore", "pipe", "pipe"],
1552
1565
  shell: false,
1553
1566
  env: childEnv(options),
1567
+ detached: Boolean(options.detached) && process.platform !== "win32",
1554
1568
  };
1555
1569
  if (options.force) {
1556
1570
  spawnOpts.env = {
@@ -1562,6 +1576,7 @@ export function runOpenCodeAgentWithPrompt(cliWorkspace, promptText, options = {
1562
1576
  }
1563
1577
 
1564
1578
  const child = spawn(opencodeCmd, args, spawnOpts);
1579
+ notifyPromptAgentChild(options, child);
1565
1580
 
1566
1581
  const emit = (payload) => {
1567
1582
  try {
@@ -1664,7 +1679,9 @@ export function runClaudeCodeAgentWithPrompt(cliWorkspace, promptText, options =
1664
1679
  stdio: ["ignore", "pipe", useStderrInherit ? "inherit" : "pipe"],
1665
1680
  shell: false,
1666
1681
  env: childEnv(options),
1682
+ detached: Boolean(options.detached) && process.platform !== "win32",
1667
1683
  });
1684
+ notifyPromptAgentChild(options, child);
1668
1685
 
1669
1686
  let lastResult = null;
1670
1687
  let hadError = false;
@@ -1845,7 +1862,9 @@ export function runCodexAgentWithPrompt(cliWorkspace, promptText, options = {})
1845
1862
  stdio: ["ignore", "pipe", useStderrInherit ? "inherit" : "pipe"],
1846
1863
  shell: false,
1847
1864
  env: childEnv(options),
1865
+ detached: Boolean(options.detached) && process.platform !== "win32",
1848
1866
  });
1867
+ notifyPromptAgentChild(options, child);
1849
1868
 
1850
1869
  let hadError = false;
1851
1870
  let emittedNatural = false;
@@ -417,6 +417,8 @@ export function startComposerAgent(opts) {
417
417
  const common = {
418
418
  onStreamEvent: opts.onStreamEvent,
419
419
  onToolCall: opts.onToolCall,
420
+ onChild: opts.onChild,
421
+ detached: Boolean(opts.detached),
420
422
  force: Boolean(opts.force),
421
423
  env,
422
424
  };
@@ -128,6 +128,7 @@ import {
128
128
  listWorkspaceRunLogs,
129
129
  readWorkspaceRunLogEvents,
130
130
  } from "./workspace-run-logs.mjs";
131
+ import { createWorkspaceRunController } from "./workspace-run-controller.mjs";
131
132
 
132
133
  const MIME = {
133
134
  ".html": "text/html; charset=utf-8",
@@ -4947,6 +4948,8 @@ async function workspaceGeneratePlannedImplementationMarkdown({
4947
4948
  prompt,
4948
4949
  modelKey,
4949
4950
  agentflowUserId: userCtx?.userId || "",
4951
+ detached: process.platform !== "win32",
4952
+ onChild: onActiveChild,
4950
4953
  extraEnv: runtimeEnvForUser(userCtx, {
4951
4954
  AGENTFLOW_IMPLEMENTATION_REF: implementationPath || "",
4952
4955
  AGENTFLOW_NODE_RUN_DIR: runPackage?.nodeRunDir || "",
@@ -4967,7 +4970,6 @@ async function workspaceGeneratePlannedImplementationMarkdown({
4967
4970
  emit?.({ type: "status", nodeId, line: `优化工具 ${tool || "thinking"}${sub ? ` (${sub})` : ""}` });
4968
4971
  },
4969
4972
  });
4970
- if (typeof onActiveChild === "function") onActiveChild(handle.child || null);
4971
4973
  try {
4972
4974
  await handle.finished;
4973
4975
  } finally {
@@ -5009,6 +5011,8 @@ async function workspaceGenerateImplementationMarkdown({
5009
5011
  prompt,
5010
5012
  modelKey,
5011
5013
  agentflowUserId: userCtx?.userId || "",
5014
+ detached: process.platform !== "win32",
5015
+ onChild: onActiveChild,
5012
5016
  extraEnv: runtimeEnvForUser(userCtx, {
5013
5017
  AGENTFLOW_IMPLEMENTATION_REF: implementationPath || "",
5014
5018
  AGENTFLOW_NODE_RUN_DIR: runPackage?.nodeRunDir || "",
@@ -5029,7 +5033,6 @@ async function workspaceGenerateImplementationMarkdown({
5029
5033
  emit?.({ type: "status", line: `总结方案工具 ${tool || "thinking"}${sub ? ` (${sub})` : ""}` });
5030
5034
  },
5031
5035
  });
5032
- if (typeof onActiveChild === "function") onActiveChild(handle.child || null);
5033
5036
  try {
5034
5037
  await handle.finished;
5035
5038
  } finally {
@@ -5994,6 +5997,8 @@ async function workspaceRunToolNodejsScript({
5994
5997
  userCtx,
5995
5998
  envOverlay = {},
5996
5999
  emit,
6000
+ signal,
6001
+ onActiveChild,
5997
6002
  }) {
5998
6003
  const scriptRef = String(instance?.scriptRef || "").trim();
5999
6004
  const scriptAbs = scriptRef ? workspaceResolveFlowFile(scopedRoot, scriptRef, "scriptRef") : "";
@@ -6041,14 +6046,24 @@ async function workspaceRunToolNodejsScript({
6041
6046
 
6042
6047
  const started = Date.now();
6043
6048
  return await new Promise((resolve, reject) => {
6049
+ const processGroup = process.platform !== "win32";
6044
6050
  const child = spawn(command, [], {
6045
6051
  cwd: runPackage.nodeRunDir,
6046
6052
  shell: true,
6047
6053
  stdio: ["ignore", "pipe", "pipe"],
6048
6054
  env,
6055
+ detached: processGroup,
6049
6056
  });
6057
+ if (typeof onActiveChild === "function") onActiveChild(child, { processGroup });
6050
6058
  let stdout = "";
6051
6059
  let stderr = "";
6060
+ let settled = false;
6061
+ const finish = (callback) => {
6062
+ if (settled) return;
6063
+ settled = true;
6064
+ if (typeof onActiveChild === "function") onActiveChild(null);
6065
+ callback();
6066
+ };
6052
6067
  child.stdout.setEncoding("utf-8");
6053
6068
  child.stderr.setEncoding("utf-8");
6054
6069
  child.stdout.on("data", (chunk) => {
@@ -6057,19 +6072,27 @@ async function workspaceRunToolNodejsScript({
6057
6072
  child.stderr.on("data", (chunk) => {
6058
6073
  stderr += String(chunk);
6059
6074
  });
6060
- child.on("error", reject);
6075
+ child.on("error", (error) => finish(() => reject(error)));
6061
6076
  child.on("close", (code) => {
6077
+ if (signal?.aborted) {
6078
+ finish(() => {
6079
+ const error = new Error("Workspace run stopped");
6080
+ error.code = "WORKSPACE_RUN_ABORTED";
6081
+ reject(error);
6082
+ });
6083
+ return;
6084
+ }
6062
6085
  if (stderr.trim()) {
6063
6086
  emit?.({ type: "natural", kind: "warning", text: `[script stderr]\n${stderr.trim().slice(-4000)}` });
6064
6087
  }
6065
6088
  if (code !== 0) {
6066
- reject(new Error(`tool_nodejs script exited ${code}${stderr.trim() ? `: ${stderr.trim().slice(-800)}` : ""}`));
6089
+ finish(() => reject(new Error(`tool_nodejs script exited ${code}${stderr.trim() ? `: ${stderr.trim().slice(-800)}` : ""}`)));
6067
6090
  return;
6068
6091
  }
6069
6092
  const elapsedMs = Math.max(0, Date.now() - started);
6070
6093
  emit?.({ type: "status", line: `Timing script: ${elapsedMs}ms`, timing: { label: "script", elapsedMs } });
6071
6094
  const content = stdout.trim() || workspaceEnvelopeFromOutputFiles(outputRefs, runPackage.nodeRunDir);
6072
- resolve(content);
6095
+ finish(() => resolve(content));
6073
6096
  });
6074
6097
  });
6075
6098
  }
@@ -6691,6 +6714,8 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
6691
6714
  userCtx,
6692
6715
  envOverlay: runEnv,
6693
6716
  emit: (event) => emit({ ...event, nodeId }),
6717
+ signal,
6718
+ onActiveChild: opts.onActiveChild,
6694
6719
  });
6695
6720
  const normalizedAgentOutput = workspacePublishAgentOutputFiles(workspaceStructuredAgentOutput(content), runPackage);
6696
6721
  const resultContent = normalizedAgentOutput.result || content;
@@ -6787,6 +6812,8 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
6787
6812
  prompt,
6788
6813
  modelKey: nodeModelKey,
6789
6814
  agentflowUserId: userCtx.userId || "",
6815
+ detached: process.platform !== "win32",
6816
+ onChild: opts.onActiveChild,
6790
6817
  extraEnv: runtimeEnv({
6791
6818
  AGENTFLOW_WORKSPACE_TMP_ROOT: runTmpRoot,
6792
6819
  AGENTFLOW_NODE_RUN_DIR: runPackage.nodeRunDir,
@@ -6823,7 +6850,6 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
6823
6850
  emit({ type: "status", nodeId, line: `工具 ${tool || "thinking"}${sub ? ` (${sub})` : ""}` });
6824
6851
  },
6825
6852
  });
6826
- if (typeof opts.onActiveChild === "function") opts.onActiveChild(handle.child || null);
6827
6853
  emitTiming(nodeId, "spawn-agent", spawnStartedAt, { attempt });
6828
6854
  try {
6829
6855
  await handle.finished;
@@ -9026,6 +9052,14 @@ function workspaceRunEntryKey(scopeKey, runId) {
9026
9052
  return `${scopeKey}:${String(runId || "").trim() || runLedgerId("workspace")}`;
9027
9053
  }
9028
9054
 
9055
+ function workspaceRunControl(abortController) {
9056
+ return createWorkspaceRunController({
9057
+ abortController,
9058
+ gracefulTimeoutMs: 3_000,
9059
+ forceTimeoutMs: 1_500,
9060
+ });
9061
+ }
9062
+
9029
9063
  function workspaceRuntimeNodeLabel(graph, nodeId, fallback = "Workspace Run") {
9030
9064
  const id = String(nodeId || "").trim();
9031
9065
  const instance = graph?.instances && typeof graph.instances === "object" ? graph.instances[id] : null;
@@ -9463,11 +9497,12 @@ async function runWorkspaceScheduledEntry(root, entry) {
9463
9497
  }
9464
9498
 
9465
9499
  const controller = new AbortController();
9500
+ const runControl = workspaceRunControl(controller);
9466
9501
  const runKey = workspaceRunEntryKey(scopeKey, runId);
9467
9502
  const runEntry = {
9468
9503
  scopeKey,
9469
9504
  controller,
9470
- child: null,
9505
+ runControl,
9471
9506
  runId,
9472
9507
  userId: userCtx.userId,
9473
9508
  username: String(authUser.username || entry.username || userCtx.userId),
@@ -9478,11 +9513,6 @@ async function runWorkspaceScheduledEntry(root, entry) {
9478
9513
  plannedNodeIds,
9479
9514
  startedAt: Date.now(),
9480
9515
  scheduled: true,
9481
- stopChild() {
9482
- if (this.child && !this.child.killed) {
9483
- try { this.child.kill("SIGTERM"); } catch (_) {}
9484
- }
9485
- },
9486
9516
  };
9487
9517
  activeWorkspaceRuns.set(runKey, runEntry);
9488
9518
  appendWorkspaceRunStarted(runEntry);
@@ -9496,9 +9526,8 @@ async function runWorkspaceScheduledEntry(root, entry) {
9496
9526
  timezone: config.timezone,
9497
9527
  lastError: "",
9498
9528
  });
9499
- const setActiveChild = (child) => {
9500
- runEntry.child = child || null;
9501
- if (controller.signal.aborted) runEntry.stopChild();
9529
+ const setActiveChild = (child, childOptions = {}) => {
9530
+ runControl.setChild(child, childOptions);
9502
9531
  };
9503
9532
  try {
9504
9533
  const result = await runWorkspaceGraph(root, scoped.root, {
@@ -9531,23 +9560,28 @@ async function runWorkspaceScheduledEntry(root, entry) {
9531
9560
  } catch (e) {
9532
9561
  const endedAt = Date.now();
9533
9562
  const error = (e && e.message) || String(e);
9534
- appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "failed");
9535
- appendWorkspaceRunLogEvent(runLog.runId, { type: "error", error, ts: endedAt });
9536
- finishWorkspaceRunLogSession(runLog.runId, "failed", {
9563
+ const stopped = isWorkspaceRunAbortError(e) || controller.signal.aborted;
9564
+ const finalStatus = stopped ? "stopped" : "failed";
9565
+ appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, finalStatus);
9566
+ appendWorkspaceRunLogEvent(runLog.runId, stopped
9567
+ ? { type: "stopped", message: "Workspace run stopped", ts: endedAt }
9568
+ : { type: "error", error, ts: endedAt });
9569
+ finishWorkspaceRunLogSession(runLog.runId, finalStatus, {
9537
9570
  endedAt,
9538
9571
  durationMs: endedAt - runEntry.startedAt,
9539
9572
  runNodeId: targetRunNodeId,
9540
- error,
9573
+ error: stopped ? "" : error,
9541
9574
  });
9542
9575
  updateWorkspaceScheduleEntry(entry.key, {
9543
9576
  nextRunAt: computeNext(config),
9544
9577
  lastFinishedAt: endedAt,
9545
- lastStatus: "failed",
9546
- lastError: error,
9547
- lastErrorAt: endedAt,
9578
+ lastStatus: finalStatus,
9579
+ lastError: stopped ? "" : error,
9580
+ ...(stopped ? {} : { lastErrorAt: endedAt }),
9548
9581
  });
9549
- log.info(`[workspace-scheduler] failed ${entry.flowId}/${targetRunNodeId}: ${error}`);
9582
+ if (!stopped) log.info(`[workspace-scheduler] failed ${entry.flowId}/${targetRunNodeId}: ${error}`);
9550
9583
  } finally {
9584
+ runControl.finish(controller.signal.aborted ? "stopped" : "finished");
9551
9585
  if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
9552
9586
  }
9553
9587
  }
@@ -11516,13 +11550,14 @@ export function startUiServer({
11516
11550
  return;
11517
11551
  }
11518
11552
  const controller = new AbortController();
11553
+ const runControl = workspaceRunControl(controller);
11519
11554
  const runId = String(payload.runSessionId || payload.runId || "").trim() || runLedgerId("workspace");
11520
11555
  const runKey = workspaceRunEntryKey(scopeKey, runId);
11521
11556
  const runAlias = String(payload.runAlias || "").trim() || workspaceRuntimeNodeLabel(runtimeGraph, runNodeId, "Workspace Run");
11522
11557
  const runEntry = {
11523
11558
  scopeKey,
11524
11559
  controller,
11525
- child: null,
11560
+ runControl,
11526
11561
  runId,
11527
11562
  userId: String(userCtx.userId || ""),
11528
11563
  username: String(authUser?.username || userCtx.userId || ""),
@@ -11532,11 +11567,6 @@ export function startUiServer({
11532
11567
  flowSource: scoped.flowSource || payload.flowSource || "user",
11533
11568
  plannedNodeIds,
11534
11569
  startedAt: Date.now(),
11535
- stopChild() {
11536
- if (this.child && !this.child.killed) {
11537
- try { this.child.kill("SIGTERM"); } catch (_) {}
11538
- }
11539
- },
11540
11570
  };
11541
11571
  const runLog = createWorkspaceRunLogSession({
11542
11572
  runId,
@@ -11553,11 +11583,11 @@ export function startUiServer({
11553
11583
  });
11554
11584
  activeWorkspaceRuns.set(runKey, runEntry);
11555
11585
  appendWorkspaceRunStarted(runEntry);
11556
- const setActiveChild = (child) => {
11557
- runEntry.child = child || null;
11558
- if (controller.signal.aborted) runEntry.stopChild();
11586
+ const setActiveChild = (child, childOptions = {}) => {
11587
+ runControl.setChild(child, childOptions);
11559
11588
  };
11560
- const clearActiveRun = () => {
11589
+ const clearActiveRun = (status = "finished") => {
11590
+ runControl.finish(status);
11561
11591
  if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
11562
11592
  };
11563
11593
  if (wantsStream) {
@@ -11626,7 +11656,7 @@ export function startUiServer({
11626
11656
  }
11627
11657
  res.end();
11628
11658
  } finally {
11629
- clearActiveRun();
11659
+ clearActiveRun(controller.signal.aborted ? "stopped" : "finished");
11630
11660
  }
11631
11661
  return;
11632
11662
  }
@@ -11684,7 +11714,7 @@ export function startUiServer({
11684
11714
  throw e;
11685
11715
  }
11686
11716
  } finally {
11687
- clearActiveRun();
11717
+ clearActiveRun(controller.signal.aborted ? "stopped" : "finished");
11688
11718
  }
11689
11719
  } catch (e) {
11690
11720
  json(res, 500, { error: (e && e.message) || String(e) });
@@ -11750,6 +11780,7 @@ export function startUiServer({
11750
11780
  const entry = entries[0] || null;
11751
11781
  json(res, 200, {
11752
11782
  running: entries.length > 0,
11783
+ state: entry?.runControl?.state || (entries.length > 0 ? "running" : "idle"),
11753
11784
  flowId,
11754
11785
  flowSource,
11755
11786
  runNodeId: entry?.runNodeId || "",
@@ -11762,6 +11793,7 @@ export function startUiServer({
11762
11793
  startedAt: item?.startedAt || null,
11763
11794
  plannedNodeIds: Array.isArray(item?.plannedNodeIds) ? item.plannedNodeIds : [],
11764
11795
  scheduled: item?.scheduled === true,
11796
+ state: item?.runControl?.state || "running",
11765
11797
  })),
11766
11798
  });
11767
11799
  return;
@@ -11792,9 +11824,38 @@ export function startUiServer({
11792
11824
  json(res, 404, { error: "该 Workspace 未在运行" });
11793
11825
  return;
11794
11826
  }
11795
- try { entry.controller?.abort(); } catch (_) {}
11796
- try { entry.stopChild?.(); } catch (_) {}
11797
- json(res, 200, { ok: true, stopped: true });
11827
+ appendWorkspaceRunLogEvent(entry.runId, {
11828
+ type: "stop-requested",
11829
+ runNodeId: entry.runNodeId || "",
11830
+ ts: Date.now(),
11831
+ });
11832
+ const result = await entry.runControl.stop();
11833
+ if (!result.stopped) {
11834
+ appendWorkspaceRunLogEvent(entry.runId, {
11835
+ type: "stop-failed",
11836
+ runNodeId: entry.runNodeId || "",
11837
+ reason: result.timedOut ? "timeout" : "unknown",
11838
+ ts: Date.now(),
11839
+ });
11840
+ json(res, 409, {
11841
+ error: "停止请求已发送,但运行进程未能退出",
11842
+ ok: false,
11843
+ stopped: false,
11844
+ state: entry.runControl.state,
11845
+ });
11846
+ return;
11847
+ }
11848
+ appendWorkspaceRunLogEvent(entry.runId, {
11849
+ type: "stop-completed",
11850
+ runNodeId: entry.runNodeId || "",
11851
+ forced: result.forced === true,
11852
+ ts: Date.now(),
11853
+ });
11854
+ json(res, 200, {
11855
+ ok: true,
11856
+ stopped: true,
11857
+ forced: result.forced === true,
11858
+ });
11798
11859
  return;
11799
11860
  }
11800
11861
 
@@ -0,0 +1,125 @@
1
+ function childHasExited(child) {
2
+ return !child || child.exitCode != null || child.signalCode != null;
3
+ }
4
+
5
+ function waitForPromise(promise, timeoutMs) {
6
+ const ms = Math.max(0, Number(timeoutMs) || 0);
7
+ if (ms === 0) return Promise.resolve({ timedOut: true });
8
+ return new Promise((resolve) => {
9
+ let settled = false;
10
+ const timer = setTimeout(() => {
11
+ if (settled) return;
12
+ settled = true;
13
+ resolve({ timedOut: true });
14
+ }, ms);
15
+ Promise.resolve(promise).then((value) => {
16
+ if (settled) return;
17
+ settled = true;
18
+ clearTimeout(timer);
19
+ resolve({ timedOut: false, value });
20
+ });
21
+ });
22
+ }
23
+
24
+ export function terminateWorkspaceChild(child, options = {}) {
25
+ if (childHasExited(child)) return false;
26
+ const signal = String(options.signal || "SIGTERM");
27
+ const processGroup = options.processGroup === true && process.platform !== "win32";
28
+ const pid = Number(child?.pid || 0);
29
+ if (processGroup && pid > 0) {
30
+ try {
31
+ process.kill(-pid, signal);
32
+ return true;
33
+ } catch {
34
+ // The group may already be gone. Fall back to the direct child.
35
+ }
36
+ }
37
+ try {
38
+ return child.kill(signal) !== false;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ export function createWorkspaceRunController(options = {}) {
45
+ const abortController = options.abortController || new AbortController();
46
+ const gracefulTimeoutMs = Math.max(0, Number(options.gracefulTimeoutMs ?? 3_000));
47
+ const forceTimeoutMs = Math.max(0, Number(options.forceTimeoutMs ?? 1_000));
48
+ let activeChild = null;
49
+ let activeChildProcessGroup = false;
50
+ let state = "running";
51
+ let finished = false;
52
+ let finishResult = null;
53
+ let resolveDone;
54
+ let stopPromise = null;
55
+ const done = new Promise((resolve) => {
56
+ resolveDone = resolve;
57
+ });
58
+
59
+ const setChild = (child, childOptions = {}) => {
60
+ activeChild = child || null;
61
+ activeChildProcessGroup = Boolean(child && childOptions.processGroup === true);
62
+ if (state === "stopping" && activeChild) {
63
+ terminateWorkspaceChild(activeChild, {
64
+ signal: "SIGTERM",
65
+ processGroup: activeChildProcessGroup,
66
+ });
67
+ }
68
+ };
69
+
70
+ const finish = (status = "finished") => {
71
+ if (finished) return finishResult;
72
+ finished = true;
73
+ state = state === "stopping" ? "stopped" : String(status || "finished");
74
+ finishResult = { status: state };
75
+ activeChild = null;
76
+ activeChildProcessGroup = false;
77
+ resolveDone(finishResult);
78
+ return finishResult;
79
+ };
80
+
81
+ const stop = () => {
82
+ if (stopPromise) return stopPromise;
83
+ stopPromise = (async () => {
84
+ if (finished) return { stopped: true, alreadyFinished: true, status: state };
85
+ state = "stopping";
86
+ try {
87
+ abortController.abort();
88
+ } catch {
89
+ // Best effort; process termination below is authoritative.
90
+ }
91
+ terminateWorkspaceChild(activeChild, {
92
+ signal: "SIGTERM",
93
+ processGroup: activeChildProcessGroup,
94
+ });
95
+ let waited = await waitForPromise(done, gracefulTimeoutMs);
96
+ if (!waited.timedOut) return { stopped: true, forced: false, status: waited.value?.status || state };
97
+
98
+ terminateWorkspaceChild(activeChild, {
99
+ signal: "SIGKILL",
100
+ processGroup: activeChildProcessGroup,
101
+ });
102
+ waited = await waitForPromise(done, forceTimeoutMs);
103
+ if (!waited.timedOut) return { stopped: true, forced: true, status: waited.value?.status || state };
104
+ return { stopped: false, forced: true, timedOut: true, status: state };
105
+ })();
106
+ return stopPromise;
107
+ };
108
+
109
+ return {
110
+ abortController,
111
+ done,
112
+ finish,
113
+ setChild,
114
+ stop,
115
+ get child() {
116
+ return activeChild;
117
+ },
118
+ get state() {
119
+ return state;
120
+ },
121
+ get finished() {
122
+ return finished;
123
+ },
124
+ };
125
+ }