@rudderhq/agent-runtime-utils 0.4.6-canary.8 → 0.5.0

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 (48) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/rudder-mcp-contract.d.ts +1028 -0
  6. package/dist/rudder-mcp-contract.d.ts.map +1 -0
  7. package/dist/rudder-mcp-contract.js +257 -0
  8. package/dist/rudder-mcp-contract.js.map +1 -0
  9. package/dist/rudder-mcp-fingerprint.d.ts +4 -0
  10. package/dist/rudder-mcp-fingerprint.d.ts.map +1 -0
  11. package/dist/rudder-mcp-fingerprint.js +23 -0
  12. package/dist/rudder-mcp-fingerprint.js.map +1 -0
  13. package/dist/rudder-mcp-preflight.d.ts +12 -0
  14. package/dist/rudder-mcp-preflight.d.ts.map +1 -0
  15. package/dist/rudder-mcp-preflight.js +324 -0
  16. package/dist/rudder-mcp-preflight.js.map +1 -0
  17. package/dist/rudder-mcp-preflight.test.d.ts +2 -0
  18. package/dist/rudder-mcp-preflight.test.d.ts.map +1 -0
  19. package/dist/rudder-mcp-preflight.test.js +315 -0
  20. package/dist/rudder-mcp-preflight.test.js.map +1 -0
  21. package/dist/rudder-mcp-server.d.ts.map +1 -1
  22. package/dist/rudder-mcp-server.js +3 -0
  23. package/dist/rudder-mcp-server.js.map +1 -1
  24. package/dist/rudder-mcp-tool-descriptors.generated.d.ts +620 -0
  25. package/dist/rudder-mcp-tool-descriptors.generated.d.ts.map +1 -0
  26. package/dist/rudder-mcp-tool-descriptors.generated.js +699 -0
  27. package/dist/rudder-mcp-tool-descriptors.generated.js.map +1 -0
  28. package/dist/rudder-mcp.d.ts +32 -2
  29. package/dist/rudder-mcp.d.ts.map +1 -1
  30. package/dist/rudder-mcp.js +28 -9
  31. package/dist/rudder-mcp.js.map +1 -1
  32. package/dist/rudder-mcp.test.js +38 -2
  33. package/dist/rudder-mcp.test.js.map +1 -1
  34. package/dist/server-utils.cli.d.ts +41 -3
  35. package/dist/server-utils.cli.d.ts.map +1 -1
  36. package/dist/server-utils.cli.js +307 -23
  37. package/dist/server-utils.cli.js.map +1 -1
  38. package/dist/server-utils.prompts.d.ts +8 -6
  39. package/dist/server-utils.prompts.d.ts.map +1 -1
  40. package/dist/server-utils.prompts.js +77 -24
  41. package/dist/server-utils.prompts.js.map +1 -1
  42. package/dist/server-utils.prompts.test.js +97 -1
  43. package/dist/server-utils.prompts.test.js.map +1 -1
  44. package/dist/server-utils.test.js +582 -5
  45. package/dist/server-utils.test.js.map +1 -1
  46. package/dist/types.d.ts +58 -0
  47. package/dist/types.d.ts.map +1 -1
  48. package/package.json +1 -1
@@ -6,6 +6,29 @@ import path from "node:path";
6
6
  import { defaultPathForPlatform, fileExists, quoteForCmd, resolveCommandPath, resolveSpawnTarget } from "./server-utils.instructions.js";
7
7
  import { appendWithCap, asString, buildManagedSkillOrigin, compactSkillText, DEFAULT_LOCAL_CLI_CREDENTIAL_HOME_ENTRIES, DEFAULT_LOCAL_CLI_OPERATOR_HOME_SHIM_COMMANDS, isChildProcessAlive, isMaintainerOnlySkillTarget, parseObject, readSkillMetadataFromDirectory, resolveInstalledEntryTarget, RUDDER_SKILL_ROOT_RELATIVE_CANDIDATES, runningProcesses, skillLocationLabel } from "./server-utils.process.js";
8
8
  const LOCAL_CLI_CREDENTIAL_AUTH_CHECK_TIMEOUT_MS = 3000;
9
+ const RUDDER_DESKTOP_CLI_ENTRY_ENV = "RUDDER_DESKTOP_CLI_ENTRY";
10
+ export const OPERATOR_INTERRUPT_ABORT_REASON_KIND = "operator_interrupt";
11
+ export function createOperatorInterruptAbortReason(hardDeadlineMs) {
12
+ if (!Number.isFinite(hardDeadlineMs) || hardDeadlineMs <= 0) {
13
+ throw new RangeError("operator interrupt hardDeadlineMs must be a positive finite number");
14
+ }
15
+ return {
16
+ kind: OPERATOR_INTERRUPT_ABORT_REASON_KIND,
17
+ hardDeadlineMs: Math.max(1, Math.floor(hardDeadlineMs)),
18
+ };
19
+ }
20
+ function operatorInterruptHardDeadlineMs(reason) {
21
+ if (!reason || typeof reason !== "object" || Array.isArray(reason))
22
+ return null;
23
+ const candidate = reason;
24
+ if (candidate.kind !== OPERATOR_INTERRUPT_ABORT_REASON_KIND)
25
+ return null;
26
+ if (typeof candidate.hardDeadlineMs !== "number" || !Number.isFinite(candidate.hardDeadlineMs))
27
+ return null;
28
+ if (candidate.hardDeadlineMs <= 0)
29
+ return null;
30
+ return Math.max(1, Math.floor(candidate.hardDeadlineMs));
31
+ }
9
32
  export function ensurePathInEnv(env) {
10
33
  if (typeof env.PATH === "string" && env.PATH.length > 0)
11
34
  return env;
@@ -26,7 +49,7 @@ export function prependPathEntry(env, entry) {
26
49
  [pathKey]: current.length > 0 ? `${entry}${delimiter}${current}` : entry,
27
50
  };
28
51
  }
29
- function killChildProcessTree(child, force) {
52
+ export function killChildProcessTree(child, force) {
30
53
  if (process.platform === "win32" && typeof child.pid === "number" && child.pid > 0) {
31
54
  const args = ["/pid", String(child.pid), "/t"];
32
55
  if (force)
@@ -38,7 +61,39 @@ function killChildProcessTree(child, force) {
38
61
  killer.on("error", () => { });
39
62
  return;
40
63
  }
41
- child.kill(force ? "SIGKILL" : "SIGTERM");
64
+ const signal = force ? "SIGKILL" : "SIGTERM";
65
+ if (typeof child.pid === "number" && child.pid > 0) {
66
+ try {
67
+ process.kill(-child.pid, signal);
68
+ return;
69
+ }
70
+ catch {
71
+ // Fall back to the direct child if its process group is already gone.
72
+ }
73
+ }
74
+ try {
75
+ child.kill(signal);
76
+ }
77
+ catch {
78
+ // The process may have exited between the liveness check and signal.
79
+ }
80
+ }
81
+ function isChildProcessTreeAlive(child) {
82
+ if (process.platform === "win32")
83
+ return isChildProcessAlive(child);
84
+ const pid = child.pid;
85
+ if (typeof pid !== "number" || pid <= 0)
86
+ return false;
87
+ try {
88
+ process.kill(-pid, 0);
89
+ return true;
90
+ }
91
+ catch (error) {
92
+ const code = error instanceof Error && "code" in error ? error.code : null;
93
+ if (code === "EPERM")
94
+ return true;
95
+ return isChildProcessAlive(child);
96
+ }
42
97
  }
43
98
  export async function findAncestorWithFile(startDir, relativePath, maxDepth = 12) {
44
99
  let current = path.resolve(startDir);
@@ -56,12 +111,69 @@ export async function findAncestorWithFile(startDir, relativePath, maxDepth = 12
56
111
  export function shellQuote(arg) {
57
112
  return `'${arg.replace(/'/g, `'\\''`)}'`;
58
113
  }
114
+ async function readPackageVersion(filePath, expectedName) {
115
+ try {
116
+ const parsed = JSON.parse(await fs.readFile(filePath, "utf8"));
117
+ if (expectedName && parsed.name !== expectedName)
118
+ return null;
119
+ return typeof parsed.version === "string" && parsed.version.trim().length > 0 ? parsed.version.trim() : null;
120
+ }
121
+ catch {
122
+ return null;
123
+ }
124
+ }
125
+ async function cliVersionBesideEntry(cliEntry) {
126
+ const directory = path.dirname(cliEntry);
127
+ return await readPackageVersion(path.join(directory, "rudder-cli-package.json"), "@rudderhq/cli")
128
+ ?? await readPackageVersion(path.join(directory, "package.json"), "@rudderhq/cli");
129
+ }
130
+ async function resolveExplicitDesktopCliEntry(env = process.env) {
131
+ const configured = env[RUDDER_DESKTOP_CLI_ENTRY_ENV]?.trim();
132
+ if (!configured)
133
+ return null;
134
+ const cliEntry = path.resolve(configured);
135
+ return await fileExists(cliEntry) ? cliEntry : null;
136
+ }
137
+ async function resolveDesktopExecutableBesideCliEntry(cliEntry) {
138
+ const resourcesDir = path.dirname(path.dirname(cliEntry));
139
+ const appRoot = path.dirname(resourcesDir);
140
+ const executable = process.platform === "darwin"
141
+ ? path.join(appRoot, "MacOS", "Rudder")
142
+ : path.join(appRoot, process.platform === "win32" ? "Rudder.exe" : "Rudder");
143
+ return await fileExists(executable) ? executable : null;
144
+ }
145
+ export function resolveDesktopCliSpawnTarget(cliEntry, executable, platform = process.platform, nodeRunner = null) {
146
+ if (platform === "win32" && nodeRunner) {
147
+ return {
148
+ command: executable,
149
+ args: [nodeRunner],
150
+ env: { ELECTRON_RUN_AS_NODE: "1" },
151
+ };
152
+ }
153
+ return {
154
+ command: executable,
155
+ args: ["--desktop-cli"],
156
+ };
157
+ }
59
158
  export async function resolveRudderCliShimTarget(moduleDir) {
159
+ const explicitDesktopCli = await resolveExplicitDesktopCliEntry();
160
+ if (explicitDesktopCli) {
161
+ const executable = await resolveDesktopExecutableBesideCliEntry(explicitDesktopCli) ?? process.execPath;
162
+ const nodeRunner = path.join(path.dirname(explicitDesktopCli), "desktop-cli-runner.js");
163
+ return {
164
+ ...resolveDesktopCliSpawnTarget(explicitDesktopCli, executable, process.platform, await fileExists(nodeRunner) ? nodeRunner : null),
165
+ provenance: "desktop_bundle",
166
+ version: await cliVersionBesideEntry(explicitDesktopCli),
167
+ };
168
+ }
60
169
  const packagedCli = await findAncestorWithFile(moduleDir, "desktop-cli.js");
61
170
  if (packagedCli) {
171
+ const runtimeMetadata = await findAncestorWithFile(moduleDir, "runtime.json");
62
172
  return {
63
173
  command: process.execPath,
64
174
  args: [packagedCli],
175
+ provenance: runtimeMetadata ? "external_runtime" : "desktop_bundle",
176
+ version: await cliVersionBesideEntry(packagedCli),
65
177
  };
66
178
  }
67
179
  const repoRoot = await findAncestorWithFile(moduleDir, path.join("cli", "src", "index.ts"));
@@ -74,6 +186,8 @@ export async function resolveRudderCliShimTarget(moduleDir) {
74
186
  return {
75
187
  command: process.execPath,
76
188
  args: [tsxEntry, cliSource],
189
+ provenance: "repo",
190
+ version: await readPackageVersion(path.join(rootDir, "cli", "package.json"), "@rudderhq/cli"),
77
191
  };
78
192
  }
79
193
  const builtCliEntry = path.join(rootDir, "cli", "dist", "index.js");
@@ -81,13 +195,15 @@ export async function resolveRudderCliShimTarget(moduleDir) {
81
195
  return {
82
196
  command: process.execPath,
83
197
  args: [builtCliEntry],
198
+ provenance: "repo",
199
+ version: await readPackageVersion(path.join(rootDir, "cli", "package.json"), "@rudderhq/cli"),
84
200
  };
85
201
  }
86
202
  return null;
87
203
  }
88
204
  export async function materializeRudderCliShim(target) {
89
205
  const hash = createHash("sha1")
90
- .update(JSON.stringify({ command: target.command, args: target.args, platform: process.platform }))
206
+ .update(JSON.stringify({ command: target.command, args: target.args, env: target.env, platform: process.platform }))
91
207
  .digest("hex")
92
208
  .slice(0, 12);
93
209
  const shimDir = path.join(os.tmpdir(), "rudder-cli-shims", hash);
@@ -95,7 +211,8 @@ export async function materializeRudderCliShim(target) {
95
211
  if (process.platform === "win32") {
96
212
  const shimPath = path.join(shimDir, "rudder.cmd");
97
213
  const commandLine = [quoteForCmd(target.command), ...target.args.map(quoteForCmd), "%*"].join(" ");
98
- await fs.writeFile(shimPath, `@echo off\r\n${commandLine}\r\n`, "utf8");
214
+ const electronNodeMode = target.env?.ELECTRON_RUN_AS_NODE === "1";
215
+ await fs.writeFile(shimPath, `@echo off\r\n${electronNodeMode ? "set \"ELECTRON_RUN_AS_NODE=1\"\r\n" : ""}${commandLine}\r\n`, "utf8");
99
216
  return shimPath;
100
217
  }
101
218
  const shimPath = path.join(shimDir, "rudder");
@@ -347,19 +464,31 @@ export function readRudderSkillSyncPreference(config) {
347
464
  };
348
465
  }
349
466
  export function canonicalizeDesiredRudderSkillReference(reference, availableEntries) {
350
- const normalizedReference = reference.trim().toLowerCase().replace(/^rudder\/rudder\//, "rudder/");
467
+ const normalizedReference = reference.trim().toLowerCase();
351
468
  if (!normalizedReference)
352
469
  return "";
353
470
  const exactKey = availableEntries.find((entry) => entry.key.trim().toLowerCase() === normalizedReference);
354
471
  if (exactKey)
355
472
  return exactKey.key;
356
- const byRuntimeName = availableEntries.filter((entry) => typeof entry.runtimeName === "string" && entry.runtimeName.trim().toLowerCase() === normalizedReference);
473
+ if (normalizedReference === "rudder"
474
+ || normalizedReference === "rudder/rudder"
475
+ || normalizedReference === "bundled:rudder/rudder") {
476
+ const canonicalRudderDocsEntry = availableEntries.find((entry) => entry.key.trim().toLowerCase() === "rudder/rudder-docs"
477
+ || entry.key.trim().toLowerCase() === "bundled:rudder/rudder-docs"
478
+ || entry.runtimeName?.trim().toLowerCase() === "rudder-docs");
479
+ return canonicalRudderDocsEntry?.key ?? "rudder/rudder-docs";
480
+ }
481
+ const lookupReference = normalizedReference.replace(/^rudder\/rudder\//, "rudder/");
482
+ const legacyNestedKey = availableEntries.find((entry) => entry.key.trim().toLowerCase() === lookupReference);
483
+ if (legacyNestedKey)
484
+ return legacyNestedKey.key;
485
+ const byRuntimeName = availableEntries.filter((entry) => typeof entry.runtimeName === "string" && entry.runtimeName.trim().toLowerCase() === lookupReference);
357
486
  if (byRuntimeName.length === 1)
358
487
  return byRuntimeName[0].key;
359
- const slugMatches = availableEntries.filter((entry) => entry.key.trim().toLowerCase().split("/").pop() === normalizedReference);
488
+ const slugMatches = availableEntries.filter((entry) => entry.key.trim().toLowerCase().split("/").pop() === lookupReference);
360
489
  if (slugMatches.length === 1)
361
490
  return slugMatches[0].key;
362
- return normalizedReference;
491
+ return lookupReference;
363
492
  }
364
493
  export function resolveRudderDesiredSkillNames(config, availableEntries) {
365
494
  const preference = readRudderSkillSyncPreference(config);
@@ -368,6 +497,14 @@ export function resolveRudderDesiredSkillNames(config, availableEntries) {
368
497
  .filter(Boolean);
369
498
  return Array.from(new Set(desiredSkills));
370
499
  }
500
+ export function filterRudderDesiredSkillsForBrowserCapability(availableEntries, desiredSkills, browserEnabled) {
501
+ if (browserEnabled)
502
+ return [...desiredSkills];
503
+ const browserSkillKeys = new Set(availableEntries
504
+ .filter((entry) => entry.runtimeName?.trim().toLowerCase() === "browser")
505
+ .map((entry) => entry.key));
506
+ return desiredSkills.filter((skill) => !browserSkillKeys.has(skill));
507
+ }
371
508
  export function writeRudderSkillSyncPreference(config, desiredSkills) {
372
509
  const next = { ...config };
373
510
  const raw = next.rudderSkillSync;
@@ -650,6 +787,120 @@ export async function ensureRudderSkillSymlink(source, target, linkSkill = (link
650
787
  export async function removeMaintainerOnlySkillSymlinks(skillsHome, allowedSkillNames) {
651
788
  return removeUnselectedRudderSkillSymlinks(skillsHome, allowedSkillNames);
652
789
  }
790
+ function isFileSystemErrorCode(error, code) {
791
+ return error instanceof Error && "code" in error && error.code === code;
792
+ }
793
+ function sameFileSystemEntry(left, right) {
794
+ return left.dev === right.dev && left.ino === right.ino;
795
+ }
796
+ export async function cleanupLegacyRudderDocsManagedEntry(skillsHome, selectedEntries) {
797
+ const targetPath = path.join(path.resolve(skillsHome), "rudder");
798
+ const canonicalEntry = Array.from(selectedEntries).find((entry) => (entry.key === "rudder/rudder-docs" || entry.key === "bundled:rudder/rudder-docs")
799
+ && entry.runtimeName === "rudder-docs");
800
+ if (!canonicalEntry) {
801
+ return { state: "not_applicable", targetPath, legacySourcePath: null, kind: null };
802
+ }
803
+ const legacySourcePath = path.join(path.dirname(path.resolve(canonicalEntry.source)), "rudder");
804
+ let existing;
805
+ try {
806
+ existing = await fs.lstat(targetPath);
807
+ }
808
+ catch (error) {
809
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
810
+ return { state: "absent", targetPath, legacySourcePath, kind: null };
811
+ }
812
+ return { state: "collision", targetPath, legacySourcePath, kind: "other" };
813
+ }
814
+ if (existing.isSymbolicLink()) {
815
+ const linkedPath = await fs.readlink(targetPath).catch(() => null);
816
+ if (linkedPath === null) {
817
+ return { state: "collision", targetPath, legacySourcePath, kind: "symlink" };
818
+ }
819
+ const resolvedLinkedPath = path.resolve(path.dirname(targetPath), linkedPath);
820
+ if (resolvedLinkedPath !== legacySourcePath) {
821
+ return { state: "collision", targetPath, legacySourcePath, kind: "symlink" };
822
+ }
823
+ try {
824
+ await fs.stat(resolvedLinkedPath);
825
+ return { state: "collision", targetPath, legacySourcePath, kind: "symlink" };
826
+ }
827
+ catch (error) {
828
+ if (!isFileSystemErrorCode(error, "ENOENT")) {
829
+ return { state: "collision", targetPath, legacySourcePath, kind: "symlink" };
830
+ }
831
+ }
832
+ // Ownership evidence is path-based, so revalidate inode, type, and target immediately before deletion.
833
+ const revalidated = await fs.lstat(targetPath).catch(() => null);
834
+ if (!revalidated?.isSymbolicLink() || !sameFileSystemEntry(existing, revalidated)) {
835
+ return { state: "collision", targetPath, legacySourcePath, kind: "symlink" };
836
+ }
837
+ const revalidatedLink = await fs.readlink(targetPath).catch(() => null);
838
+ if (revalidatedLink === null
839
+ || path.resolve(path.dirname(targetPath), revalidatedLink) !== legacySourcePath) {
840
+ return { state: "collision", targetPath, legacySourcePath, kind: "symlink" };
841
+ }
842
+ const finalEntry = await fs.lstat(targetPath).catch(() => null);
843
+ if (!finalEntry?.isSymbolicLink() || !sameFileSystemEntry(existing, finalEntry)) {
844
+ return { state: "collision", targetPath, legacySourcePath, kind: "symlink" };
845
+ }
846
+ try {
847
+ await fs.stat(legacySourcePath);
848
+ return { state: "collision", targetPath, legacySourcePath, kind: "symlink" };
849
+ }
850
+ catch (error) {
851
+ if (!isFileSystemErrorCode(error, "ENOENT")) {
852
+ return { state: "collision", targetPath, legacySourcePath, kind: "symlink" };
853
+ }
854
+ }
855
+ try {
856
+ await fs.unlink(targetPath);
857
+ }
858
+ catch (error) {
859
+ return {
860
+ state: "failed",
861
+ targetPath,
862
+ legacySourcePath,
863
+ kind: "symlink",
864
+ detail: `unlink failed: ${error instanceof Error ? error.message : String(error)}`,
865
+ };
866
+ }
867
+ return { state: "removed", targetPath, legacySourcePath, kind: "symlink" };
868
+ }
869
+ if (existing.isDirectory()) {
870
+ const materializedSource = await readRudderMaterializedSkillSource(targetPath);
871
+ if (materializedSource === legacySourcePath) {
872
+ // A matching manifest can move with a replacement; inode and provenance must both remain stable.
873
+ const revalidated = await fs.lstat(targetPath).catch(() => null);
874
+ if (!revalidated?.isDirectory() || !sameFileSystemEntry(existing, revalidated)) {
875
+ return { state: "collision", targetPath, legacySourcePath, kind: "directory" };
876
+ }
877
+ const revalidatedSource = await readRudderMaterializedSkillSource(targetPath);
878
+ if (revalidatedSource !== legacySourcePath) {
879
+ return { state: "collision", targetPath, legacySourcePath, kind: "directory" };
880
+ }
881
+ const finalEntry = await fs.lstat(targetPath).catch(() => null);
882
+ if (!finalEntry?.isDirectory() || !sameFileSystemEntry(existing, finalEntry)) {
883
+ return { state: "collision", targetPath, legacySourcePath, kind: "directory" };
884
+ }
885
+ try {
886
+ await fs.rm(targetPath, { recursive: true, force: true });
887
+ }
888
+ catch (error) {
889
+ return {
890
+ state: "failed",
891
+ targetPath,
892
+ legacySourcePath,
893
+ kind: "directory",
894
+ detail: `recursive removal failed: ${error instanceof Error ? error.message : String(error)}`,
895
+ };
896
+ }
897
+ return { state: "removed", targetPath, legacySourcePath, kind: "directory" };
898
+ }
899
+ return { state: "collision", targetPath, legacySourcePath, kind: "directory" };
900
+ }
901
+ const kind = existing.isFile() ? "file" : "other";
902
+ return { state: "collision", targetPath, legacySourcePath, kind };
903
+ }
653
904
  async function readRudderMaterializedSkillSource(target) {
654
905
  const manifestPath = path.join(target, ".rudder", "materialized-skill.json");
655
906
  const raw = await fs.readFile(manifestPath, "utf8").catch(() => null);
@@ -722,6 +973,7 @@ export async function runChildProcess(runId, command, args, opts) {
722
973
  const onLogError = opts.onLogError ?? ((err, id, msg) => console.warn({ err, runId: id }, msg));
723
974
  return new Promise((resolve, reject) => {
724
975
  const rawMerged = { ...process.env, ...opts.env };
976
+ delete rawMerged.RUDDER_DESKTOP_CLI_ENTRY;
725
977
  const requestedHome = typeof opts.env.HOME === "string" && opts.env.HOME.trim().length > 0
726
978
  ? path.resolve(opts.env.HOME)
727
979
  : null;
@@ -777,6 +1029,7 @@ export async function runChildProcess(runId, command, args, opts) {
777
1029
  }
778
1030
  const child = spawn(target.command, target.args, {
779
1031
  cwd: opts.cwd,
1032
+ detached: process.platform !== "win32",
780
1033
  env: mergedEnv,
781
1034
  shell: false,
782
1035
  stdio: [opts.stdin != null ? "pipe" : "ignore", "pipe", "pipe"],
@@ -797,43 +1050,70 @@ export async function runChildProcess(runId, command, args, opts) {
797
1050
  let stdout = "";
798
1051
  let stderr = "";
799
1052
  let logChain = Promise.resolve();
1053
+ let operatorInterrupted = false;
1054
+ let forceKillTimer = null;
1055
+ let forceKillAt = Number.POSITIVE_INFINITY;
1056
+ const clearForceKillTimer = () => {
1057
+ if (forceKillTimer)
1058
+ clearTimeout(forceKillTimer);
1059
+ forceKillTimer = null;
1060
+ forceKillAt = Number.POSITIVE_INFINITY;
1061
+ };
1062
+ const scheduleForceKill = (delayMs) => {
1063
+ const boundedDelayMs = Math.max(1, delayMs);
1064
+ const killAt = Date.now() + boundedDelayMs;
1065
+ if (forceKillTimer && forceKillAt <= killAt)
1066
+ return;
1067
+ clearForceKillTimer();
1068
+ forceKillAt = killAt;
1069
+ forceKillTimer = setTimeout(() => {
1070
+ forceKillTimer = null;
1071
+ forceKillAt = Number.POSITIVE_INFINITY;
1072
+ if (isChildProcessTreeAlive(child)) {
1073
+ killChildProcessTree(child, true);
1074
+ }
1075
+ }, boundedDelayMs);
1076
+ };
800
1077
  const timeout = opts.timeoutSec > 0
801
1078
  ? setTimeout(() => {
802
1079
  timedOut = true;
803
1080
  killChildProcessTree(child, false);
804
- setTimeout(() => {
805
- if (isChildProcessAlive(child)) {
806
- killChildProcessTree(child, true);
807
- }
808
- }, Math.max(1, opts.graceSec) * 1000);
1081
+ scheduleForceKill(Math.max(1, opts.graceSec) * 1000);
809
1082
  }, opts.timeoutSec * 1000)
810
1083
  : null;
811
1084
  let abortCleanup = null;
812
1085
  if (opts.abortSignal) {
813
1086
  const onAbort = () => {
814
1087
  aborted = true;
1088
+ const operatorHardDeadlineMs = operatorInterruptHardDeadlineMs(opts.abortSignal?.reason);
1089
+ operatorInterrupted = operatorHardDeadlineMs !== null;
815
1090
  killChildProcessTree(child, false);
816
- setTimeout(() => {
817
- if (isChildProcessAlive(child)) {
818
- killChildProcessTree(child, true);
819
- }
820
- }, Math.max(1, opts.graceSec) * 1000);
1091
+ scheduleForceKill(operatorHardDeadlineMs ?? Math.max(1, opts.graceSec) * 1000);
821
1092
  };
822
- opts.abortSignal.addEventListener("abort", onAbort, { once: true });
823
- abortCleanup = () => opts.abortSignal?.removeEventListener("abort", onAbort);
1093
+ if (opts.abortSignal.aborted) {
1094
+ onAbort();
1095
+ }
1096
+ else {
1097
+ opts.abortSignal.addEventListener("abort", onAbort, { once: true });
1098
+ abortCleanup = () => opts.abortSignal?.removeEventListener("abort", onAbort);
1099
+ }
824
1100
  }
825
1101
  child.stdout?.on("data", (chunk) => {
826
1102
  const text = String(chunk);
827
1103
  stdout = appendWithCap(stdout, text);
1104
+ if (operatorInterrupted)
1105
+ return;
828
1106
  logChain = logChain
829
- .then(() => opts.onLog("stdout", text))
1107
+ .then(() => operatorInterrupted ? undefined : opts.onLog("stdout", text))
830
1108
  .catch((err) => onLogError(err, runId, "failed to append stdout log chunk"));
831
1109
  });
832
1110
  child.stderr?.on("data", (chunk) => {
833
1111
  const text = String(chunk);
834
1112
  stderr = appendWithCap(stderr, text);
1113
+ if (operatorInterrupted)
1114
+ return;
835
1115
  logChain = logChain
836
- .then(() => opts.onLog("stderr", text))
1116
+ .then(() => operatorInterrupted ? undefined : opts.onLog("stderr", text))
837
1117
  .catch((err) => onLogError(err, runId, "failed to append stderr log chunk"));
838
1118
  });
839
1119
  child.on("error", (err) => {
@@ -841,6 +1121,7 @@ export async function runChildProcess(runId, command, args, opts) {
841
1121
  clearTimeout(timeout);
842
1122
  if (abortCleanup)
843
1123
  abortCleanup();
1124
+ clearForceKillTimer();
844
1125
  runningProcesses.delete(runId);
845
1126
  const errno = err.code;
846
1127
  const pathValue = mergedEnv.PATH ?? mergedEnv.Path ?? "";
@@ -854,8 +1135,11 @@ export async function runChildProcess(runId, command, args, opts) {
854
1135
  clearTimeout(timeout);
855
1136
  if (abortCleanup)
856
1137
  abortCleanup();
1138
+ if (!isChildProcessTreeAlive(child))
1139
+ clearForceKillTimer();
857
1140
  runningProcesses.delete(runId);
858
- void logChain.finally(() => {
1141
+ const completionBarrier = operatorInterrupted ? Promise.resolve() : logChain;
1142
+ void completionBarrier.finally(() => {
859
1143
  resolve({
860
1144
  exitCode: code,
861
1145
  signal: aborted ? "SIGTERM" : signal,