@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/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:展示名 → 可执行文件名。 */
|
|
@@ -102,7 +103,7 @@ export async function collectMachineHello(agentsRoot, executionLimits, runtimePl
|
|
|
102
103
|
(dependencies.detectInstalled ?? detectRuntimes)(),
|
|
103
104
|
listAgentHandles(agentsRoot),
|
|
104
105
|
]);
|
|
105
|
-
const backend = executionBackendCapability(runtimePlatform);
|
|
106
|
+
const backend = executionBackendCapability(runtimePlatform, dependencies.jobObjectProbe);
|
|
106
107
|
const executionRuntimes = backend.supported
|
|
107
108
|
? await (dependencies.detectExecutable ?? detectExecutionRuntimes)(runtimes)
|
|
108
109
|
: [];
|
package/dist/main.js
CHANGED
|
@@ -6,15 +6,18 @@
|
|
|
6
6
|
* 后续 (M3c):常驻 + 连 server 控制面 WS,由 agent:start 自动唤醒。
|
|
7
7
|
*/
|
|
8
8
|
import { parseArgs } from "node:util";
|
|
9
|
+
import { homedir } from "node:os";
|
|
9
10
|
import { loadConfig, ConfigError } from "./config.js";
|
|
10
|
-
import { detectDaemonLang, translateDaemon } from "./i18n.js";
|
|
11
|
+
import { detectDaemonLang, formatDaemonText, translateDaemon } from "./i18n.js";
|
|
11
12
|
import { cliVersion, daemonVersion } from "./machine-info.js";
|
|
12
13
|
import { runAgent } from "./runner.js";
|
|
13
14
|
import { serve } from "./serve.js";
|
|
14
15
|
import { initSlog, flushSlog } from "./slog.js";
|
|
15
16
|
import { formatDaemonLogLine } from "./log-format.js";
|
|
16
|
-
import { loadProfile,
|
|
17
|
+
import { applyProfileToEnv, assertProfileAgentsRootUnique, daemonHome, loadProfile, PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE, ProfileAgentsRootConflictError, } from "./computer-profile.js";
|
|
17
18
|
import { runComputerCommand } from "./computer-cli.js";
|
|
19
|
+
import { runServeLifecycle } from "./serve-lifecycle.js";
|
|
20
|
+
import { formatDaemonStartupError } from "./daemon-startup-error.js";
|
|
18
21
|
async function main() {
|
|
19
22
|
const computerResult = await runComputerCommand(process.argv.slice(2));
|
|
20
23
|
if (computerResult !== null) {
|
|
@@ -51,8 +54,13 @@ async function main() {
|
|
|
51
54
|
// 不把 machine token 放进 argv / plist / systemd unit / scheduled task。
|
|
52
55
|
if (values["daemon-home"])
|
|
53
56
|
process.env.CREW_DAEMON_HOME = values["daemon-home"];
|
|
54
|
-
if (values.profile)
|
|
55
|
-
|
|
57
|
+
if (values.profile) {
|
|
58
|
+
const home = daemonHome();
|
|
59
|
+
const profile = await loadProfile(values.profile, home);
|
|
60
|
+
if (cmd === "serve")
|
|
61
|
+
await assertProfileAgentsRootUnique(profile, home, homedir());
|
|
62
|
+
applyProfileToEnv(profile, process.env);
|
|
63
|
+
}
|
|
56
64
|
// 命令行参数优先于环境变量,填回 env 供 loadConfig 读取
|
|
57
65
|
if (values["server-url"])
|
|
58
66
|
process.env.CREW_SERVER_URL = values["server-url"];
|
|
@@ -72,8 +80,10 @@ async function main() {
|
|
|
72
80
|
}
|
|
73
81
|
if (cmd === "serve") {
|
|
74
82
|
process.stdout.write(formatDaemonLogLine(`🛰️ crew-daemon v${daemonVersion()} (cli v${cliVersion()}) ${td("resident, connecting to")} ${config.serverUrl} ${td("control plane")}...`) + "\n");
|
|
75
|
-
serve(config
|
|
76
|
-
|
|
83
|
+
const service = serve(config, {
|
|
84
|
+
...(values.profile === undefined ? {} : { profileName: values.profile }),
|
|
85
|
+
});
|
|
86
|
+
await runServeLifecycle(service);
|
|
77
87
|
return;
|
|
78
88
|
}
|
|
79
89
|
if (!values.agent || !values.channel) {
|
|
@@ -93,6 +103,16 @@ async function main() {
|
|
|
93
103
|
process.exit(result.exitCode);
|
|
94
104
|
}
|
|
95
105
|
main().catch((e) => {
|
|
96
|
-
|
|
97
|
-
|
|
106
|
+
const lang = detectDaemonLang();
|
|
107
|
+
const message = formatDaemonStartupError(e, lang)
|
|
108
|
+
?? (e instanceof ProfileAgentsRootConflictError
|
|
109
|
+
? formatDaemonText(lang, PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE, {
|
|
110
|
+
profile: e.profile,
|
|
111
|
+
conflict: e.conflict,
|
|
112
|
+
agentsRoot: e.agentsRoot,
|
|
113
|
+
command: e.command,
|
|
114
|
+
})
|
|
115
|
+
: e.message);
|
|
116
|
+
process.stderr.write(`crew-daemon: ${message}\n`);
|
|
117
|
+
process.exitCode = 1;
|
|
98
118
|
});
|
package/dist/runner.js
CHANGED
|
@@ -4,10 +4,12 @@ import { join } from "node:path";
|
|
|
4
4
|
import { mintAgentToken } from "./token.js";
|
|
5
5
|
import { buildSystemPrompt, buildWakePrompt, capMemoryForInject, capWorkLogForInject } from "./prompt.js";
|
|
6
6
|
import { deliverScheduledReport, } from "./scheduled-report.js";
|
|
7
|
-
import { executeLocal } from "./local-executor.js";
|
|
7
|
+
import { executeLocal, } from "./local-executor.js";
|
|
8
8
|
import { ReasoningSchema } from "./execution-protocol.js";
|
|
9
9
|
import { readOriginDecisionFile, resetOriginDecisionFile } from "./origin-decision.js";
|
|
10
10
|
import { readBoundImDecisionFile, resetBoundImDecisionFile } from "./bound-im-decision.js";
|
|
11
|
+
import { launchSupervisedRuntime } from "./supervised-runtime.js";
|
|
12
|
+
import { awaitWithCancellation, } from "./runtime-cancellation.js";
|
|
11
13
|
export { awaitExit, exitActivity, sanitizeEnvVars } from "./local-executor.js";
|
|
12
14
|
const ICON = {
|
|
13
15
|
init: "🟢", text: "💬", reading: "📖", sending: "📨", checking: "🔎",
|
|
@@ -18,12 +20,12 @@ function runtimeName(value) {
|
|
|
18
20
|
return value;
|
|
19
21
|
throw new Error(`unsupported runtime: ${value}`);
|
|
20
22
|
}
|
|
21
|
-
export async function runAgent(config, input, onActivity = defaultPrint, onConsole = () => { }) {
|
|
22
|
-
const credential = await mintAgentToken(config.serverUrl, config.machineToken, input.handle, input.displayName, {
|
|
23
|
+
export async function runAgent(config, input, onActivity = defaultPrint, onConsole = () => { }, dependencies = {}) {
|
|
24
|
+
const credential = await awaitWithCancellation((dependencies.mintAgentToken ?? mintAgentToken)(config.serverUrl, config.machineToken, input.handle, input.displayName, {
|
|
23
25
|
...(input.wakeMessageId ? { wakeThreadRoot: input.wakeMessageId } : {}),
|
|
24
26
|
...(input.wakeContextUpToSeq === undefined ? {} : { wakeContextUpToSeq: input.wakeContextUpToSeq }),
|
|
25
27
|
...(input.runId ? { agentRunId: input.runId } : {}),
|
|
26
|
-
});
|
|
28
|
+
}), dependencies.cancellation);
|
|
27
29
|
const providerConfig = credential.config ?? {};
|
|
28
30
|
const runtime = runtimeName(providerConfig.runtime ?? config.runtimeBin);
|
|
29
31
|
const reasoning = ReasoningSchema.safeParse(providerConfig.reasoning);
|
|
@@ -35,7 +37,7 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
35
37
|
const boundImDecisionFileName = input.scheduled?.externalNotificationPolicy === "agent_decides"
|
|
36
38
|
? `.bound-im-decision-${executionId}.json`
|
|
37
39
|
: null;
|
|
38
|
-
const local = await executeLocal({
|
|
40
|
+
const local = await (dependencies.executeLocal ?? executeLocal)({
|
|
39
41
|
executionId,
|
|
40
42
|
handle: input.handle,
|
|
41
43
|
channelId: input.channelId,
|
|
@@ -101,7 +103,10 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
101
103
|
softTokens: config.sessionSoftTokens,
|
|
102
104
|
maxTurns: config.sessionMaxTurns,
|
|
103
105
|
},
|
|
104
|
-
}, { onActivity, onConsole }
|
|
106
|
+
}, { onActivity, onConsole }, {
|
|
107
|
+
launchRuntime: dependencies.launchRuntime ?? ((request) => launchSupervisedRuntime(request, dependencies.startSupervisor, dependencies.cancellation, dependencies.platform)),
|
|
108
|
+
...(dependencies.cancellation === undefined ? {} : { cancellation: dependencies.cancellation }),
|
|
109
|
+
});
|
|
105
110
|
const activities = [...local.activities];
|
|
106
111
|
if (!input.scheduled && (runtime === "codex" || runtime === "kimi")
|
|
107
112
|
&& local.exitCode === 0 && !local.sentViaCrew && local.finalText) {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export class RuntimeCancelledError extends Error {
|
|
2
|
+
constructor(message = "Runtime launch cancelled") {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "RuntimeCancelledError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export async function awaitWithCancellation(promise, cancellation) {
|
|
8
|
+
if (cancellation === undefined)
|
|
9
|
+
return promise;
|
|
10
|
+
if (cancellation.isRequested())
|
|
11
|
+
throw new RuntimeCancelledError();
|
|
12
|
+
const result = await Promise.race([
|
|
13
|
+
promise,
|
|
14
|
+
cancellation.requested.then(() => { throw new RuntimeCancelledError(); }),
|
|
15
|
+
]);
|
|
16
|
+
if (cancellation.isRequested())
|
|
17
|
+
throw new RuntimeCancelledError();
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
export function createRuntimeCancellation() {
|
|
21
|
+
let requested = false;
|
|
22
|
+
let resolveRequested;
|
|
23
|
+
const requestedPromise = new Promise((resolve) => { resolveRequested = resolve; });
|
|
24
|
+
const registrations = new Map();
|
|
25
|
+
const startRegisteredStops = () => {
|
|
26
|
+
if (!requested)
|
|
27
|
+
return;
|
|
28
|
+
for (const [cancel, stopPromise] of registrations) {
|
|
29
|
+
if (stopPromise !== null)
|
|
30
|
+
continue;
|
|
31
|
+
const started = Promise.resolve().then(cancel);
|
|
32
|
+
registrations.set(cancel, started);
|
|
33
|
+
void started.catch(() => undefined);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const waitForStop = async () => {
|
|
37
|
+
if (!requested)
|
|
38
|
+
return;
|
|
39
|
+
while (true) {
|
|
40
|
+
startRegisteredStops();
|
|
41
|
+
const registrationCount = registrations.size;
|
|
42
|
+
const results = await Promise.allSettled([...registrations.values()].filter((value) => value !== null));
|
|
43
|
+
if (registrations.size !== registrationCount)
|
|
44
|
+
continue;
|
|
45
|
+
const failures = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
46
|
+
if (failures.length === 1)
|
|
47
|
+
throw failures[0];
|
|
48
|
+
if (failures.length > 1)
|
|
49
|
+
throw new AggregateError(failures, "Runtime cancellation failed");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const cancellation = {
|
|
54
|
+
isRequested: () => requested,
|
|
55
|
+
requested: requestedPromise,
|
|
56
|
+
register: (next) => {
|
|
57
|
+
if (registrations.has(next))
|
|
58
|
+
return;
|
|
59
|
+
registrations.set(next, null);
|
|
60
|
+
startRegisteredStops();
|
|
61
|
+
},
|
|
62
|
+
waitForStop,
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
cancellation,
|
|
66
|
+
request: () => {
|
|
67
|
+
if (requested)
|
|
68
|
+
return;
|
|
69
|
+
requested = true;
|
|
70
|
+
resolveRequested();
|
|
71
|
+
startRegisteredStops();
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
package/dist/runtime-path.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* daemon 被非登录/非交互进程(sh -c / pnpm script / tmux / 后台转发)拉起时,继承的 PATH 常退化为系统
|
|
3
3
|
* 默认,缺用户级 CLI 目录——尤其 Claude 官方原生安装器默认的 `~/.local/bin`。结果 `which claude` 探测
|
|
4
|
-
* 落空、真正 spawn 也 ENOENT
|
|
4
|
+
* 落空、真正 spawn 也 ENOENT。Kimi 官方安装器同样只把 `~/.kimi-code/bin` 写入 shell rc。
|
|
5
|
+
* 这里在探测与启动前把常见安装目录补进 PATH,保证「扫得到 = 起得来」。
|
|
5
6
|
*
|
|
6
7
|
* codex 装在 `/usr/local/bin`(系统默认 PATH 本就含之)所以不受影响;本模块对已在 PATH 中的目录是无操作。
|
|
7
8
|
*/
|
|
@@ -13,8 +14,10 @@ export function commonBinDirs(env = process.env, platform = process.platform) {
|
|
|
13
14
|
const p = pathApi(platform);
|
|
14
15
|
if (platform === "win32") {
|
|
15
16
|
const dirs = [];
|
|
16
|
-
if (env.USERPROFILE)
|
|
17
|
-
dirs.push(p.join(env.USERPROFILE, ".
|
|
17
|
+
if (env.USERPROFILE) {
|
|
18
|
+
dirs.push(p.join(env.USERPROFILE, ".kimi-code", "bin"), // Kimi 官方安装器默认落点
|
|
19
|
+
p.join(env.USERPROFILE, ".local", "bin"));
|
|
20
|
+
}
|
|
18
21
|
if (env.APPDATA)
|
|
19
22
|
dirs.push(p.join(env.APPDATA, "npm"));
|
|
20
23
|
if (env.LOCALAPPDATA)
|
|
@@ -24,7 +27,8 @@ export function commonBinDirs(env = process.env, platform = process.platform) {
|
|
|
24
27
|
const dirs = [];
|
|
25
28
|
const home = env.HOME;
|
|
26
29
|
if (home) {
|
|
27
|
-
dirs.push(p.join(home, ".
|
|
30
|
+
dirs.push(p.join(home, ".kimi-code", "bin"), // Kimi 官方安装器默认落点
|
|
31
|
+
p.join(home, ".local", "bin"), // Claude 官方原生安装器默认落点
|
|
28
32
|
p.join(home, ".claude", "local"));
|
|
29
33
|
}
|
|
30
34
|
dirs.push("/opt/homebrew/bin", "/usr/local/bin"); // Apple Silicon brew / Intel brew & npm 全局
|
package/dist/runtimes/claude.js
CHANGED
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
// cross-spawn:win32 上 npm CLI 是 .cmd shim,node 原生 spawn 不带 shell 无法执行(ENOENT/EINVAL)
|
|
5
5
|
import spawn from "cross-spawn";
|
|
6
6
|
// Claude Code 原生 --effort 档位(claude 2.1.196 实测:--help 与非法值告警均枚举这五档)。
|
|
7
|
-
// 白名单外的值(含 "default" 与 codex 专属档)
|
|
7
|
+
// 白名单外的值(含 "default" 与 codex 专属档)回落 CLAUDE_DEFAULT_EFFORT,脏数据不影响启动。
|
|
8
8
|
export const CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
9
|
+
// 未配置/非法档位时的默认思考强度:medium 开启原生 thinking(终端透传要展示思考过程),
|
|
10
|
+
// 又不至于 high/max 的 token 开销;agent 配置白名单档位可覆盖。
|
|
11
|
+
export const CLAUDE_DEFAULT_EFFORT = "medium";
|
|
9
12
|
export function buildClaudeArgs(input) {
|
|
10
13
|
const args = [
|
|
11
14
|
"--print",
|
|
@@ -18,9 +21,10 @@ export function buildClaudeArgs(input) {
|
|
|
18
21
|
];
|
|
19
22
|
if (input.model)
|
|
20
23
|
args.push("--model", input.model);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
const effort = input.reasoning && CLAUDE_EFFORT_LEVELS.includes(input.reasoning)
|
|
25
|
+
? input.reasoning
|
|
26
|
+
: CLAUDE_DEFAULT_EFFORT;
|
|
27
|
+
args.push("--effort", effort);
|
|
24
28
|
if (input.sessionId) {
|
|
25
29
|
args.push(input.resume ? "--resume" : "--session-id", input.sessionId);
|
|
26
30
|
}
|
package/dist/runtimes/codex.js
CHANGED
|
@@ -5,17 +5,21 @@
|
|
|
5
5
|
import spawn from "cross-spawn";
|
|
6
6
|
// Codex CLI 原生 model_reasoning_effort 档位(codex 0.135.0 实测:非法值时 config 解析报错枚举这六档)。
|
|
7
7
|
// 注意:codex 对非法值是硬失败(进程直接退出),所以必须白名单过滤;白名单外(含 "default"、
|
|
8
|
-
// claude 专属的 "max")
|
|
8
|
+
// claude 专属的 "max")回落 CODEX_DEFAULT_EFFORT。
|
|
9
9
|
export const CODEX_EFFORT_LEVELS = ["none", "minimal", "low", "medium", "high", "xhigh"];
|
|
10
|
+
// 未配置/非法档位时的默认思考强度:medium 开启 reasoning(终端透传要展示思考过程);
|
|
11
|
+
// 配置白名单档位(含显式 none 关思考)可覆盖。
|
|
12
|
+
export const CODEX_DEFAULT_EFFORT = "medium";
|
|
10
13
|
export function buildCodexArgs(input) {
|
|
11
14
|
// agent 运行目录由 daemon 管理,不是 git 仓库;不带 --skip-git-repo-check 时 codex exec
|
|
12
15
|
// 会以 "Not inside a trusted directory" 秒退(且只报在本地 stderr),表现为 agent 静默不回复。
|
|
13
16
|
const args = ["exec", "--json", "--skip-git-repo-check"];
|
|
14
17
|
if (input.model)
|
|
15
18
|
args.push("--model", input.model);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
const effort = input.reasoning && CODEX_EFFORT_LEVELS.includes(input.reasoning)
|
|
20
|
+
? input.reasoning
|
|
21
|
+
: CODEX_DEFAULT_EFFORT;
|
|
22
|
+
args.push("-c", `model_reasoning_effort=${effort}`);
|
|
19
23
|
if (input.effectivePermission === "sandboxed")
|
|
20
24
|
args.push("--sandbox", "read-only");
|
|
21
25
|
else if (input.effectivePermission === "workspace_write")
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { dslog, flushSlog } from "./slog.js";
|
|
2
|
+
function defaultDiagnose(diagnostic) {
|
|
3
|
+
const fields = {
|
|
4
|
+
signal: diagnostic.signal,
|
|
5
|
+
stage: diagnostic.stage,
|
|
6
|
+
active_execution_count: diagnostic.activeExecutionCount,
|
|
7
|
+
active_legacy_count: diagnostic.activeLegacyCount,
|
|
8
|
+
deadline_ms: diagnostic.deadlineMs,
|
|
9
|
+
...(diagnostic.error === undefined ? {} : { error: diagnostic.error }),
|
|
10
|
+
};
|
|
11
|
+
try {
|
|
12
|
+
dslog(diagnostic.stage === "failed" ? "daemon.shutdown_failed" : "daemon.shutdown", `daemon shutdown ${diagnostic.stage}`, { level: diagnostic.stage === "failed" ? "ERROR" : "INFO", ...fields });
|
|
13
|
+
process.stderr.write(`${JSON.stringify({
|
|
14
|
+
level: diagnostic.stage === "failed" ? "ERROR" : "INFO",
|
|
15
|
+
event_type: diagnostic.stage === "failed" ? "daemon.shutdown_failed" : "daemon.shutdown",
|
|
16
|
+
...fields,
|
|
17
|
+
})}\n`);
|
|
18
|
+
}
|
|
19
|
+
catch { /* shutdown diagnostics must never interrupt cleanup */ }
|
|
20
|
+
}
|
|
21
|
+
export async function runServeLifecycle(service, dependencies = {}) {
|
|
22
|
+
const signals = dependencies.signals ?? process;
|
|
23
|
+
const flush = dependencies.flush ?? flushSlog;
|
|
24
|
+
const diagnose = dependencies.diagnose ?? defaultDiagnose;
|
|
25
|
+
let receivedSignal = null;
|
|
26
|
+
let resolveSignal;
|
|
27
|
+
const signalReceived = new Promise((resolve) => { resolveSignal = resolve; });
|
|
28
|
+
const receive = (signal) => {
|
|
29
|
+
if (receivedSignal !== null)
|
|
30
|
+
return;
|
|
31
|
+
receivedSignal = signal;
|
|
32
|
+
resolveSignal(signal);
|
|
33
|
+
};
|
|
34
|
+
const onSigint = () => receive("SIGINT");
|
|
35
|
+
const onSigterm = () => receive("SIGTERM");
|
|
36
|
+
signals.on("SIGINT", onSigint);
|
|
37
|
+
signals.on("SIGTERM", onSigterm);
|
|
38
|
+
try {
|
|
39
|
+
try {
|
|
40
|
+
await service.ready;
|
|
41
|
+
}
|
|
42
|
+
catch (readyError) {
|
|
43
|
+
try {
|
|
44
|
+
await service.stop();
|
|
45
|
+
}
|
|
46
|
+
catch { /* preserve the readiness error */ }
|
|
47
|
+
try {
|
|
48
|
+
await flush();
|
|
49
|
+
}
|
|
50
|
+
catch { /* slog is best effort */ }
|
|
51
|
+
throw readyError;
|
|
52
|
+
}
|
|
53
|
+
const signal = await signalReceived;
|
|
54
|
+
const snapshot = service.shutdownSnapshot();
|
|
55
|
+
diagnose({ signal, stage: "stopping", ...snapshot });
|
|
56
|
+
let stopError;
|
|
57
|
+
try {
|
|
58
|
+
await service.stop();
|
|
59
|
+
diagnose({ signal, stage: "completed", ...snapshot });
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
stopError = error;
|
|
63
|
+
diagnose({
|
|
64
|
+
signal,
|
|
65
|
+
stage: "failed",
|
|
66
|
+
error: error instanceof Error ? error.message : String(error),
|
|
67
|
+
...snapshot,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
await flush();
|
|
72
|
+
}
|
|
73
|
+
catch { /* slog owns its own spool fallback */ }
|
|
74
|
+
if (stopError !== undefined)
|
|
75
|
+
throw stopError;
|
|
76
|
+
return { signal };
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
signals.off("SIGINT", onSigint);
|
|
80
|
+
signals.off("SIGTERM", onSigterm);
|
|
81
|
+
}
|
|
82
|
+
}
|