@nowcrew/daemon 0.5.19 → 0.5.21
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/dist/attachments.js +47 -12
- package/dist/computer-cli.js +72 -12
- package/dist/computer-profile-lock.js +395 -0
- package/dist/computer-profile.js +189 -20
- package/dist/config.js +2 -1
- package/dist/console.js +175 -9
- package/dist/daemon-startup-error.js +30 -0
- package/dist/execution-backend.js +35 -2
- package/dist/execution-journal-lock.js +199 -40
- package/dist/execution-journal.js +42 -4
- package/dist/execution-protocol.js +2 -0
- package/dist/execution-recovery.js +95 -0
- package/dist/execution-runner.js +49 -91
- package/dist/execution-supervisor-child.js +51 -0
- package/dist/execution-supervisor.js +83 -33
- package/dist/external-output.js +28 -0
- package/dist/i18n.js +7 -5
- package/dist/local-executor.js +66 -15
- package/dist/machine-info.js +2 -1
- package/dist/main.js +28 -8
- package/dist/runner.js +11 -6
- package/dist/runtime-cancellation.js +74 -0
- package/dist/runtime-path.js +8 -4
- package/dist/runtimes/claude.js +8 -4
- package/dist/runtimes/codex.js +8 -4
- package/dist/serve-lifecycle.js +82 -0
- package/dist/serve.js +189 -220
- package/dist/shared-execution-slots.js +68 -0
- package/dist/shutdown-deadline.js +32 -0
- package/dist/slog.js +34 -20
- package/dist/supervised-runtime.js +104 -0
- package/dist/websocket-shutdown.js +53 -0
- package/dist/win32-job-object.js +193 -0
- package/package.json +5 -2
package/dist/execution-runner.js
CHANGED
|
@@ -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 {
|
|
9
|
+
import { CLAUDE_EFFORT_LEVELS } from "./runtimes/claude.js";
|
|
11
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,70 +185,6 @@ function launchProviderConfig(config) {
|
|
|
183
185
|
...(config.description === undefined ? {} : { description: config.description }),
|
|
184
186
|
};
|
|
185
187
|
}
|
|
186
|
-
export 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: process.execPath,
|
|
215
|
-
args: [
|
|
216
|
-
fileURLToPath(new URL("./runtimes/codex-app-server-runner.js", import.meta.url)),
|
|
217
|
-
"--bin", request.bin,
|
|
218
|
-
],
|
|
219
|
-
cwd: request.cwd,
|
|
220
|
-
env: request.env,
|
|
221
|
-
stdinText: JSON.stringify({
|
|
222
|
-
systemPrompt: request.systemPrompt,
|
|
223
|
-
wakePrompt: request.wakePrompt,
|
|
224
|
-
effectivePermission: request.effectivePermission,
|
|
225
|
-
...(request.model === undefined ? {} : { model: request.model }),
|
|
226
|
-
...(request.reasoning === undefined ? {} : { reasoning: request.reasoning }),
|
|
227
|
-
...(request.sessionId === undefined ? {} : { sessionId: request.sessionId }),
|
|
228
|
-
...(request.imagePaths === undefined ? {} : { imagePaths: request.imagePaths }),
|
|
229
|
-
resume: request.resume,
|
|
230
|
-
}),
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
if (request.effectivePermission !== "full_access") {
|
|
234
|
-
throw new Error(`Kimi ACP cannot enforce ${request.effectivePermission} permission`);
|
|
235
|
-
}
|
|
236
|
-
return {
|
|
237
|
-
command: process.execPath,
|
|
238
|
-
args: [
|
|
239
|
-
fileURLToPath(new URL("./runtimes/kimi-acp-runner.js", import.meta.url)),
|
|
240
|
-
"--bin", request.bin,
|
|
241
|
-
...(request.model === undefined ? [] : ["--model", request.model]),
|
|
242
|
-
...(request.sessionId === undefined ? [] : ["--session", request.sessionId]),
|
|
243
|
-
...(request.resume ? ["--resume"] : []),
|
|
244
|
-
],
|
|
245
|
-
cwd: request.cwd,
|
|
246
|
-
env: request.env,
|
|
247
|
-
stdinText: `${request.systemPrompt}\n\n${request.wakePrompt}`,
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
188
|
function rejection(executionId, reason, message, at) {
|
|
251
189
|
return ExecutionRejectedSchema.parse({
|
|
252
190
|
type: "execution:rejected",
|
|
@@ -494,7 +432,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
494
432
|
catch { /* best-effort console omitted when its envelope cannot fit */ }
|
|
495
433
|
},
|
|
496
434
|
} : {}),
|
|
497
|
-
...(spec.context.externalResponseSessionId ? {
|
|
435
|
+
...(spec.context.externalResponseSessionId || spec.context.answerStream ? {
|
|
498
436
|
onExternalOutput: (text) => {
|
|
499
437
|
const frame = DaemonToServerExecutionFrameSchema.parse({
|
|
500
438
|
type: "execution:output",
|
|
@@ -513,6 +451,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
513
451
|
} : {}),
|
|
514
452
|
};
|
|
515
453
|
const localDependencies = {
|
|
454
|
+
...(dependencies.cancellation === undefined ? {} : { cancellation: dependencies.cancellation }),
|
|
516
455
|
launchRuntime: async (request) => {
|
|
517
456
|
if (launchClosed || dependencies.cancellation?.isRequested())
|
|
518
457
|
throw new ExecutionCancelledError();
|
|
@@ -521,27 +460,42 @@ export async function runExecution(config, input, dependencies) {
|
|
|
521
460
|
launchAttempts.add(launchSettled);
|
|
522
461
|
try {
|
|
523
462
|
const processStartedAt = now().toISOString();
|
|
524
|
-
const
|
|
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
|
+
});
|
|
525
492
|
if (guarded.kind !== "started") {
|
|
526
493
|
throw new Error(`Execution became ${guarded.entry.state} before local launch`);
|
|
527
494
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
try {
|
|
533
|
-
stopPromise = Promise.resolve(operation());
|
|
534
|
-
}
|
|
535
|
-
catch (error) {
|
|
536
|
-
stopPromise = Promise.reject(error);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
return stopPromise;
|
|
540
|
-
};
|
|
541
|
-
const cancelOnce = () => stopOnce(guarded.handle.cancel);
|
|
542
|
-
supervisorState.abortOnce = () => stopOnce(guarded.handle.abort);
|
|
543
|
-
dependencies.cancellation?.register(cancelOnce);
|
|
544
|
-
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;
|
|
545
499
|
await reportBestEffort(dependencies.report, boundExecutionFrame(DaemonToServerExecutionFrameSchema.parse({
|
|
546
500
|
type: "execution:started",
|
|
547
501
|
protocolVersion: 1,
|
|
@@ -552,14 +506,14 @@ export async function runExecution(config, input, dependencies) {
|
|
|
552
506
|
timeout = setTimeout(() => {
|
|
553
507
|
timedOut = true;
|
|
554
508
|
try {
|
|
555
|
-
void
|
|
509
|
+
void installedCancel().catch(rejectCancellationFailure);
|
|
556
510
|
}
|
|
557
511
|
catch (error) {
|
|
558
512
|
rejectCancellationFailure(error);
|
|
559
513
|
}
|
|
560
514
|
}, effectiveTimeoutMs);
|
|
561
515
|
}
|
|
562
|
-
return guarded.handle;
|
|
516
|
+
return { ...guarded.handle, cancel: installedCancel };
|
|
563
517
|
}
|
|
564
518
|
finally {
|
|
565
519
|
launchAttempts.delete(launchSettled);
|
|
@@ -608,10 +562,10 @@ export async function runExecution(config, input, dependencies) {
|
|
|
608
562
|
maxTurns: config.sessionMaxTurns,
|
|
609
563
|
},
|
|
610
564
|
};
|
|
611
|
-
const result = await
|
|
565
|
+
const result = await Promise.race([
|
|
612
566
|
execute(localInput, callbacks, localDependencies),
|
|
613
567
|
cancellationFailure,
|
|
614
|
-
])
|
|
568
|
+
]);
|
|
615
569
|
if (timeout !== undefined)
|
|
616
570
|
clearTimeout(timeout);
|
|
617
571
|
const finishedAt = now().toISOString();
|
|
@@ -652,6 +606,9 @@ export async function runExecution(config, input, dependencies) {
|
|
|
652
606
|
...(!spec.reporting.captureFinal || result.finalText === null
|
|
653
607
|
? {}
|
|
654
608
|
: { finalText: result.finalText }),
|
|
609
|
+
...(spec.context.answerStream && result.exitCode === 0 && result.externalAnswer
|
|
610
|
+
? { externalAnswer: result.externalAnswer }
|
|
611
|
+
: {}),
|
|
655
612
|
...(result.usage === undefined ? {} : { usage: result.usage }),
|
|
656
613
|
startedAt,
|
|
657
614
|
finishedAt,
|
|
@@ -660,7 +617,8 @@ export async function runExecution(config, input, dependencies) {
|
|
|
660
617
|
catch (error) {
|
|
661
618
|
if (timeout !== undefined)
|
|
662
619
|
clearTimeout(timeout);
|
|
663
|
-
|
|
620
|
+
const cancelled = error instanceof ExecutionCancelledError || error instanceof RuntimeCancelledError;
|
|
621
|
+
if (cancelled) {
|
|
664
622
|
await closeLaunchGate();
|
|
665
623
|
await dependencies.cancellation?.waitForStop();
|
|
666
624
|
}
|
|
@@ -673,7 +631,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
673
631
|
throw new AggregateError([error, abortError], `Failed to stop the execution supervisor: ${detail}`);
|
|
674
632
|
}
|
|
675
633
|
}
|
|
676
|
-
completion =
|
|
634
|
+
completion = cancelled
|
|
677
635
|
? ExecutionCompletedSchema.parse({
|
|
678
636
|
type: "execution:completed",
|
|
679
637
|
protocolVersion: 1,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import spawn from "cross-spawn";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { assignProcessToJob, createKillOnCloseJob, terminateJob, } from "./win32-job-object.js";
|
|
4
5
|
const LaunchSchema = z.object({
|
|
5
6
|
command: z.string().min(1),
|
|
6
7
|
args: z.array(z.string()),
|
|
@@ -8,6 +9,9 @@ const LaunchSchema = z.object({
|
|
|
8
9
|
env: z.record(z.string()),
|
|
9
10
|
stdinText: z.string().optional(),
|
|
10
11
|
}).strict();
|
|
12
|
+
function messageOf(error) {
|
|
13
|
+
return error instanceof Error ? error.message : String(error);
|
|
14
|
+
}
|
|
11
15
|
function send(message, callback) {
|
|
12
16
|
if (!process.connected) {
|
|
13
17
|
callback?.();
|
|
@@ -56,10 +60,17 @@ function forwardWhileWritable(source, destination) {
|
|
|
56
60
|
}
|
|
57
61
|
export function runExecutionSupervisorChild() {
|
|
58
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;
|
|
59
67
|
let released = false;
|
|
60
68
|
let runtime = null;
|
|
61
69
|
let settled = false;
|
|
62
70
|
let cleaningTree = false;
|
|
71
|
+
// win32:本 child 创建并持有的 Job(KILL_ON_JOB_CLOSE)。持有它 = 拥有整棵 runtime 进程树:
|
|
72
|
+
// 本进程一死(含崩溃)内核即回收 Job 内全部进程,等价于 POSIX killpg 且覆盖 supervisor 自身崩溃。
|
|
73
|
+
let jobHandle = null;
|
|
63
74
|
const outputForwarders = [];
|
|
64
75
|
const terminateOwnedTree = () => {
|
|
65
76
|
if (cleaningTree)
|
|
@@ -68,6 +79,15 @@ export function runExecutionSupervisorChild() {
|
|
|
68
79
|
for (const forwarder of outputForwarders)
|
|
69
80
|
forwarder.discard();
|
|
70
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
|
+
}
|
|
71
91
|
process.exit(1);
|
|
72
92
|
return;
|
|
73
93
|
}
|
|
@@ -126,6 +146,7 @@ export function runExecutionSupervisorChild() {
|
|
|
126
146
|
return;
|
|
127
147
|
}
|
|
128
148
|
launch = parsed.data;
|
|
149
|
+
useJobObject = raw.useJobObject === true;
|
|
129
150
|
send({ type: "ready" });
|
|
130
151
|
return;
|
|
131
152
|
}
|
|
@@ -139,6 +160,18 @@ export function runExecutionSupervisorChild() {
|
|
|
139
160
|
if (raw.type !== "release" || released || launch === null)
|
|
140
161
|
return;
|
|
141
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
|
+
}
|
|
142
175
|
const child = spawn(launch.command, launch.args, {
|
|
143
176
|
cwd: launch.cwd,
|
|
144
177
|
env: launch.env,
|
|
@@ -146,6 +179,24 @@ export function runExecutionSupervisorChild() {
|
|
|
146
179
|
stdio: [launch.stdinText === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
147
180
|
});
|
|
148
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
|
+
}
|
|
149
200
|
outputForwarders.push(forwardWhileWritable(child.stdout, process.stdout), forwardWhileWritable(child.stderr, process.stderr));
|
|
150
201
|
child.once("spawn", () => {
|
|
151
202
|
send({ type: "runtime-started" });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fork } from "node:child_process";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { executionBackendCapability } from "./execution-backend.js";
|
|
3
|
+
import { executionBackendCapability, ownsRuntimeViaJobObject } from "./execution-backend.js";
|
|
4
4
|
const DEFAULT_ABORT_TIMEOUT_MS = 5_000;
|
|
5
5
|
const DEFAULT_HANDSHAKE_TIMEOUT_MS = 5_000;
|
|
6
6
|
const DEFAULT_TASKKILL_TIMEOUT_MS = 5_000;
|
|
@@ -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,15 @@ 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
|
|
90
|
-
if (
|
|
91
|
-
|
|
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
|
+
// 仅 durable + win32 + Job Object 后端已选中才让 child 套 Job;legacy 恒 false(见 ownsRuntimeViaJobObject)。
|
|
145
|
+
const useJobObject = ownsRuntimeViaJobObject(ownershipMode, platform);
|
|
146
|
+
const signalTree = options.signalTree ?? signalSupervisorTree;
|
|
92
147
|
const childEntry = options.childEntry
|
|
93
148
|
?? fileURLToPath(new URL("./execution-supervisor-child.js", import.meta.url));
|
|
94
149
|
const abortTimeoutMs = options.abortTimeoutMs ?? DEFAULT_ABORT_TIMEOUT_MS;
|
|
@@ -118,12 +173,16 @@ export async function startDormantSupervisor(launch, options = {}) {
|
|
|
118
173
|
...(supervisorSpawnError === undefined && signal !== null ? { terminationSignal: signal } : {}),
|
|
119
174
|
}));
|
|
120
175
|
});
|
|
176
|
+
const supervisorClosed = new Promise((resolve) => child.once("close", () => resolve()));
|
|
177
|
+
let treeStopPromise = null;
|
|
178
|
+
const ensureTreeStopped = () => {
|
|
179
|
+
treeStopPromise ??= confirmOrTerminateOwnedTree(pid, platform, abortTimeoutMs, supervisorClosed, signalTree);
|
|
180
|
+
return treeStopPromise;
|
|
181
|
+
};
|
|
121
182
|
const exit = supervisorExit.then(async (result) => {
|
|
122
|
-
|
|
123
|
-
await waitForProcessGroupExit(pid, abortTimeoutMs);
|
|
183
|
+
await ensureTreeStopped();
|
|
124
184
|
return result;
|
|
125
185
|
});
|
|
126
|
-
const supervisorClosed = new Promise((resolve) => child.once("close", () => resolve()));
|
|
127
186
|
let readyResolve;
|
|
128
187
|
let readyReject;
|
|
129
188
|
const ready = new Promise((resolve, reject) => {
|
|
@@ -161,41 +220,30 @@ export async function startDormantSupervisor(launch, options = {}) {
|
|
|
161
220
|
const abort = async () => {
|
|
162
221
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
163
222
|
await supervisorClosed;
|
|
223
|
+
await ensureTreeStopped();
|
|
164
224
|
return;
|
|
165
225
|
}
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
}
|
|
226
|
+
await ensureTreeStopped();
|
|
227
|
+
await waitForExit(supervisorClosed.then(() => ({ exitCode: 0 })), abortTimeoutMs, pid);
|
|
228
|
+
await ensureTreeStopped();
|
|
188
229
|
};
|
|
189
230
|
const cancel = async () => {
|
|
190
231
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
191
232
|
await supervisorClosed;
|
|
233
|
+
await ensureTreeStopped();
|
|
192
234
|
return;
|
|
193
235
|
}
|
|
194
236
|
try {
|
|
195
237
|
await new Promise((resolve, reject) => {
|
|
196
238
|
child.send({ type: "abort" }, (error) => error === null ? resolve() : reject(error));
|
|
197
239
|
});
|
|
198
|
-
|
|
240
|
+
if (platform === "win32") {
|
|
241
|
+
await ensureTreeStopped();
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
await waitForExit(supervisorClosed.then(() => ({ exitCode: 0 })), abortTimeoutMs, pid);
|
|
245
|
+
await ensureTreeStopped();
|
|
246
|
+
}
|
|
199
247
|
}
|
|
200
248
|
catch {
|
|
201
249
|
await abort();
|
|
@@ -203,7 +251,7 @@ export async function startDormantSupervisor(launch, options = {}) {
|
|
|
203
251
|
};
|
|
204
252
|
try {
|
|
205
253
|
await new Promise((resolve, reject) => {
|
|
206
|
-
child.send({ type: "launch", launch: { ...launch, args: [...launch.args], env } }, (error) => {
|
|
254
|
+
child.send({ type: "launch", launch: { ...launch, args: [...launch.args], env }, useJobObject }, (error) => {
|
|
207
255
|
if (error === null)
|
|
208
256
|
resolve();
|
|
209
257
|
else
|
|
@@ -218,9 +266,8 @@ export async function startDormantSupervisor(launch, options = {}) {
|
|
|
218
266
|
});
|
|
219
267
|
throw error;
|
|
220
268
|
}
|
|
221
|
-
|
|
269
|
+
const handle = {
|
|
222
270
|
pid,
|
|
223
|
-
parentExitGuard: "pipe-eof",
|
|
224
271
|
stdout: child.stdout,
|
|
225
272
|
stderr: child.stderr,
|
|
226
273
|
exit,
|
|
@@ -249,4 +296,7 @@ export async function startDormantSupervisor(launch, options = {}) {
|
|
|
249
296
|
abort,
|
|
250
297
|
cancel,
|
|
251
298
|
};
|
|
299
|
+
return ownershipMode === "durable"
|
|
300
|
+
? { ...handle, ownershipMode, parentExitGuard: "pipe-eof" }
|
|
301
|
+
: { ...handle, ownershipMode };
|
|
252
302
|
}
|
package/dist/external-output.js
CHANGED
|
@@ -84,3 +84,31 @@ export function stripExternalAnswerMarkers(value) {
|
|
|
84
84
|
}
|
|
85
85
|
return (sections.length > 0 ? sections.join("") : value).trim();
|
|
86
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* 提取 marker 内容:有 marker 返回拼接内容(trim,空→null),无 marker 返回 null。
|
|
89
|
+
* 与 stripExternalAnswerMarkers 的区别:strip 在无 marker 时回退整段原文(finalText 展示用),
|
|
90
|
+
* 本函数用于判定"agent 是否给出了频道直接回复"——必须能区分有无 marker。
|
|
91
|
+
*/
|
|
92
|
+
export function extractExternalAnswer(value) {
|
|
93
|
+
const parts = [];
|
|
94
|
+
let found = false;
|
|
95
|
+
let rest = value;
|
|
96
|
+
for (;;) {
|
|
97
|
+
const open = rest.indexOf(EXTERNAL_ANSWER_OPEN);
|
|
98
|
+
if (open < 0)
|
|
99
|
+
break;
|
|
100
|
+
found = true;
|
|
101
|
+
const afterOpen = rest.slice(open + EXTERNAL_ANSWER_OPEN.length);
|
|
102
|
+
const close = afterOpen.indexOf(EXTERNAL_ANSWER_CLOSE);
|
|
103
|
+
if (close < 0) {
|
|
104
|
+
parts.push(afterOpen);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
parts.push(afterOpen.slice(0, close));
|
|
108
|
+
rest = afterOpen.slice(close + EXTERNAL_ANSWER_CLOSE.length);
|
|
109
|
+
}
|
|
110
|
+
if (!found)
|
|
111
|
+
return null;
|
|
112
|
+
const answer = parts.join("").trim();
|
|
113
|
+
return answer.length > 0 ? answer : null;
|
|
114
|
+
}
|
package/dist/i18n.js
CHANGED
|
@@ -17,6 +17,8 @@ export function detectDaemonLang(env = process.env) {
|
|
|
17
17
|
}
|
|
18
18
|
const zh = {
|
|
19
19
|
"Claude session started": "Claude 会话启动",
|
|
20
|
+
"Codex session started": "Codex 会话启动",
|
|
21
|
+
"Files changed": "文件变更",
|
|
20
22
|
"Run failed": "运行出错",
|
|
21
23
|
"Run finished": "本轮结束",
|
|
22
24
|
"Missing CREW_MACHINE_TOKEN (sk_machine_*, printed by seed)": "缺少 CREW_MACHINE_TOKEN(sk_machine_*,由 seed 打印)",
|
|
@@ -40,6 +42,7 @@ const zh = {
|
|
|
40
42
|
"Saved profile '{{name}}' with private credentials.": "已保存配置 '{{name}}',凭证仅私有可读。",
|
|
41
43
|
"Service '{{id}}' is not installed": "服务 '{{id}}' 尚未安装",
|
|
42
44
|
"Upgraded daemon and restart request accepted for '{{name}}'. Verify with status.": "daemon 已升级,并已请求重启 '{{name}}';请用 status 确认。",
|
|
45
|
+
"Upgraded daemon but skipped restart for '{{name}}': {{reason}}": "daemon 已升级,但已跳过 '{{name}}' 的重启:{{reason}}",
|
|
43
46
|
"Upgraded daemon. Installed services were not restarted; pass --profile to restart one.": "daemon 已升级;已安装服务尚未重启,可传入 --profile 重启指定服务。",
|
|
44
47
|
"Service lifecycle requires the built daemon entry (.js), not a TypeScript development entry": "服务生命周期必须使用已构建的 daemon 入口(.js),不能使用 TypeScript 开发入口",
|
|
45
48
|
"Installed '{{id}}'. Use status to confirm runtime state.": "已安装 '{{id}}';请用 status 确认运行状态。",
|
|
@@ -47,6 +50,9 @@ const zh = {
|
|
|
47
50
|
"{{action}} request accepted for '{{id}}'. Verify with status.": "已接受对 '{{id}}' 的 {{action}} 请求;请用 status 确认。",
|
|
48
51
|
"--token-stdin requires a token on standard input": "--token-stdin 需要从标准输入读取令牌",
|
|
49
52
|
"Service lifecycle requires a global @nowcrew/daemon install; run npm install --global @nowcrew/daemon@latest": "服务生命周期需要全局安装 @nowcrew/daemon;请运行 npm install --global @nowcrew/daemon@latest",
|
|
53
|
+
"Profile '{{profile}}' conflicts with profile '{{conflict}}': both resolve to agents root '{{agentsRoot}}'. Save it with a unique root, for example: {{command}}": "配置 '{{profile}}' 与配置 '{{conflict}}' 解析到了同一个 agents root '{{agentsRoot}}'。请保存为唯一目录,例如:{{command}}",
|
|
54
|
+
"Daemon is already running (PID={{ownerPid}}) for agents root '{{agentsRoot}}'. Journal: '{{journalPath}}'. Do not delete the active journal lock. Stop the existing daemon before retrying. If this profile is managed as a service, use 'crew-daemon stop --profile {{profileName}}' or 'crew-daemon restart --profile {{profileName}}'.": "daemon 已在运行(PID={{ownerPid}}),agents root 为 '{{agentsRoot}}'。日志目录:'{{journalPath}}'。不要删除活跃进程持有的 journal 锁。请先停止现有 daemon 再重试;如果此 profile 由系统服务管理,请使用 'crew-daemon stop --profile {{profileName}}' 或 'crew-daemon restart --profile {{profileName}}'。",
|
|
55
|
+
"Daemon is already running (PID={{ownerPid}}) for agents root '{{agentsRoot}}'. Journal: '{{journalPath}}'. Do not delete the active journal lock. Stop the existing daemon before retrying. If it is managed as a service, use 'crew-daemon stop --profile <name>' or 'crew-daemon restart --profile <name>'.": "daemon 已在运行(PID={{ownerPid}}),agents root 为 '{{agentsRoot}}'。日志目录:'{{journalPath}}'。不要删除活跃进程持有的 journal 锁。请先停止现有 daemon 再重试;如果它由系统服务管理,请使用 'crew-daemon stop --profile <name>' 或 'crew-daemon restart --profile <name>'。",
|
|
50
56
|
};
|
|
51
57
|
export function translateDaemon(lang, message) {
|
|
52
58
|
if (lang === "zh")
|
|
@@ -54,9 +60,5 @@ export function translateDaemon(lang, message) {
|
|
|
54
60
|
return message;
|
|
55
61
|
}
|
|
56
62
|
export function formatDaemonText(lang, message, values = {}) {
|
|
57
|
-
|
|
58
|
-
for (const [key, value] of Object.entries(values)) {
|
|
59
|
-
rendered = rendered.replaceAll(`{{${key}}}`, String(value));
|
|
60
|
-
}
|
|
61
|
-
return rendered;
|
|
63
|
+
return translateDaemon(lang, message).replace(/\{\{([^{}]+)\}\}/g, (token, key) => Object.hasOwn(values, key) ? String(values[key]) : token);
|
|
62
64
|
}
|