@nowcrew/daemon 0.5.28 → 0.5.29

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 (67) hide show
  1. package/dist/attachments.js +196 -0
  2. package/dist/bound-im-decision.js +22 -0
  3. package/dist/completion-retransmitter.js +77 -0
  4. package/dist/computer-cli.js +274 -0
  5. package/dist/computer-profile-lock.js +395 -0
  6. package/dist/computer-profile.js +364 -0
  7. package/dist/computer-service.js +358 -0
  8. package/dist/config.js +82 -0
  9. package/dist/console-collapse.js +13 -0
  10. package/dist/console-formatter.js +77 -0
  11. package/dist/console-payload.js +73 -0
  12. package/dist/console.js +329 -0
  13. package/dist/daemon-startup-error.js +30 -0
  14. package/dist/execution-backend.js +44 -0
  15. package/dist/execution-event-limit.js +64 -0
  16. package/dist/execution-journal-lock.js +421 -0
  17. package/dist/execution-journal.js +716 -0
  18. package/dist/execution-protocol.js +342 -0
  19. package/dist/execution-recovery.js +95 -0
  20. package/dist/execution-runner.js +659 -0
  21. package/dist/execution-supervisor-child.js +236 -0
  22. package/dist/execution-supervisor.js +316 -0
  23. package/dist/execution-telemetry-journal.js +71 -0
  24. package/dist/external-output.js +114 -0
  25. package/dist/i18n.js +64 -0
  26. package/dist/json-result.js +27 -0
  27. package/dist/list-models.js +92 -0
  28. package/dist/local-executor.js +439 -0
  29. package/dist/log-format.js +10 -0
  30. package/dist/machine-info.js +124 -0
  31. package/dist/main.js +118 -0
  32. package/dist/normalize.js +170 -0
  33. package/dist/origin-decision.js +44 -0
  34. package/dist/platform.js +8 -0
  35. package/dist/prompt.js +307 -0
  36. package/dist/provider-env.js +90 -0
  37. package/dist/runner.js +234 -0
  38. package/dist/runtime-cancellation.js +74 -0
  39. package/dist/runtime-capabilities.js +43 -0
  40. package/dist/runtime-path.js +60 -0
  41. package/dist/runtimes/claude.js +51 -0
  42. package/dist/runtimes/codex-app-server-runner.js +344 -0
  43. package/dist/runtimes/codex-deepseek-catalog.js +7 -0
  44. package/dist/runtimes/codex-deepseek-config.js +50 -0
  45. package/dist/runtimes/codex.js +53 -0
  46. package/dist/runtimes/kimi-acp-runner.js +364 -0
  47. package/dist/runtimes/kimi.js +45 -0
  48. package/dist/runtimes/progress-watchdog.js +26 -0
  49. package/dist/scheduled-report.js +51 -0
  50. package/dist/scheduled-run-report.js +57 -0
  51. package/dist/serve-lifecycle.js +82 -0
  52. package/dist/serve.js +868 -0
  53. package/dist/session.js +82 -0
  54. package/dist/shared-execution-slots.js +68 -0
  55. package/dist/shutdown-deadline.js +32 -0
  56. package/dist/skill-preview.js +21 -0
  57. package/dist/skills.js +56 -0
  58. package/dist/slog.js +228 -0
  59. package/dist/supervised-runtime.js +104 -0
  60. package/dist/token.js +24 -0
  61. package/dist/unified-diff.js +84 -0
  62. package/dist/websocket-shutdown.js +53 -0
  63. package/dist/win32-job-object.js +193 -0
  64. package/dist/workspace-fs.js +80 -0
  65. package/dist/workspace-import.js +127 -0
  66. package/dist/workspace.js +148 -0
  67. package/package.json +1 -1
@@ -0,0 +1,236 @@
1
+ import spawn from "cross-spawn";
2
+ import { z } from "zod";
3
+ import { pathToFileURL } from "node:url";
4
+ import { assignProcessToJob, createKillOnCloseJob, terminateJob, } from "./win32-job-object.js";
5
+ const LaunchSchema = z.object({
6
+ command: z.string().min(1),
7
+ args: z.array(z.string()),
8
+ cwd: z.string().min(1),
9
+ env: z.record(z.string()),
10
+ stdinText: z.string().optional(),
11
+ }).strict();
12
+ function messageOf(error) {
13
+ return error instanceof Error ? error.message : String(error);
14
+ }
15
+ function send(message, callback) {
16
+ if (!process.connected) {
17
+ callback?.();
18
+ return;
19
+ }
20
+ process.send?.(message, () => callback?.());
21
+ }
22
+ /**
23
+ * Keep consuming runtime output even when the daemon-side pipe disappears. A direct `pipe()` can
24
+ * crash this supervisor with EPIPE and orphan the runtime. While connected, source pause/resume
25
+ * preserves authoritative final output. Once disconnected, the source remains drained and discarded.
26
+ */
27
+ function forwardWhileWritable(source, destination) {
28
+ if (source === null)
29
+ return { discard: () => undefined };
30
+ let blocked = false;
31
+ let broken = destination.destroyed;
32
+ const discard = () => {
33
+ broken = true;
34
+ blocked = false;
35
+ source.resume();
36
+ };
37
+ destination.on("error", discard);
38
+ destination.on("close", discard);
39
+ destination.on("drain", () => {
40
+ if (broken)
41
+ return;
42
+ blocked = false;
43
+ source.resume();
44
+ });
45
+ source.on("data", (chunk) => {
46
+ if (broken)
47
+ return;
48
+ try {
49
+ if (!destination.write(chunk)) {
50
+ blocked = true;
51
+ source.pause();
52
+ }
53
+ }
54
+ catch {
55
+ discard();
56
+ }
57
+ });
58
+ source.on("error", () => undefined);
59
+ return { discard };
60
+ }
61
+ export function runExecutionSupervisorChild() {
62
+ let launch = null;
63
+ // 是否用 Job Object 拥有 runtime 树。由 parent 依据「durable + win32 + Job Object 后端已选中」决定并下发;
64
+ // legacy(process-lifetime)路径恒为 false —— 关键:不能仅凭 process.platform 就建 Job,否则灰度关时
65
+ // legacy Windows 执行会被强行套 Job,且 koffi 加载失败会误杀 legacy runtime(退回 fail-closed 承诺被破坏)。
66
+ let useJobObject = false;
67
+ let released = false;
68
+ let runtime = null;
69
+ let settled = false;
70
+ let cleaningTree = false;
71
+ // win32:本 child 创建并持有的 Job(KILL_ON_JOB_CLOSE)。持有它 = 拥有整棵 runtime 进程树:
72
+ // 本进程一死(含崩溃)内核即回收 Job 内全部进程,等价于 POSIX killpg 且覆盖 supervisor 自身崩溃。
73
+ let jobHandle = null;
74
+ const outputForwarders = [];
75
+ const terminateOwnedTree = () => {
76
+ if (cleaningTree)
77
+ return;
78
+ cleaningTree = true;
79
+ for (const forwarder of outputForwarders)
80
+ forwarder.discard();
81
+ if (process.platform === "win32") {
82
+ // 显式一次性杀光 Job 内进程;即便这里失败,process.exit 关闭 job handle 也会触发内核回收。
83
+ if (jobHandle !== null) {
84
+ try {
85
+ terminateJob(jobHandle);
86
+ }
87
+ catch {
88
+ // handle 随进程退出自动关闭,KILL_ON_JOB_CLOSE 兜底。
89
+ }
90
+ }
91
+ process.exit(1);
92
+ return;
93
+ }
94
+ try {
95
+ process.kill(-process.pid, "SIGTERM");
96
+ }
97
+ catch (error) {
98
+ if (error.code !== "ESRCH")
99
+ throw error;
100
+ }
101
+ setTimeout(() => {
102
+ try {
103
+ process.kill(-process.pid, "SIGKILL");
104
+ }
105
+ catch (error) {
106
+ if (error.code !== "ESRCH")
107
+ throw error;
108
+ process.exit();
109
+ }
110
+ }, 250);
111
+ };
112
+ const stopBeforeRelease = () => {
113
+ if (released || settled)
114
+ return;
115
+ settled = true;
116
+ process.exit(0);
117
+ };
118
+ process.on("SIGTERM", () => {
119
+ if (released)
120
+ terminateOwnedTree();
121
+ else
122
+ stopBeforeRelease();
123
+ });
124
+ process.on("SIGINT", () => {
125
+ if (released)
126
+ terminateOwnedTree();
127
+ else
128
+ stopBeforeRelease();
129
+ });
130
+ process.on("disconnect", () => {
131
+ if (released)
132
+ terminateOwnedTree();
133
+ else
134
+ stopBeforeRelease();
135
+ });
136
+ process.on("message", (raw) => {
137
+ if (raw.type === "launch") {
138
+ if (launch !== null) {
139
+ process.exitCode = 2;
140
+ return;
141
+ }
142
+ const parsed = LaunchSchema.safeParse(raw.launch);
143
+ if (!parsed.success) {
144
+ send({ type: "runtime-spawn-error", message: parsed.error.message });
145
+ process.exit(2);
146
+ return;
147
+ }
148
+ launch = parsed.data;
149
+ useJobObject = raw.useJobObject === true;
150
+ send({ type: "ready" });
151
+ return;
152
+ }
153
+ if (raw.type === "abort") {
154
+ if (runtime === null)
155
+ stopBeforeRelease();
156
+ else
157
+ runtime.kill("SIGTERM");
158
+ return;
159
+ }
160
+ if (raw.type !== "release" || released || launch === null)
161
+ return;
162
+ released = true;
163
+ if (useJobObject) {
164
+ // 所有权链条落地(仅 durable win32):先建 Job(带 KILL_ON_JOB_CLOSE),再 spawn,spawn 后立即 assign。
165
+ // 建 Job 失败即无法保证所有权 → 宁可不 spawn,报 spawn-error 让上层 fail-closed。
166
+ try {
167
+ jobHandle = createKillOnCloseJob();
168
+ }
169
+ catch (error) {
170
+ send({ type: "runtime-spawn-error", message: `Job Object creation failed: ${messageOf(error)}` });
171
+ process.exit(2);
172
+ return;
173
+ }
174
+ }
175
+ const child = spawn(launch.command, launch.args, {
176
+ cwd: launch.cwd,
177
+ env: launch.env,
178
+ detached: false,
179
+ stdio: [launch.stdinText === undefined ? "ignore" : "pipe", "pipe", "pipe"],
180
+ });
181
+ runtime = child;
182
+ if (jobHandle !== null && child.pid !== undefined) {
183
+ // spawn 返回后 child.pid 同步可用,此刻立即 assign,把「assign 前已 fork 孙进程」的窗口压到最小。
184
+ try {
185
+ assignProcessToJob(jobHandle, child.pid);
186
+ }
187
+ catch (error) {
188
+ // 无法建立所有权:杀掉刚起的 runtime 并拆除,不让它脱离 Job 裸奔。
189
+ send({ type: "runtime-spawn-error", message: `AssignProcessToJobObject failed: ${messageOf(error)}` });
190
+ try {
191
+ child.kill("SIGKILL");
192
+ }
193
+ catch {
194
+ // 已退出。
195
+ }
196
+ terminateOwnedTree();
197
+ return;
198
+ }
199
+ }
200
+ outputForwarders.push(forwardWhileWritable(child.stdout, process.stdout), forwardWhileWritable(child.stderr, process.stderr));
201
+ child.once("spawn", () => {
202
+ send({ type: "runtime-started" });
203
+ if (launch?.stdinText !== undefined && child.stdin !== null) {
204
+ child.stdin.on("error", () => undefined);
205
+ child.stdin.end(launch.stdinText);
206
+ }
207
+ });
208
+ child.once("error", (error) => {
209
+ send({ type: "runtime-spawn-error", message: error.message });
210
+ });
211
+ child.once("close", (code, signal) => {
212
+ const exitMessage = {
213
+ type: "runtime-exit",
214
+ exitCode: code ?? 128,
215
+ ...(signal === null ? {} : { terminationSignal: signal }),
216
+ };
217
+ settled = true;
218
+ process.exitCode = code ?? 128;
219
+ let cleanupStarted = false;
220
+ const cleanup = () => {
221
+ if (cleanupStarted)
222
+ return;
223
+ cleanupStarted = true;
224
+ terminateOwnedTree();
225
+ };
226
+ const fallback = setTimeout(cleanup, 50);
227
+ send(exitMessage, () => {
228
+ clearTimeout(fallback);
229
+ cleanup();
230
+ });
231
+ });
232
+ });
233
+ }
234
+ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
235
+ runExecutionSupervisorChild();
236
+ }
@@ -0,0 +1,316 @@
1
+ import { fork } from "node:child_process";
2
+ import { fileURLToPath } from "node:url";
3
+ import { executionBackendCapability, ownsRuntimeViaJobObject } from "./execution-backend.js";
4
+ const DEFAULT_ABORT_TIMEOUT_MS = 5_000;
5
+ const DEFAULT_HANDSHAKE_TIMEOUT_MS = 5_000;
6
+ const DEFAULT_TASKKILL_TIMEOUT_MS = 5_000;
7
+ const PROCESS_GROUP_POLL_MS = 10;
8
+ function messageError(error) {
9
+ return error instanceof Error ? error : new Error(String(error));
10
+ }
11
+ async function waitForExit(exit, timeoutMs, pid) {
12
+ let timer;
13
+ try {
14
+ await Promise.race([
15
+ exit.then(() => undefined),
16
+ new Promise((_resolve, reject) => {
17
+ timer = setTimeout(() => reject(new Error(`Supervisor ${pid} did not exit within ${timeoutMs}ms`)), timeoutMs);
18
+ }),
19
+ ]);
20
+ }
21
+ finally {
22
+ if (timer !== undefined)
23
+ clearTimeout(timer);
24
+ }
25
+ }
26
+ async function waitForProcessGroupExit(pid, timeoutMs) {
27
+ const deadline = Date.now() + timeoutMs;
28
+ while (true) {
29
+ try {
30
+ process.kill(-pid, 0);
31
+ }
32
+ catch (error) {
33
+ const code = error.code;
34
+ if (code === "ESRCH")
35
+ return;
36
+ // POSIX defines EPERM here as "the process group exists, but is not signalable".
37
+ // It is therefore an alive observation, not a completed cleanup or an API failure.
38
+ if (code !== "EPERM")
39
+ throw error;
40
+ }
41
+ if (Date.now() >= deadline) {
42
+ throw new Error(`Supervisor process group ${pid} did not exit within ${timeoutMs}ms`);
43
+ }
44
+ await new Promise((resolve) => setTimeout(resolve, PROCESS_GROUP_POLL_MS));
45
+ }
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, supervisorExited, hasSupervisorExited, signalTree) {
71
+ const waitUntilStopped = () => platform === "win32"
72
+ ? waitForExit(supervisorExited.then(() => ({ exitCode: 0 })), timeoutMs, pid)
73
+ : waitForProcessGroupExit(pid, timeoutMs);
74
+ if (platform === "win32" && hasSupervisorExited())
75
+ return;
76
+ let termError;
77
+ try {
78
+ await signalOwnedTreeIfPresent(pid, "SIGTERM", platform, signalTree);
79
+ await waitUntilStopped();
80
+ return;
81
+ }
82
+ catch (error) {
83
+ if (platform === "win32" && hasSupervisorExited())
84
+ return;
85
+ termError = error;
86
+ }
87
+ try {
88
+ await signalOwnedTreeIfPresent(pid, "SIGKILL", platform, signalTree);
89
+ await waitUntilStopped();
90
+ }
91
+ catch (killError) {
92
+ if (platform === "win32" && hasSupervisorExited())
93
+ return;
94
+ throw new AggregateError([termError, killError], "Supervisor process-tree termination failed");
95
+ }
96
+ }
97
+ async function confirmOrTerminateOwnedTree(pid, platform, timeoutMs, supervisorExited, hasSupervisorExited, signalTree) {
98
+ if (platform !== "win32" && !(await processGroupExists(pid)))
99
+ return;
100
+ await terminateAndConfirmOwnedTree(pid, platform, timeoutMs, supervisorExited, hasSupervisorExited, signalTree);
101
+ }
102
+ async function withTimeout(promise, timeoutMs, phase) {
103
+ let timer;
104
+ try {
105
+ return await Promise.race([
106
+ promise,
107
+ new Promise((_resolve, reject) => {
108
+ timer = setTimeout(() => reject(new Error(`Supervisor ${phase} timed out after ${timeoutMs}ms`)), timeoutMs);
109
+ }),
110
+ ]);
111
+ }
112
+ finally {
113
+ if (timer !== undefined)
114
+ clearTimeout(timer);
115
+ }
116
+ }
117
+ /** Terminate only a supervisor-created process tree. Never use this for arbitrary child PIDs. */
118
+ export async function signalSupervisorTree(pid, signal, platform = process.platform) {
119
+ if (platform === "win32") {
120
+ const { execFile } = await import("node:child_process");
121
+ await new Promise((resolve, reject) => {
122
+ const args = ["/PID", String(pid), "/T", ...(signal === "SIGKILL" ? ["/F"] : [])];
123
+ const abort = new AbortController();
124
+ const timeout = setTimeout(() => abort.abort(), DEFAULT_TASKKILL_TIMEOUT_MS);
125
+ execFile("taskkill.exe", args, { signal: abort.signal }, (error, stdout, stderr) => {
126
+ clearTimeout(timeout);
127
+ if (error === null) {
128
+ resolve();
129
+ return;
130
+ }
131
+ const detail = `${stdout} ${stderr} ${error.message}`;
132
+ if (/not found|no running instance|不存在|找不到/i.test(detail))
133
+ resolve();
134
+ else
135
+ reject(error);
136
+ });
137
+ });
138
+ return;
139
+ }
140
+ process.kill(-pid, signal);
141
+ }
142
+ export async function startDormantSupervisor(launch, options = {}) {
143
+ const platform = options.platform ?? process.platform;
144
+ const ownershipMode = options.ownershipMode ?? "durable";
145
+ if (ownershipMode === "durable") {
146
+ const backend = executionBackendCapability(platform);
147
+ if (!backend.supported)
148
+ throw new Error(backend.reason);
149
+ }
150
+ // 仅 durable + win32 + Job Object 后端已选中才让 child 套 Job;legacy 恒 false(见 ownsRuntimeViaJobObject)。
151
+ const useJobObject = ownsRuntimeViaJobObject(ownershipMode, platform);
152
+ const signalTree = options.signalTree ?? signalSupervisorTree;
153
+ const childEntry = options.childEntry
154
+ ?? fileURLToPath(new URL("./execution-supervisor-child.js", import.meta.url));
155
+ const abortTimeoutMs = options.abortTimeoutMs ?? DEFAULT_ABORT_TIMEOUT_MS;
156
+ const handshakeTimeoutMs = options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;
157
+ if (!Number.isFinite(handshakeTimeoutMs) || handshakeTimeoutMs <= 0) {
158
+ throw new RangeError("handshakeTimeoutMs must be a positive finite number");
159
+ }
160
+ const env = Object.fromEntries(Object.entries(launch.env).filter((entry) => entry[1] !== undefined));
161
+ const child = fork(childEntry, [], {
162
+ detached: true,
163
+ env: { ...process.env, ...options.childEnv },
164
+ execArgv: options.execArgv === undefined ? process.execArgv : [...options.execArgv],
165
+ stdio: ["ignore", "pipe", "pipe", "ipc"],
166
+ });
167
+ const pid = child.pid;
168
+ if (pid === undefined || child.stdout === null || child.stderr === null) {
169
+ child.kill("SIGKILL");
170
+ throw new Error("Supervisor failed to expose a process identity and output pipes");
171
+ }
172
+ let runtimeResult;
173
+ let supervisorSpawnError;
174
+ const supervisorExit = new Promise((resolve) => {
175
+ child.once("error", (error) => { supervisorSpawnError = error.message; });
176
+ child.once("close", (code, signal) => resolve(runtimeResult ?? {
177
+ exitCode: supervisorSpawnError === undefined ? (code ?? 128) : -1,
178
+ ...(supervisorSpawnError === undefined ? {} : { spawnError: supervisorSpawnError }),
179
+ ...(supervisorSpawnError === undefined && signal !== null ? { terminationSignal: signal } : {}),
180
+ }));
181
+ });
182
+ const supervisorClosed = new Promise((resolve) => child.once("close", () => resolve()));
183
+ // Windows PID ownership ends at `exit`; `close` may lag while stdio/IPC handles drain.
184
+ let didSupervisorExit = false;
185
+ const supervisorExited = new Promise((resolve) => {
186
+ child.once("exit", () => {
187
+ didSupervisorExit = true;
188
+ resolve();
189
+ });
190
+ });
191
+ let treeStopPromise = null;
192
+ const ensureTreeStopped = () => {
193
+ treeStopPromise ??= confirmOrTerminateOwnedTree(pid, platform, abortTimeoutMs, supervisorExited, () => didSupervisorExit, signalTree);
194
+ return treeStopPromise;
195
+ };
196
+ const exit = supervisorExit.then(async (result) => {
197
+ await ensureTreeStopped();
198
+ return result;
199
+ });
200
+ let readyResolve;
201
+ let readyReject;
202
+ const ready = new Promise((resolve, reject) => {
203
+ readyResolve = resolve;
204
+ readyReject = reject;
205
+ });
206
+ let releaseResolve;
207
+ let releaseReject;
208
+ let released = false;
209
+ child.on("message", (raw) => {
210
+ if (raw.type === "ready")
211
+ readyResolve?.();
212
+ if (raw.type === "runtime-started")
213
+ releaseResolve?.();
214
+ if (raw.type === "runtime-spawn-error") {
215
+ runtimeResult ??= { exitCode: -1, spawnError: raw.message };
216
+ releaseReject?.(new Error(raw.message));
217
+ }
218
+ if (raw.type === "runtime-exit") {
219
+ runtimeResult ??= {
220
+ exitCode: raw.exitCode,
221
+ ...(raw.terminationSignal === undefined ? {} : { terminationSignal: raw.terminationSignal }),
222
+ };
223
+ }
224
+ });
225
+ child.once("error", (error) => {
226
+ readyReject?.(error);
227
+ releaseReject?.(error);
228
+ });
229
+ child.once("close", (code, signal) => {
230
+ const error = new Error(`Supervisor exited before launch: code=${code} signal=${signal}`);
231
+ readyReject?.(error);
232
+ releaseReject?.(error);
233
+ });
234
+ const abort = async () => {
235
+ if (child.exitCode !== null || child.signalCode !== null) {
236
+ await supervisorClosed;
237
+ await ensureTreeStopped();
238
+ return;
239
+ }
240
+ await ensureTreeStopped();
241
+ await waitForExit(supervisorClosed.then(() => ({ exitCode: 0 })), abortTimeoutMs, pid);
242
+ await ensureTreeStopped();
243
+ };
244
+ const cancel = async () => {
245
+ if (child.exitCode !== null || child.signalCode !== null) {
246
+ await supervisorClosed;
247
+ await ensureTreeStopped();
248
+ return;
249
+ }
250
+ try {
251
+ await new Promise((resolve, reject) => {
252
+ child.send({ type: "abort" }, (error) => error === null ? resolve() : reject(error));
253
+ });
254
+ if (platform === "win32") {
255
+ await ensureTreeStopped();
256
+ }
257
+ else {
258
+ await waitForExit(supervisorClosed.then(() => ({ exitCode: 0 })), abortTimeoutMs, pid);
259
+ await ensureTreeStopped();
260
+ }
261
+ }
262
+ catch {
263
+ await abort();
264
+ }
265
+ };
266
+ try {
267
+ await new Promise((resolve, reject) => {
268
+ child.send({ type: "launch", launch: { ...launch, args: [...launch.args], env }, useJobObject }, (error) => {
269
+ if (error === null)
270
+ resolve();
271
+ else
272
+ reject(error);
273
+ });
274
+ });
275
+ await withTimeout(ready, handshakeTimeoutMs, "ready handshake");
276
+ }
277
+ catch (error) {
278
+ await abort().catch((abortError) => {
279
+ throw new AggregateError([messageError(error), messageError(abortError)], "Supervisor start failed");
280
+ });
281
+ throw error;
282
+ }
283
+ const handle = {
284
+ pid,
285
+ stdout: child.stdout,
286
+ stderr: child.stderr,
287
+ exit,
288
+ release: async () => {
289
+ if (released)
290
+ return;
291
+ released = true;
292
+ const acknowledgement = new Promise((resolve, reject) => {
293
+ releaseResolve = resolve;
294
+ releaseReject = reject;
295
+ child.send({ type: "release" }, (error) => {
296
+ if (error !== null)
297
+ reject(error);
298
+ });
299
+ });
300
+ try {
301
+ await withTimeout(acknowledgement, handshakeTimeoutMs, "release handshake");
302
+ }
303
+ catch (error) {
304
+ await abort().catch((abortError) => {
305
+ throw new AggregateError([messageError(error), messageError(abortError)], "Supervisor release failed");
306
+ });
307
+ throw error;
308
+ }
309
+ },
310
+ abort,
311
+ cancel,
312
+ };
313
+ return ownershipMode === "durable"
314
+ ? { ...handle, ownershipMode, parentExitGuard: "pipe-eof" }
315
+ : { ...handle, ownershipMode };
316
+ }
@@ -0,0 +1,71 @@
1
+ import { access, open, mkdir, readdir, readFile, rename, unlink } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import { DaemonToServerExecutionFrameSchema, } from "./execution-protocol.js";
5
+ const MAX_PENDING_FILES = 4_096;
6
+ const fileName = (frame) => `${frame.executionId}.${frame.type === "execution:activity" ? "activity" : "console"}.${frame.seq}.json`;
7
+ export class ExecutionTelemetryJournal {
8
+ directory;
9
+ constructor(agentsRoot) {
10
+ this.directory = join(agentsRoot, ".execution-telemetry");
11
+ }
12
+ async append(frame) {
13
+ await mkdir(this.directory, { recursive: true, mode: 0o700 });
14
+ const path = join(this.directory, fileName(frame));
15
+ if (await access(path).then(() => true, () => false))
16
+ return;
17
+ const pending = await readdir(this.directory);
18
+ if (pending.length >= MAX_PENDING_FILES) {
19
+ throw new Error(`execution telemetry journal is full (${MAX_PENDING_FILES} frames)`);
20
+ }
21
+ const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
22
+ let handle;
23
+ try {
24
+ handle = await open(tempPath, "wx", 0o600);
25
+ await handle.writeFile(JSON.stringify(frame), "utf8");
26
+ await handle.sync();
27
+ await handle.close();
28
+ handle = undefined;
29
+ await rename(tempPath, path);
30
+ }
31
+ catch (error) {
32
+ await unlink(tempPath).catch(() => { });
33
+ if (error.code !== "EEXIST")
34
+ throw error;
35
+ }
36
+ finally {
37
+ await handle?.close();
38
+ }
39
+ }
40
+ async acknowledge(executionId, kind, seq) {
41
+ await unlink(join(this.directory, `${executionId}.${kind}.${seq}.json`)).catch((error) => {
42
+ if (error.code !== "ENOENT")
43
+ throw error;
44
+ });
45
+ }
46
+ async replay() {
47
+ const names = await readdir(this.directory).catch((error) => {
48
+ if (error.code === "ENOENT")
49
+ return [];
50
+ throw error;
51
+ });
52
+ const frames = [];
53
+ for (const name of names.filter((entry) => entry.endsWith(".json")).sort()) {
54
+ let raw;
55
+ try {
56
+ raw = JSON.parse(await readFile(join(this.directory, name), "utf8"));
57
+ }
58
+ catch (error) {
59
+ throw new Error(`invalid execution telemetry journal entry: ${name}`, { cause: error });
60
+ }
61
+ const parsed = DaemonToServerExecutionFrameSchema.safeParse(raw);
62
+ if (!parsed.success || (parsed.data.type !== "execution:activity" && parsed.data.type !== "execution:console")) {
63
+ throw new Error(`invalid execution telemetry journal entry: ${name}`);
64
+ }
65
+ frames.push(parsed.data);
66
+ }
67
+ return frames.sort((left, right) => left.at.localeCompare(right.at)
68
+ || left.type.localeCompare(right.type) || left.seq - right.seq);
69
+ }
70
+ }
71
+ export const createExecutionTelemetryJournal = (agentsRoot) => new ExecutionTelemetryJournal(agentsRoot);