@nowcrew/daemon 0.5.19 → 0.5.20
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/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 +71 -0
- package/dist/execution-runner.js +49 -91
- package/dist/execution-supervisor.js +79 -31
- package/dist/external-output.js +28 -0
- package/dist/i18n.js +5 -5
- package/dist/local-executor.js +66 -15
- package/dist/machine-info.js +1 -0
- package/dist/main.js +23 -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 +186 -218
- 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/package.json +2 -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,
|
|
@@ -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,13 @@ 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
|
+
const signalTree = options.signalTree ?? signalSupervisorTree;
|
|
92
145
|
const childEntry = options.childEntry
|
|
93
146
|
?? fileURLToPath(new URL("./execution-supervisor-child.js", import.meta.url));
|
|
94
147
|
const abortTimeoutMs = options.abortTimeoutMs ?? DEFAULT_ABORT_TIMEOUT_MS;
|
|
@@ -118,12 +171,16 @@ export async function startDormantSupervisor(launch, options = {}) {
|
|
|
118
171
|
...(supervisorSpawnError === undefined && signal !== null ? { terminationSignal: signal } : {}),
|
|
119
172
|
}));
|
|
120
173
|
});
|
|
174
|
+
const supervisorClosed = new Promise((resolve) => child.once("close", () => resolve()));
|
|
175
|
+
let treeStopPromise = null;
|
|
176
|
+
const ensureTreeStopped = () => {
|
|
177
|
+
treeStopPromise ??= confirmOrTerminateOwnedTree(pid, platform, abortTimeoutMs, supervisorClosed, signalTree);
|
|
178
|
+
return treeStopPromise;
|
|
179
|
+
};
|
|
121
180
|
const exit = supervisorExit.then(async (result) => {
|
|
122
|
-
|
|
123
|
-
await waitForProcessGroupExit(pid, abortTimeoutMs);
|
|
181
|
+
await ensureTreeStopped();
|
|
124
182
|
return result;
|
|
125
183
|
});
|
|
126
|
-
const supervisorClosed = new Promise((resolve) => child.once("close", () => resolve()));
|
|
127
184
|
let readyResolve;
|
|
128
185
|
let readyReject;
|
|
129
186
|
const ready = new Promise((resolve, reject) => {
|
|
@@ -161,41 +218,30 @@ export async function startDormantSupervisor(launch, options = {}) {
|
|
|
161
218
|
const abort = async () => {
|
|
162
219
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
163
220
|
await supervisorClosed;
|
|
221
|
+
await ensureTreeStopped();
|
|
164
222
|
return;
|
|
165
223
|
}
|
|
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
|
-
}
|
|
224
|
+
await ensureTreeStopped();
|
|
225
|
+
await waitForExit(supervisorClosed.then(() => ({ exitCode: 0 })), abortTimeoutMs, pid);
|
|
226
|
+
await ensureTreeStopped();
|
|
188
227
|
};
|
|
189
228
|
const cancel = async () => {
|
|
190
229
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
191
230
|
await supervisorClosed;
|
|
231
|
+
await ensureTreeStopped();
|
|
192
232
|
return;
|
|
193
233
|
}
|
|
194
234
|
try {
|
|
195
235
|
await new Promise((resolve, reject) => {
|
|
196
236
|
child.send({ type: "abort" }, (error) => error === null ? resolve() : reject(error));
|
|
197
237
|
});
|
|
198
|
-
|
|
238
|
+
if (platform === "win32") {
|
|
239
|
+
await ensureTreeStopped();
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
await waitForExit(supervisorClosed.then(() => ({ exitCode: 0 })), abortTimeoutMs, pid);
|
|
243
|
+
await ensureTreeStopped();
|
|
244
|
+
}
|
|
199
245
|
}
|
|
200
246
|
catch {
|
|
201
247
|
await abort();
|
|
@@ -218,9 +264,8 @@ export async function startDormantSupervisor(launch, options = {}) {
|
|
|
218
264
|
});
|
|
219
265
|
throw error;
|
|
220
266
|
}
|
|
221
|
-
|
|
267
|
+
const handle = {
|
|
222
268
|
pid,
|
|
223
|
-
parentExitGuard: "pipe-eof",
|
|
224
269
|
stdout: child.stdout,
|
|
225
270
|
stderr: child.stderr,
|
|
226
271
|
exit,
|
|
@@ -249,4 +294,7 @@ export async function startDormantSupervisor(launch, options = {}) {
|
|
|
249
294
|
abort,
|
|
250
295
|
cancel,
|
|
251
296
|
};
|
|
297
|
+
return ownershipMode === "durable"
|
|
298
|
+
? { ...handle, ownershipMode, parentExitGuard: "pipe-eof" }
|
|
299
|
+
: { ...handle, ownershipMode };
|
|
252
300
|
}
|
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,7 @@ 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}}",
|
|
50
54
|
};
|
|
51
55
|
export function translateDaemon(lang, message) {
|
|
52
56
|
if (lang === "zh")
|
|
@@ -54,9 +58,5 @@ export function translateDaemon(lang, message) {
|
|
|
54
58
|
return message;
|
|
55
59
|
}
|
|
56
60
|
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;
|
|
61
|
+
return translateDaemon(lang, message).replace(/\{\{([^{}]+)\}\}/g, (token, key) => Object.hasOwn(values, key) ? String(values[key]) : token);
|
|
62
62
|
}
|
package/dist/local-executor.js
CHANGED
|
@@ -11,9 +11,10 @@ import { extractFinalText, extractRunMeta, normalizeEvent, parseLine, } from "./
|
|
|
11
11
|
import { readSession, writeSession, pickResumeId, isNearBudget } from "./session.js";
|
|
12
12
|
import { toConsoleLines } from "./console.js";
|
|
13
13
|
import { capMemoryForInject, capWorkLogForInject } from "./prompt.js";
|
|
14
|
-
import { decodeExternalOutputEvent, ExternalAnswerDecoder, stripExternalAnswerMarkers, } from "./external-output.js";
|
|
15
|
-
import { cleanupMaterializedAttachments as cleanupAttachments, materializeAttachments, } from "./attachments.js";
|
|
14
|
+
import { decodeExternalOutputEvent, extractExternalAnswer, ExternalAnswerDecoder, stripExternalAnswerMarkers, } from "./external-output.js";
|
|
15
|
+
import { cleanupMaterializedAttachments as cleanupAttachments, executionAttachmentDirectory, materializeAttachments, } from "./attachments.js";
|
|
16
16
|
import { routeRuntimeAttachments, runtimeCapability, } from "./runtime-capabilities.js";
|
|
17
|
+
import { awaitWithCancellation, RuntimeCancelledError, } from "./runtime-cancellation.js";
|
|
17
18
|
function truncateUtf8(value, maxBytes) {
|
|
18
19
|
if (maxBytes <= 0)
|
|
19
20
|
return "";
|
|
@@ -138,20 +139,22 @@ function resolvePrompt(prompt, context) {
|
|
|
138
139
|
return typeof prompt === "string" ? prompt : prompt(context);
|
|
139
140
|
}
|
|
140
141
|
const nativeSessionLeaseTails = new Map();
|
|
141
|
-
async function withKeyedLease(key, operation) {
|
|
142
|
+
async function withKeyedLease(key, operation, cancellation) {
|
|
142
143
|
const predecessor = nativeSessionLeaseTails.get(key) ?? Promise.resolve();
|
|
143
144
|
let release;
|
|
144
145
|
const current = new Promise((resolve) => { release = resolve; });
|
|
145
146
|
const tail = predecessor.then(() => current);
|
|
146
147
|
nativeSessionLeaseTails.set(key, tail);
|
|
147
|
-
await predecessor;
|
|
148
148
|
try {
|
|
149
|
+
await awaitWithCancellation(predecessor, cancellation);
|
|
149
150
|
return await operation();
|
|
150
151
|
}
|
|
151
152
|
finally {
|
|
152
153
|
release();
|
|
153
|
-
|
|
154
|
-
nativeSessionLeaseTails.
|
|
154
|
+
void tail.then(() => {
|
|
155
|
+
if (nativeSessionLeaseTails.get(key) === tail)
|
|
156
|
+
nativeSessionLeaseTails.delete(key);
|
|
157
|
+
});
|
|
155
158
|
}
|
|
156
159
|
}
|
|
157
160
|
export async function executeLocal(input, callbacks = {}, dependencies = {}) {
|
|
@@ -167,14 +170,14 @@ export async function executeLocal(input, callbacks = {}, dependencies = {}) {
|
|
|
167
170
|
input.keyMode ?? "legacy",
|
|
168
171
|
input.resumeKey ?? input.taskKey ?? "",
|
|
169
172
|
]);
|
|
170
|
-
return withKeyedLease(leaseKey, () => executeLocalUnlocked(input, callbacks, dependencies));
|
|
173
|
+
return withKeyedLease(leaseKey, () => executeLocalUnlocked(input, callbacks, dependencies), dependencies.cancellation);
|
|
171
174
|
}
|
|
172
175
|
async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
173
176
|
const providerConfig = input.launch.providerConfig ?? {};
|
|
174
177
|
const { runtime } = input;
|
|
175
178
|
const currentModel = runtime.model ?? null;
|
|
176
179
|
const providerFp = providerFingerprint(runtime.name, providerConfig);
|
|
177
|
-
const workspace = await prepareWorkspace({
|
|
180
|
+
const workspace = await awaitWithCancellation(prepareWorkspace({
|
|
178
181
|
agentsRoot: input.launch.agentsRoot,
|
|
179
182
|
handle: input.handle,
|
|
180
183
|
cliPath: input.launch.cliPath,
|
|
@@ -183,8 +186,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
183
186
|
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
|
|
184
187
|
...(input.resumeKey === undefined ? {} : { resumeKey: input.resumeKey }),
|
|
185
188
|
...(input.launch.description ? { description: input.launch.description } : {}),
|
|
186
|
-
});
|
|
189
|
+
}), dependencies.cancellation);
|
|
187
190
|
let materialized = null;
|
|
191
|
+
let knownAttachmentDirectory = null;
|
|
188
192
|
try {
|
|
189
193
|
const supportsNativeResume = runtime.name === "claude"
|
|
190
194
|
|| (dependencies.launchRuntime !== undefined && runtimeCapability(runtime.name).nativeResume);
|
|
@@ -208,17 +212,38 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
208
212
|
};
|
|
209
213
|
const systemPrompt = resolvePrompt(input.systemPrompt, promptContext);
|
|
210
214
|
if (input.attachments && input.attachments.length > 0) {
|
|
211
|
-
|
|
215
|
+
if (dependencies.cancellation?.isRequested())
|
|
216
|
+
throw new RuntimeCancelledError();
|
|
217
|
+
knownAttachmentDirectory = executionAttachmentDirectory(workspace.runDir, input.executionId);
|
|
218
|
+
const controller = new AbortController();
|
|
219
|
+
const materialization = Promise.resolve().then(() => (dependencies.materializeAttachments ?? materializeAttachments)({
|
|
212
220
|
serverUrl: input.launch.serverUrl,
|
|
213
221
|
token: input.launch.token,
|
|
214
222
|
runDir: workspace.runDir,
|
|
215
223
|
executionId: input.executionId,
|
|
216
224
|
attachments: input.attachments,
|
|
225
|
+
signal: controller.signal,
|
|
226
|
+
}));
|
|
227
|
+
dependencies.cancellation?.register(async () => {
|
|
228
|
+
controller.abort(new RuntimeCancelledError());
|
|
229
|
+
await materialization.then(() => undefined, () => undefined);
|
|
217
230
|
});
|
|
231
|
+
try {
|
|
232
|
+
materialized = await materialization;
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (dependencies.cancellation?.isRequested())
|
|
236
|
+
throw new RuntimeCancelledError();
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
if (dependencies.cancellation?.isRequested()) {
|
|
240
|
+
await dependencies.cancellation.waitForStop();
|
|
241
|
+
throw new RuntimeCancelledError();
|
|
242
|
+
}
|
|
218
243
|
}
|
|
219
244
|
const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
|
|
220
245
|
const wakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
221
|
-
await writeFile(workspace.systemPromptPath, systemPrompt, "utf8");
|
|
246
|
+
await awaitWithCancellation(writeFile(workspace.systemPromptPath, systemPrompt, "utf8"), dependencies.cancellation);
|
|
222
247
|
const baseEnv = {
|
|
223
248
|
...process.env,
|
|
224
249
|
...sanitizeEnvVars(providerConfig.envVars),
|
|
@@ -242,6 +267,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
242
267
|
};
|
|
243
268
|
const childEnv = applyProviderEnv(baseEnv, runtime.name, providerConfig, workspace.homeDir);
|
|
244
269
|
const launchRuntime = dependencies.launchRuntime ?? launchLegacyRuntime;
|
|
270
|
+
if (dependencies.cancellation?.isRequested())
|
|
271
|
+
throw new RuntimeCancelledError();
|
|
245
272
|
const child = await launchRuntime({
|
|
246
273
|
runtime: runtime.name,
|
|
247
274
|
bin: runtime.name,
|
|
@@ -259,6 +286,13 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
259
286
|
? { imagePaths: attachmentPlan.nativeImagePaths }
|
|
260
287
|
: {}),
|
|
261
288
|
});
|
|
289
|
+
if (child.cancel !== undefined) {
|
|
290
|
+
dependencies.cancellation?.register(child.cancel);
|
|
291
|
+
}
|
|
292
|
+
if (dependencies.cancellation?.isRequested()) {
|
|
293
|
+
await dependencies.cancellation.waitForStop();
|
|
294
|
+
throw new RuntimeCancelledError();
|
|
295
|
+
}
|
|
262
296
|
const activities = [];
|
|
263
297
|
let sessionId = launchSessionId;
|
|
264
298
|
let usage;
|
|
@@ -301,7 +335,17 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
301
335
|
process.stderr.write(data);
|
|
302
336
|
stderrTail = (stderrTail + String(data)).slice(-STDERR_TAIL_CAP);
|
|
303
337
|
});
|
|
304
|
-
|
|
338
|
+
let runtimeExit;
|
|
339
|
+
try {
|
|
340
|
+
runtimeExit = await awaitWithCancellation(child.exit, dependencies.cancellation);
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
if (error instanceof RuntimeCancelledError) {
|
|
344
|
+
await dependencies.cancellation?.waitForStop();
|
|
345
|
+
}
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
const { exitCode, spawnError, terminationSignal } = runtimeExit;
|
|
305
349
|
const errorTail = [
|
|
306
350
|
stderrTail.trim(),
|
|
307
351
|
spawnError,
|
|
@@ -343,17 +387,24 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
343
387
|
finalText: input.captureFinal && finalText !== null
|
|
344
388
|
? stripExternalAnswerMarkers(finalText)
|
|
345
389
|
: null,
|
|
390
|
+
externalAnswer: input.captureFinal && finalText !== null
|
|
391
|
+
? extractExternalAnswer(finalText)
|
|
392
|
+
: null,
|
|
346
393
|
sentViaCrew,
|
|
347
394
|
};
|
|
348
395
|
}
|
|
349
396
|
finally {
|
|
350
|
-
|
|
397
|
+
const attachmentDirectories = new Set([
|
|
398
|
+
...(knownAttachmentDirectory === null ? [] : [knownAttachmentDirectory]),
|
|
399
|
+
...(materialized === null ? [] : [materialized.directory]),
|
|
400
|
+
]);
|
|
401
|
+
for (const directory of attachmentDirectories) {
|
|
351
402
|
try {
|
|
352
|
-
await (dependencies.cleanupMaterializedAttachments ?? cleanupAttachments)(
|
|
403
|
+
await (dependencies.cleanupMaterializedAttachments ?? cleanupAttachments)(directory);
|
|
353
404
|
}
|
|
354
405
|
catch (error) {
|
|
355
406
|
const detail = error instanceof Error ? error.message : String(error);
|
|
356
|
-
process.stderr.write(`[execution] failed to remove attachments ${
|
|
407
|
+
process.stderr.write(`[execution] failed to remove attachments ${directory}: ${detail}\n`);
|
|
357
408
|
}
|
|
358
409
|
}
|
|
359
410
|
try {
|
package/dist/machine-info.js
CHANGED
|
@@ -24,6 +24,7 @@ export const DAEMON_CAPABILITIES = [
|
|
|
24
24
|
"execution_telemetry_ack_v1",
|
|
25
25
|
"execution_external_output_v1",
|
|
26
26
|
"execution_attachments_v1",
|
|
27
|
+
"execution_answer_stream_v1",
|
|
27
28
|
];
|
|
28
29
|
export const EXECUTION_PROTOCOL = Object.freeze({ min: 1, max: 1 });
|
|
29
30
|
/** 候选 runtime CLI:展示名 → 可执行文件名。 */
|