@nowcrew/daemon 0.5.11 → 0.5.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -0
- package/dist/config.js +38 -0
- package/dist/execution-event-limit.js +59 -0
- package/dist/execution-journal-lock.js +262 -0
- package/dist/execution-journal.js +678 -0
- package/dist/execution-protocol.js +310 -0
- package/dist/execution-runner.js +637 -0
- package/dist/execution-supervisor-child.js +185 -0
- package/dist/execution-supervisor.js +209 -0
- package/dist/local-executor.js +326 -0
- package/dist/machine-info.js +13 -4
- package/dist/origin-decision.js +42 -0
- package/dist/prompt.js +23 -1
- package/dist/runner.js +146 -340
- package/dist/runtimes/claude.js +9 -1
- package/dist/runtimes/codex.js +21 -5
- package/dist/runtimes/kimi.js +3 -0
- package/dist/scheduled-run-report.js +15 -7
- package/dist/serve.js +412 -57
- package/dist/token.js +5 -2
- package/dist/workspace.js +39 -11
- package/package.json +3 -2
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import spawn from "cross-spawn";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
const LaunchSchema = z.object({
|
|
5
|
+
command: z.string().min(1),
|
|
6
|
+
args: z.array(z.string()),
|
|
7
|
+
cwd: z.string().min(1),
|
|
8
|
+
env: z.record(z.string()),
|
|
9
|
+
stdinText: z.string().optional(),
|
|
10
|
+
}).strict();
|
|
11
|
+
function send(message, callback) {
|
|
12
|
+
if (!process.connected) {
|
|
13
|
+
callback?.();
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
process.send?.(message, () => callback?.());
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Keep consuming runtime output even when the daemon-side pipe disappears. A direct `pipe()` can
|
|
20
|
+
* crash this supervisor with EPIPE and orphan the runtime. While connected, source pause/resume
|
|
21
|
+
* preserves authoritative final output. Once disconnected, the source remains drained and discarded.
|
|
22
|
+
*/
|
|
23
|
+
function forwardWhileWritable(source, destination) {
|
|
24
|
+
if (source === null)
|
|
25
|
+
return { discard: () => undefined };
|
|
26
|
+
let blocked = false;
|
|
27
|
+
let broken = destination.destroyed;
|
|
28
|
+
const discard = () => {
|
|
29
|
+
broken = true;
|
|
30
|
+
blocked = false;
|
|
31
|
+
source.resume();
|
|
32
|
+
};
|
|
33
|
+
destination.on("error", discard);
|
|
34
|
+
destination.on("close", discard);
|
|
35
|
+
destination.on("drain", () => {
|
|
36
|
+
if (broken)
|
|
37
|
+
return;
|
|
38
|
+
blocked = false;
|
|
39
|
+
source.resume();
|
|
40
|
+
});
|
|
41
|
+
source.on("data", (chunk) => {
|
|
42
|
+
if (broken)
|
|
43
|
+
return;
|
|
44
|
+
try {
|
|
45
|
+
if (!destination.write(chunk)) {
|
|
46
|
+
blocked = true;
|
|
47
|
+
source.pause();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
discard();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
source.on("error", () => undefined);
|
|
55
|
+
return { discard };
|
|
56
|
+
}
|
|
57
|
+
export function runExecutionSupervisorChild() {
|
|
58
|
+
let launch = null;
|
|
59
|
+
let released = false;
|
|
60
|
+
let runtime = null;
|
|
61
|
+
let settled = false;
|
|
62
|
+
let cleaningTree = false;
|
|
63
|
+
const outputForwarders = [];
|
|
64
|
+
const terminateOwnedTree = () => {
|
|
65
|
+
if (cleaningTree)
|
|
66
|
+
return;
|
|
67
|
+
cleaningTree = true;
|
|
68
|
+
for (const forwarder of outputForwarders)
|
|
69
|
+
forwarder.discard();
|
|
70
|
+
if (process.platform === "win32") {
|
|
71
|
+
process.exit(1);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
process.kill(-process.pid, "SIGTERM");
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error.code !== "ESRCH")
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
setTimeout(() => {
|
|
82
|
+
try {
|
|
83
|
+
process.kill(-process.pid, "SIGKILL");
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error.code !== "ESRCH")
|
|
87
|
+
throw error;
|
|
88
|
+
process.exit();
|
|
89
|
+
}
|
|
90
|
+
}, 250);
|
|
91
|
+
};
|
|
92
|
+
const stopBeforeRelease = () => {
|
|
93
|
+
if (released || settled)
|
|
94
|
+
return;
|
|
95
|
+
settled = true;
|
|
96
|
+
process.exit(0);
|
|
97
|
+
};
|
|
98
|
+
process.on("SIGTERM", () => {
|
|
99
|
+
if (released)
|
|
100
|
+
terminateOwnedTree();
|
|
101
|
+
else
|
|
102
|
+
stopBeforeRelease();
|
|
103
|
+
});
|
|
104
|
+
process.on("SIGINT", () => {
|
|
105
|
+
if (released)
|
|
106
|
+
terminateOwnedTree();
|
|
107
|
+
else
|
|
108
|
+
stopBeforeRelease();
|
|
109
|
+
});
|
|
110
|
+
process.on("disconnect", () => {
|
|
111
|
+
if (released)
|
|
112
|
+
terminateOwnedTree();
|
|
113
|
+
else
|
|
114
|
+
stopBeforeRelease();
|
|
115
|
+
});
|
|
116
|
+
process.on("message", (raw) => {
|
|
117
|
+
if (raw.type === "launch") {
|
|
118
|
+
if (launch !== null) {
|
|
119
|
+
process.exitCode = 2;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const parsed = LaunchSchema.safeParse(raw.launch);
|
|
123
|
+
if (!parsed.success) {
|
|
124
|
+
send({ type: "runtime-spawn-error", message: parsed.error.message });
|
|
125
|
+
process.exit(2);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
launch = parsed.data;
|
|
129
|
+
send({ type: "ready" });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (raw.type === "abort") {
|
|
133
|
+
if (runtime === null)
|
|
134
|
+
stopBeforeRelease();
|
|
135
|
+
else
|
|
136
|
+
runtime.kill("SIGTERM");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (raw.type !== "release" || released || launch === null)
|
|
140
|
+
return;
|
|
141
|
+
released = true;
|
|
142
|
+
const child = spawn(launch.command, launch.args, {
|
|
143
|
+
cwd: launch.cwd,
|
|
144
|
+
env: launch.env,
|
|
145
|
+
detached: false,
|
|
146
|
+
stdio: [launch.stdinText === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
147
|
+
});
|
|
148
|
+
runtime = child;
|
|
149
|
+
outputForwarders.push(forwardWhileWritable(child.stdout, process.stdout), forwardWhileWritable(child.stderr, process.stderr));
|
|
150
|
+
child.once("spawn", () => {
|
|
151
|
+
send({ type: "runtime-started" });
|
|
152
|
+
if (launch?.stdinText !== undefined && child.stdin !== null) {
|
|
153
|
+
child.stdin.on("error", () => undefined);
|
|
154
|
+
child.stdin.end(launch.stdinText);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
child.once("error", (error) => {
|
|
158
|
+
send({ type: "runtime-spawn-error", message: error.message });
|
|
159
|
+
});
|
|
160
|
+
child.once("close", (code, signal) => {
|
|
161
|
+
const exitMessage = {
|
|
162
|
+
type: "runtime-exit",
|
|
163
|
+
exitCode: code ?? 128,
|
|
164
|
+
...(signal === null ? {} : { terminationSignal: signal }),
|
|
165
|
+
};
|
|
166
|
+
settled = true;
|
|
167
|
+
process.exitCode = code ?? 128;
|
|
168
|
+
let cleanupStarted = false;
|
|
169
|
+
const cleanup = () => {
|
|
170
|
+
if (cleanupStarted)
|
|
171
|
+
return;
|
|
172
|
+
cleanupStarted = true;
|
|
173
|
+
terminateOwnedTree();
|
|
174
|
+
};
|
|
175
|
+
const fallback = setTimeout(cleanup, 50);
|
|
176
|
+
send(exitMessage, () => {
|
|
177
|
+
clearTimeout(fallback);
|
|
178
|
+
cleanup();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
184
|
+
runExecutionSupervisorChild();
|
|
185
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { fork } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
const DEFAULT_ABORT_TIMEOUT_MS = 5_000;
|
|
4
|
+
const DEFAULT_HANDSHAKE_TIMEOUT_MS = 5_000;
|
|
5
|
+
const DEFAULT_TASKKILL_TIMEOUT_MS = 5_000;
|
|
6
|
+
function messageError(error) {
|
|
7
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
8
|
+
}
|
|
9
|
+
async function waitForExit(exit, timeoutMs, pid) {
|
|
10
|
+
let timer;
|
|
11
|
+
try {
|
|
12
|
+
await Promise.race([
|
|
13
|
+
exit.then(() => undefined),
|
|
14
|
+
new Promise((_resolve, reject) => {
|
|
15
|
+
timer = setTimeout(() => reject(new Error(`Supervisor ${pid} did not exit within ${timeoutMs}ms`)), timeoutMs);
|
|
16
|
+
}),
|
|
17
|
+
]);
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
if (timer !== undefined)
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function withTimeout(promise, timeoutMs, phase) {
|
|
25
|
+
let timer;
|
|
26
|
+
try {
|
|
27
|
+
return await Promise.race([
|
|
28
|
+
promise,
|
|
29
|
+
new Promise((_resolve, reject) => {
|
|
30
|
+
timer = setTimeout(() => reject(new Error(`Supervisor ${phase} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
31
|
+
}),
|
|
32
|
+
]);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
if (timer !== undefined)
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Terminate only a supervisor-created process tree. Never use this for arbitrary child PIDs. */
|
|
40
|
+
export async function signalSupervisorTree(pid, signal, platform = process.platform) {
|
|
41
|
+
if (platform === "win32") {
|
|
42
|
+
const { execFile } = await import("node:child_process");
|
|
43
|
+
await new Promise((resolve, reject) => {
|
|
44
|
+
const args = ["/PID", String(pid), "/T", ...(signal === "SIGKILL" ? ["/F"] : [])];
|
|
45
|
+
const abort = new AbortController();
|
|
46
|
+
const timeout = setTimeout(() => abort.abort(), DEFAULT_TASKKILL_TIMEOUT_MS);
|
|
47
|
+
execFile("taskkill.exe", args, { signal: abort.signal }, (error, stdout, stderr) => {
|
|
48
|
+
clearTimeout(timeout);
|
|
49
|
+
if (error === null) {
|
|
50
|
+
resolve();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const detail = `${stdout} ${stderr} ${error.message}`;
|
|
54
|
+
if (/not found|no running instance|不存在|找不到/i.test(detail))
|
|
55
|
+
resolve();
|
|
56
|
+
else
|
|
57
|
+
reject(error);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
process.kill(-pid, signal);
|
|
63
|
+
}
|
|
64
|
+
export async function startDormantSupervisor(launch, options = {}) {
|
|
65
|
+
const platform = options.platform ?? process.platform;
|
|
66
|
+
if (platform === "win32") {
|
|
67
|
+
throw new Error("Execution supervisor is unsupported on Windows without Job Object ownership");
|
|
68
|
+
}
|
|
69
|
+
const childEntry = options.childEntry
|
|
70
|
+
?? fileURLToPath(new URL("./execution-supervisor-child.js", import.meta.url));
|
|
71
|
+
const abortTimeoutMs = options.abortTimeoutMs ?? DEFAULT_ABORT_TIMEOUT_MS;
|
|
72
|
+
const handshakeTimeoutMs = options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;
|
|
73
|
+
if (!Number.isFinite(handshakeTimeoutMs) || handshakeTimeoutMs <= 0) {
|
|
74
|
+
throw new RangeError("handshakeTimeoutMs must be a positive finite number");
|
|
75
|
+
}
|
|
76
|
+
const env = Object.fromEntries(Object.entries(launch.env).filter((entry) => entry[1] !== undefined));
|
|
77
|
+
const child = fork(childEntry, [], {
|
|
78
|
+
detached: true,
|
|
79
|
+
env: { ...process.env, ...options.childEnv },
|
|
80
|
+
execArgv: options.execArgv === undefined ? process.execArgv : [...options.execArgv],
|
|
81
|
+
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
82
|
+
});
|
|
83
|
+
const pid = child.pid;
|
|
84
|
+
if (pid === undefined || child.stdout === null || child.stderr === null) {
|
|
85
|
+
child.kill("SIGKILL");
|
|
86
|
+
throw new Error("Supervisor failed to expose a process identity and output pipes");
|
|
87
|
+
}
|
|
88
|
+
let runtimeResult;
|
|
89
|
+
let supervisorSpawnError;
|
|
90
|
+
const exit = new Promise((resolve) => {
|
|
91
|
+
child.once("error", (error) => { supervisorSpawnError = error.message; });
|
|
92
|
+
child.once("close", (code, signal) => resolve(runtimeResult ?? {
|
|
93
|
+
exitCode: supervisorSpawnError === undefined ? (code ?? 128) : -1,
|
|
94
|
+
...(supervisorSpawnError === undefined ? {} : { spawnError: supervisorSpawnError }),
|
|
95
|
+
...(supervisorSpawnError === undefined && signal !== null ? { terminationSignal: signal } : {}),
|
|
96
|
+
}));
|
|
97
|
+
});
|
|
98
|
+
const supervisorClosed = new Promise((resolve) => child.once("close", () => resolve()));
|
|
99
|
+
let readyResolve;
|
|
100
|
+
let readyReject;
|
|
101
|
+
const ready = new Promise((resolve, reject) => {
|
|
102
|
+
readyResolve = resolve;
|
|
103
|
+
readyReject = reject;
|
|
104
|
+
});
|
|
105
|
+
let releaseResolve;
|
|
106
|
+
let releaseReject;
|
|
107
|
+
let released = false;
|
|
108
|
+
child.on("message", (raw) => {
|
|
109
|
+
if (raw.type === "ready")
|
|
110
|
+
readyResolve?.();
|
|
111
|
+
if (raw.type === "runtime-started")
|
|
112
|
+
releaseResolve?.();
|
|
113
|
+
if (raw.type === "runtime-spawn-error") {
|
|
114
|
+
runtimeResult ??= { exitCode: -1, spawnError: raw.message };
|
|
115
|
+
releaseReject?.(new Error(raw.message));
|
|
116
|
+
}
|
|
117
|
+
if (raw.type === "runtime-exit") {
|
|
118
|
+
runtimeResult ??= {
|
|
119
|
+
exitCode: raw.exitCode,
|
|
120
|
+
...(raw.terminationSignal === undefined ? {} : { terminationSignal: raw.terminationSignal }),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
child.once("error", (error) => {
|
|
125
|
+
readyReject?.(error);
|
|
126
|
+
releaseReject?.(error);
|
|
127
|
+
});
|
|
128
|
+
child.once("close", (code, signal) => {
|
|
129
|
+
const error = new Error(`Supervisor exited before launch: code=${code} signal=${signal}`);
|
|
130
|
+
readyReject?.(error);
|
|
131
|
+
releaseReject?.(error);
|
|
132
|
+
});
|
|
133
|
+
const abort = async () => {
|
|
134
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
135
|
+
await supervisorClosed;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
await signalSupervisorTree(pid, "SIGTERM", platform);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
const code = error instanceof Error && "code" in error ? error.code : undefined;
|
|
143
|
+
if (code !== "ESRCH")
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
await waitForExit(supervisorClosed.then(() => ({ exitCode: 0 })), abortTimeoutMs, pid);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
try {
|
|
151
|
+
await signalSupervisorTree(pid, "SIGKILL", platform);
|
|
152
|
+
}
|
|
153
|
+
catch (killError) {
|
|
154
|
+
const code = killError instanceof Error && "code" in killError ? killError.code : undefined;
|
|
155
|
+
if (code !== "ESRCH")
|
|
156
|
+
throw new AggregateError([error, killError], "Supervisor abort failed");
|
|
157
|
+
}
|
|
158
|
+
await waitForExit(supervisorClosed.then(() => ({ exitCode: 0 })), abortTimeoutMs, pid);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
try {
|
|
162
|
+
await new Promise((resolve, reject) => {
|
|
163
|
+
child.send({ type: "launch", launch: { ...launch, args: [...launch.args], env } }, (error) => {
|
|
164
|
+
if (error === null)
|
|
165
|
+
resolve();
|
|
166
|
+
else
|
|
167
|
+
reject(error);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
await withTimeout(ready, handshakeTimeoutMs, "ready handshake");
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
await abort().catch((abortError) => {
|
|
174
|
+
throw new AggregateError([messageError(error), messageError(abortError)], "Supervisor start failed");
|
|
175
|
+
});
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
pid,
|
|
180
|
+
parentExitGuard: "pipe-eof",
|
|
181
|
+
stdout: child.stdout,
|
|
182
|
+
stderr: child.stderr,
|
|
183
|
+
exit,
|
|
184
|
+
release: async () => {
|
|
185
|
+
if (released)
|
|
186
|
+
return;
|
|
187
|
+
released = true;
|
|
188
|
+
const acknowledgement = new Promise((resolve, reject) => {
|
|
189
|
+
releaseResolve = resolve;
|
|
190
|
+
releaseReject = reject;
|
|
191
|
+
child.send({ type: "release" }, (error) => {
|
|
192
|
+
if (error !== null)
|
|
193
|
+
reject(error);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
try {
|
|
197
|
+
await withTimeout(acknowledgement, handshakeTimeoutMs, "release handshake");
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
await abort().catch((abortError) => {
|
|
201
|
+
throw new AggregateError([messageError(error), messageError(abortError)], "Supervisor release failed");
|
|
202
|
+
});
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
abort,
|
|
207
|
+
cancel: abort,
|
|
208
|
+
};
|
|
209
|
+
}
|