@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/computer-profile.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { access, chmod, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { dirname, posix, resolve, win32 } from "node:path";
|
|
5
6
|
import { randomUUID } from "node:crypto";
|
|
6
7
|
import { spawn } from "node:child_process";
|
|
7
8
|
import { z } from "zod";
|
|
9
|
+
import { acquireProfileSaveLock, } from "./computer-profile-lock.js";
|
|
10
|
+
import { formatDaemonText } from "./i18n.js";
|
|
8
11
|
const PROFILE_NAME = /^[a-z0-9][a-z0-9_-]{0,47}$/;
|
|
9
12
|
const ServerUrlSchema = z.string().url().max(2048).refine((value) => {
|
|
10
13
|
const url = new URL(value);
|
|
@@ -18,6 +21,11 @@ const ProfileSchema = z.object({
|
|
|
18
21
|
agentsRoot: z.string().min(1).optional(),
|
|
19
22
|
runtimePath: z.string().min(1).max(32768).refine((value) => !value.includes("\0") && !value.includes("\n") && !value.includes("\r"), "runtimePath must be a single line").optional(),
|
|
20
23
|
}).strict();
|
|
24
|
+
const ProfileAgentsRootCandidateSchema = ProfileSchema.pick({
|
|
25
|
+
name: true,
|
|
26
|
+
serverUrl: true,
|
|
27
|
+
agentsRoot: true,
|
|
28
|
+
}).strict();
|
|
21
29
|
const StoredPlainProfileSchema = ProfileSchema;
|
|
22
30
|
const StoredProtectedProfileSchema = ProfileSchema.omit({ machineToken: true }).extend({
|
|
23
31
|
machineTokenProtected: z.object({
|
|
@@ -26,9 +34,93 @@ const StoredProtectedProfileSchema = ProfileSchema.omit({ machineToken: true }).
|
|
|
26
34
|
}).strict(),
|
|
27
35
|
}).strict();
|
|
28
36
|
const StoredProfileSchema = z.union([StoredPlainProfileSchema, StoredProtectedProfileSchema]);
|
|
37
|
+
export const PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE = "Profile '{{profile}}' conflicts with profile '{{conflict}}': both resolve to agents root '{{agentsRoot}}'. Save it with a unique root, for example: {{command}}";
|
|
38
|
+
export class ProfileAgentsRootConflictError extends Error {
|
|
39
|
+
profile;
|
|
40
|
+
conflict;
|
|
41
|
+
agentsRoot;
|
|
42
|
+
command;
|
|
43
|
+
constructor(input) {
|
|
44
|
+
const quote = input.platform === "win32" ? quotePowerShellArgument : quotePosixArgument;
|
|
45
|
+
const pathApi = input.platform === "win32" ? win32 : posix;
|
|
46
|
+
const suggestedRoot = resolveAgentsRoot(pathApi.join(input.userHome, ".crew", `agents-${input.profile}`), input.userHome, input.platform);
|
|
47
|
+
const command = [
|
|
48
|
+
"crew-daemon profile save",
|
|
49
|
+
quote(input.profile),
|
|
50
|
+
"--server-url",
|
|
51
|
+
quote(input.serverUrl),
|
|
52
|
+
"--agents-root",
|
|
53
|
+
quote(suggestedRoot),
|
|
54
|
+
"--token-stdin",
|
|
55
|
+
].join(" ");
|
|
56
|
+
super(formatDaemonText("en", PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE, {
|
|
57
|
+
profile: input.profile,
|
|
58
|
+
conflict: input.conflict,
|
|
59
|
+
agentsRoot: input.agentsRoot,
|
|
60
|
+
command,
|
|
61
|
+
}));
|
|
62
|
+
this.name = "ProfileAgentsRootConflictError";
|
|
63
|
+
this.profile = input.profile;
|
|
64
|
+
this.conflict = input.conflict;
|
|
65
|
+
this.agentsRoot = input.agentsRoot;
|
|
66
|
+
this.command = command;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const quotePosixArgument = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
70
|
+
const quotePowerShellArgument = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
29
71
|
export function daemonHome(env = process.env) {
|
|
30
72
|
return resolve(env.CREW_DAEMON_HOME ?? resolve(homedir(), ".crew/daemon"));
|
|
31
73
|
}
|
|
74
|
+
export function resolveAgentsRoot(configured, userHome = homedir(), platform = process.platform) {
|
|
75
|
+
const pathApi = platform === "win32" ? win32 : posix;
|
|
76
|
+
const normalizedHome = pathApi.resolve(userHome);
|
|
77
|
+
if (configured === undefined)
|
|
78
|
+
return canonicalizeRoot(pathApi.resolve(normalizedHome, ".crew", "agents"), platform);
|
|
79
|
+
const expanded = configured === "~"
|
|
80
|
+
? normalizedHome
|
|
81
|
+
: configured.startsWith("~/") || (platform === "win32" && configured.startsWith("~\\"))
|
|
82
|
+
? pathApi.resolve(normalizedHome, configured.slice(2))
|
|
83
|
+
: configured;
|
|
84
|
+
return canonicalizeRoot(pathApi.resolve(expanded), platform);
|
|
85
|
+
}
|
|
86
|
+
function canonicalizeRoot(path, platform) {
|
|
87
|
+
const compatibleHost = (platform === "win32") === (process.platform === "win32");
|
|
88
|
+
if (!compatibleHost)
|
|
89
|
+
return path;
|
|
90
|
+
try {
|
|
91
|
+
return realpathSync.native(path);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
const code = error.code;
|
|
95
|
+
if (code === "ENOTDIR")
|
|
96
|
+
return path;
|
|
97
|
+
if (code !== "ENOENT")
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
const pathApi = platform === "win32" ? win32 : posix;
|
|
101
|
+
const missingSegments = [];
|
|
102
|
+
let ancestor = path;
|
|
103
|
+
for (;;) {
|
|
104
|
+
const parent = pathApi.dirname(ancestor);
|
|
105
|
+
if (parent === ancestor)
|
|
106
|
+
return path;
|
|
107
|
+
missingSegments.unshift(pathApi.basename(ancestor));
|
|
108
|
+
ancestor = parent;
|
|
109
|
+
try {
|
|
110
|
+
return pathApi.resolve(realpathSync.native(ancestor), ...missingSegments);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
const code = error.code;
|
|
114
|
+
if (code === "ENOTDIR")
|
|
115
|
+
return path;
|
|
116
|
+
if (code !== "ENOENT")
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function comparableAgentsRoot(path, platform) {
|
|
122
|
+
return platform === "win32" ? win32.normalize(path).toLocaleLowerCase("en-US") : path;
|
|
123
|
+
}
|
|
32
124
|
export function validateProfileName(name) {
|
|
33
125
|
if (!PROFILE_NAME.test(name)) {
|
|
34
126
|
throw new Error("profile name must match [a-z0-9][a-z0-9_-]{0,47}");
|
|
@@ -87,29 +179,74 @@ function storageOptions(options) {
|
|
|
87
179
|
...(options.harden ? { harden: options.harden } : {}),
|
|
88
180
|
};
|
|
89
181
|
}
|
|
182
|
+
function attachCleanupError(primaryError, cleanupError) {
|
|
183
|
+
try {
|
|
184
|
+
if (!(primaryError instanceof Error)
|
|
185
|
+
|| !Object.isExtensible(primaryError)
|
|
186
|
+
|| Object.prototype.hasOwnProperty.call(primaryError, "cleanupError"))
|
|
187
|
+
return;
|
|
188
|
+
Object.defineProperty(primaryError, "cleanupError", {
|
|
189
|
+
configurable: true,
|
|
190
|
+
value: cleanupError,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// Preserve the primary error even when it cannot accept diagnostics.
|
|
195
|
+
}
|
|
196
|
+
}
|
|
90
197
|
export async function saveProfile(input, home = daemonHome(), options = {}) {
|
|
91
198
|
const profile = ProfileSchema.parse({ version: 1, ...input });
|
|
92
199
|
const configured = storageOptions(options);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
200
|
+
const release = await acquireProfileSaveLock(resolve(home, "profiles"), {
|
|
201
|
+
...(options.lockTimeoutMs === undefined ? {} : { timeoutMs: options.lockTimeoutMs }),
|
|
202
|
+
...(options.lockRetryMs === undefined ? {} : { retryMs: options.lockRetryMs }),
|
|
203
|
+
...(options.lockNow === undefined ? {} : { now: options.lockNow }),
|
|
204
|
+
...(options.lockWait === undefined ? {} : { wait: options.lockWait }),
|
|
205
|
+
...(options.lockProcessController === undefined
|
|
206
|
+
? {}
|
|
207
|
+
: { processController: options.lockProcessController }),
|
|
208
|
+
...(options.lockHooks === undefined ? {} : { hooks: options.lockHooks }),
|
|
209
|
+
});
|
|
210
|
+
let bodyFailed = false;
|
|
211
|
+
let bodyError;
|
|
212
|
+
try {
|
|
213
|
+
await assertProfileAgentsRootUnique(profile, home, options.userHome ?? homedir(), options);
|
|
214
|
+
if (configured.platform === "win32") {
|
|
215
|
+
if (!configured.protector)
|
|
216
|
+
throw new Error("Windows profile storage requires CurrentUser DPAPI");
|
|
217
|
+
if (!configured.harden)
|
|
218
|
+
throw new Error("Windows profile storage requires ACL hardening");
|
|
219
|
+
const ciphertext = await configured.protector.protect(profile.machineToken);
|
|
220
|
+
if (!ciphertext)
|
|
221
|
+
throw new Error("Windows DPAPI returned an empty ciphertext");
|
|
222
|
+
const stored = StoredProtectedProfileSchema.parse({
|
|
223
|
+
version: 1,
|
|
224
|
+
name: profile.name,
|
|
225
|
+
serverUrl: profile.serverUrl,
|
|
226
|
+
...(profile.agentsRoot ? { agentsRoot: profile.agentsRoot } : {}),
|
|
227
|
+
...(profile.runtimePath ? { runtimePath: profile.runtimePath } : {}),
|
|
228
|
+
machineTokenProtected: { scheme: "dpapi-current-user", ciphertext },
|
|
229
|
+
});
|
|
230
|
+
await atomicPrivateWrite(profilePath(profile.name, home), `${JSON.stringify(stored, null, 2)}\n`, configured.harden);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
await atomicPrivateWrite(profilePath(profile.name, home), `${JSON.stringify(profile, null, 2)}\n`);
|
|
234
|
+
}
|
|
110
235
|
}
|
|
111
|
-
|
|
112
|
-
|
|
236
|
+
catch (error) {
|
|
237
|
+
bodyFailed = true;
|
|
238
|
+
bodyError = error;
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
try {
|
|
243
|
+
await release();
|
|
244
|
+
}
|
|
245
|
+
catch (cleanupError) {
|
|
246
|
+
if (!bodyFailed)
|
|
247
|
+
throw cleanupError;
|
|
248
|
+
attachCleanupError(bodyError, cleanupError);
|
|
249
|
+
}
|
|
113
250
|
}
|
|
114
251
|
return profile;
|
|
115
252
|
}
|
|
@@ -161,6 +298,38 @@ export async function listProfiles(home = daemonHome()) {
|
|
|
161
298
|
throw error;
|
|
162
299
|
}
|
|
163
300
|
}
|
|
301
|
+
export async function inspectProfileAgentsRoot(profile, home = daemonHome(), userHome = homedir(), options = {}) {
|
|
302
|
+
const platform = storageOptions(options).platform;
|
|
303
|
+
const agentsRoot = resolveAgentsRoot(profile.agentsRoot, userHome, platform);
|
|
304
|
+
const comparableRoot = comparableAgentsRoot(agentsRoot, platform);
|
|
305
|
+
const names = (await listProfiles(home)).filter((name) => name !== profile.name);
|
|
306
|
+
const profiles = await Promise.all(names.map((name) => loadProfile(name, home, options)));
|
|
307
|
+
return {
|
|
308
|
+
agentsRoot,
|
|
309
|
+
duplicateProfiles: profiles
|
|
310
|
+
.filter((candidate) => comparableAgentsRoot(resolveAgentsRoot(candidate.agentsRoot, userHome, platform), platform) === comparableRoot)
|
|
311
|
+
.map((candidate) => candidate.name),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
export async function assertProfileAgentsRootUnique(profile, home = daemonHome(), userHome = homedir(), options = {}) {
|
|
315
|
+
const candidate = ProfileAgentsRootCandidateSchema.parse({
|
|
316
|
+
name: profile.name,
|
|
317
|
+
serverUrl: profile.serverUrl,
|
|
318
|
+
...(profile.agentsRoot === undefined ? {} : { agentsRoot: profile.agentsRoot }),
|
|
319
|
+
});
|
|
320
|
+
const inspection = await inspectProfileAgentsRoot(candidate, home, userHome, options);
|
|
321
|
+
const conflict = inspection.duplicateProfiles[0];
|
|
322
|
+
if (conflict === undefined)
|
|
323
|
+
return;
|
|
324
|
+
throw new ProfileAgentsRootConflictError({
|
|
325
|
+
profile: candidate.name,
|
|
326
|
+
conflict,
|
|
327
|
+
agentsRoot: inspection.agentsRoot,
|
|
328
|
+
serverUrl: candidate.serverUrl,
|
|
329
|
+
userHome,
|
|
330
|
+
platform: storageOptions(options).platform,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
164
333
|
export async function removeProfile(name, home = daemonHome()) {
|
|
165
334
|
await rm(profilePath(name, home), { force: true });
|
|
166
335
|
}
|
package/dist/config.js
CHANGED
|
@@ -4,6 +4,7 @@ import { dirname, resolve } from "node:path";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { detectDaemonLang, translateDaemon } from "./i18n.js";
|
|
7
|
+
import { resolveAgentsRoot } from "./computer-profile.js";
|
|
7
8
|
export const DEFAULT_EXECUTION_LIMITS = Object.freeze({
|
|
8
9
|
maxPromptBytes: 256_000,
|
|
9
10
|
maxTimeoutMs: 3_600_000,
|
|
@@ -65,7 +66,7 @@ export function loadConfig(env = process.env) {
|
|
|
65
66
|
return {
|
|
66
67
|
serverUrl,
|
|
67
68
|
machineToken,
|
|
68
|
-
agentsRoot: env.CREW_AGENTS_ROOT
|
|
69
|
+
agentsRoot: resolveAgentsRoot(env.CREW_AGENTS_ROOT, homedir()),
|
|
69
70
|
cliPath: env.CREW_CLI_PATH ?? defaultCliPath(),
|
|
70
71
|
runtimeBin: env.CREW_RUNTIME ?? "claude",
|
|
71
72
|
dangerous: env.CREW_RUNTIME_SAFE !== "1", // 默认开启 (headless agent 在自有 workspace 内运行)
|
package/dist/console.js
CHANGED
|
@@ -5,14 +5,24 @@
|
|
|
5
5
|
* 做**原文透传**——保留思考文本、工具调用、工具返回正文,尽量还原原生 claude CLI 的观感。
|
|
6
6
|
* 纯函数,无 IO,完整单测。
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* 除纯文本 text 外,可识别的工具调用(Edit/Write/Bash/TodoWrite/ExitPlanMode/Task…)
|
|
9
|
+
* 会附带结构化 payload(diff/命令/todo 清单/计划…),供前端做富渲染(红绿 diff、清单勾选);
|
|
10
|
+
* 前端不识别 payload.kind 时降级为纯文本行,协议向后兼容。
|
|
11
|
+
*
|
|
12
|
+
* 覆盖三种 runtime 的流:claude(stream-json)、codex(exec --json 的 item.* 事件)、
|
|
13
|
+
* kimi(OpenAI 消息风格行)。
|
|
10
14
|
*/
|
|
11
15
|
import { detectDaemonLang, translateDaemon } from "./i18n.js";
|
|
12
16
|
/** 单条工具返回正文上限(超出截断并标注),避免单条把终端/DB 撑爆。 */
|
|
13
17
|
export const TOOL_RESULT_CAP = 4000;
|
|
14
18
|
/** 工具输入摘要上限(标题行那一段)。 */
|
|
15
19
|
const TOOL_INPUT_CAP = 160;
|
|
20
|
+
/** payload 里单段文本(diff 单侧/plan/命令)上限:富渲染要保留足够内容,但不能无界。 */
|
|
21
|
+
export const PAYLOAD_TEXT_CAP = 4000;
|
|
22
|
+
/** todo 清单条数上限。 */
|
|
23
|
+
const TODOS_MAX = 50;
|
|
24
|
+
/** MultiEdit 拆分出的 diff 块上限。 */
|
|
25
|
+
const MULTI_EDIT_MAX = 20;
|
|
16
26
|
/** kimi 工具 arguments(JSON 字符串)容错解析为对象;失败返回 undefined。 */
|
|
17
27
|
function parseKimiToolArgs(args) {
|
|
18
28
|
if (!args)
|
|
@@ -25,19 +35,106 @@ function parseKimiToolArgs(args) {
|
|
|
25
35
|
return undefined;
|
|
26
36
|
}
|
|
27
37
|
}
|
|
28
|
-
/** 把工具输入压成一行摘要:Bash 取 command
|
|
38
|
+
/** 把工具输入压成一行摘要:Bash 取 command,文件类工具取路径,其它取首个字符串字段或紧凑 JSON。 */
|
|
29
39
|
function summarizeToolInput(name, input) {
|
|
30
40
|
if (!input)
|
|
31
41
|
return `⏺ ${name}`;
|
|
32
42
|
if (name === "Bash" && typeof input.command === "string") {
|
|
33
43
|
return `⏺ Bash(${clip(input.command.replace(/\s+/g, " "), TOOL_INPUT_CAP)})`;
|
|
34
44
|
}
|
|
45
|
+
// 文件编辑类:old/new 全文在 payload 里富渲染,摘要只报文件路径,不把整段代码挤进标题行。
|
|
46
|
+
if ((name === "Edit" || name === "Write" || name === "MultiEdit") && typeof input.file_path === "string") {
|
|
47
|
+
return `⏺ ${name}(${clip(input.file_path, TOOL_INPUT_CAP)})`;
|
|
48
|
+
}
|
|
49
|
+
if (name === "TodoWrite" && Array.isArray(input.todos)) {
|
|
50
|
+
return `⏺ TodoWrite(${input.todos.length})`;
|
|
51
|
+
}
|
|
52
|
+
if (name === "ExitPlanMode")
|
|
53
|
+
return `⏺ ExitPlanMode`;
|
|
35
54
|
const entries = Object.entries(input);
|
|
36
55
|
const head = entries
|
|
37
56
|
.map(([k, v]) => `${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`)
|
|
38
57
|
.join(", ");
|
|
39
58
|
return `⏺ ${name}(${clip(head.replace(/\s+/g, " "), TOOL_INPUT_CAP)})`;
|
|
40
59
|
}
|
|
60
|
+
const TODO_STATUSES = new Set(["pending", "in_progress", "completed"]);
|
|
61
|
+
/** claude TodoWrite 的 todos 数组归一为 ConsoleTodo[](脏数据条目丢弃)。 */
|
|
62
|
+
function normalizeTodos(raw) {
|
|
63
|
+
if (!Array.isArray(raw))
|
|
64
|
+
return [];
|
|
65
|
+
const out = [];
|
|
66
|
+
for (const t of raw.slice(0, TODOS_MAX)) {
|
|
67
|
+
if (!t || typeof t !== "object")
|
|
68
|
+
continue;
|
|
69
|
+
const rec = t;
|
|
70
|
+
const text = typeof rec.content === "string" && rec.content.trim()
|
|
71
|
+
? rec.content.trim()
|
|
72
|
+
: typeof rec.subject === "string" ? rec.subject.trim() : "";
|
|
73
|
+
if (!text)
|
|
74
|
+
continue;
|
|
75
|
+
const status = typeof rec.status === "string" && TODO_STATUSES.has(rec.status)
|
|
76
|
+
? rec.status
|
|
77
|
+
: "pending";
|
|
78
|
+
out.push({ text: clip(text, TOOL_INPUT_CAP), status });
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
function diffPayload(file, oldText, newText) {
|
|
83
|
+
return {
|
|
84
|
+
kind: "diff",
|
|
85
|
+
file,
|
|
86
|
+
oldText: clip(typeof oldText === "string" ? oldText : "", PAYLOAD_TEXT_CAP),
|
|
87
|
+
newText: clip(typeof newText === "string" ? newText : "", PAYLOAD_TEXT_CAP),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** 已识别工具 → 结构化 payload;其余返回 undefined(仅纯文本摘要)。 */
|
|
91
|
+
function toolPayload(name, input) {
|
|
92
|
+
if (!input)
|
|
93
|
+
return undefined;
|
|
94
|
+
if (name === "Bash" && typeof input.command === "string") {
|
|
95
|
+
return { kind: "command", command: clip(input.command, PAYLOAD_TEXT_CAP) };
|
|
96
|
+
}
|
|
97
|
+
if (name === "Edit" && typeof input.file_path === "string") {
|
|
98
|
+
return diffPayload(input.file_path, input.old_string, input.new_string);
|
|
99
|
+
}
|
|
100
|
+
if (name === "Write" && typeof input.file_path === "string" && typeof input.content === "string") {
|
|
101
|
+
return diffPayload(input.file_path, "", input.content);
|
|
102
|
+
}
|
|
103
|
+
if (name === "TodoWrite") {
|
|
104
|
+
const todos = normalizeTodos(input.todos);
|
|
105
|
+
if (todos.length)
|
|
106
|
+
return { kind: "todos", todos };
|
|
107
|
+
}
|
|
108
|
+
if (name === "ExitPlanMode" && typeof input.plan === "string" && input.plan.trim()) {
|
|
109
|
+
return { kind: "plan", plan: clip(input.plan.trim(), PAYLOAD_TEXT_CAP) };
|
|
110
|
+
}
|
|
111
|
+
if ((name === "Task" || name === "Agent") && typeof input.description === "string" && input.description.trim()) {
|
|
112
|
+
return { kind: "subagent", description: clip(input.description.trim(), TOOL_INPUT_CAP) };
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
/** 一个 tool_use 块 → 1..N 条 console 行(MultiEdit 拆成每处编辑一块 diff)。 */
|
|
117
|
+
function toolUseChunks(name, input) {
|
|
118
|
+
if (name === "MultiEdit" && input && typeof input.file_path === "string" && Array.isArray(input.edits)) {
|
|
119
|
+
const file = input.file_path;
|
|
120
|
+
const chunks = [];
|
|
121
|
+
for (const ed of input.edits.slice(0, MULTI_EDIT_MAX)) {
|
|
122
|
+
if (!ed || typeof ed !== "object")
|
|
123
|
+
continue;
|
|
124
|
+
const rec = ed;
|
|
125
|
+
chunks.push({
|
|
126
|
+
stream: "tool",
|
|
127
|
+
text: `⏺ Edit(${clip(file, TOOL_INPUT_CAP)})`,
|
|
128
|
+
payload: diffPayload(file, rec.old_string, rec.new_string),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (chunks.length)
|
|
132
|
+
return chunks;
|
|
133
|
+
}
|
|
134
|
+
const text = summarizeToolInput(name, input);
|
|
135
|
+
const payload = toolPayload(name, input);
|
|
136
|
+
return [payload ? { stream: "tool", text, payload } : { stream: "tool", text }];
|
|
137
|
+
}
|
|
41
138
|
/** 把 tool_result 的 content(string | block[])抽成纯文本。 */
|
|
42
139
|
function extractToolResult(content) {
|
|
43
140
|
if (typeof content === "string")
|
|
@@ -53,6 +150,72 @@ function extractToolResult(content) {
|
|
|
53
150
|
function clip(s, cap) {
|
|
54
151
|
return s.length > cap ? s.slice(0, cap) + `… (+${s.length - cap})` : s;
|
|
55
152
|
}
|
|
153
|
+
/** codex exec --json 的 item.completed → console 行(agent_message/reasoning/命令/文件变更/todo…)。 */
|
|
154
|
+
function codexItemChunks(item, td) {
|
|
155
|
+
if (item.type === "agent_message" && item.text?.trim()) {
|
|
156
|
+
return [{ stream: "text", text: item.text.trim() }];
|
|
157
|
+
}
|
|
158
|
+
if (item.type === "reasoning" && item.text?.trim()) {
|
|
159
|
+
return [{ stream: "thinking", text: item.text.trim() }];
|
|
160
|
+
}
|
|
161
|
+
if (item.type === "command_execution" && item.command?.trim()) {
|
|
162
|
+
const command = item.command.trim();
|
|
163
|
+
const out = [{
|
|
164
|
+
stream: "tool",
|
|
165
|
+
text: `⏺ Bash(${clip(command.replace(/\s+/g, " "), TOOL_INPUT_CAP)})`,
|
|
166
|
+
payload: {
|
|
167
|
+
kind: "command",
|
|
168
|
+
command: clip(command, PAYLOAD_TEXT_CAP),
|
|
169
|
+
...(typeof item.exit_code === "number" ? { exitCode: item.exit_code } : {}),
|
|
170
|
+
},
|
|
171
|
+
}];
|
|
172
|
+
const output = item.aggregated_output?.trim();
|
|
173
|
+
if (output)
|
|
174
|
+
out.push({ stream: "tool_result", text: clip(output, TOOL_RESULT_CAP) });
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
if (item.type === "file_change" && Array.isArray(item.changes)) {
|
|
178
|
+
const files = item.changes
|
|
179
|
+
.filter((c) => !!c && typeof c === "object")
|
|
180
|
+
.map((c) => ({
|
|
181
|
+
path: typeof c.path === "string" ? c.path : "",
|
|
182
|
+
change: typeof c.kind === "string" ? c.kind : "update",
|
|
183
|
+
}))
|
|
184
|
+
.filter((f) => f.path);
|
|
185
|
+
if (!files.length)
|
|
186
|
+
return [];
|
|
187
|
+
return [{
|
|
188
|
+
stream: "tool",
|
|
189
|
+
text: `⏺ ${td("Files changed")}: ${clip(files.map((f) => f.path).join(", "), TOOL_INPUT_CAP)}`,
|
|
190
|
+
payload: { kind: "files", files },
|
|
191
|
+
}];
|
|
192
|
+
}
|
|
193
|
+
if (item.type === "todo_list" && Array.isArray(item.items)) {
|
|
194
|
+
const todos = item.items
|
|
195
|
+
.filter((t) => !!t && typeof t === "object")
|
|
196
|
+
.slice(0, TODOS_MAX)
|
|
197
|
+
.map((t) => ({
|
|
198
|
+
text: clip(typeof t.text === "string" ? t.text.trim() : "", TOOL_INPUT_CAP),
|
|
199
|
+
status: (t.completed === true ? "completed" : "pending"),
|
|
200
|
+
}))
|
|
201
|
+
.filter((t) => t.text);
|
|
202
|
+
if (!todos.length)
|
|
203
|
+
return [];
|
|
204
|
+
const done = todos.filter((t) => t.status === "completed").length;
|
|
205
|
+
return [{ stream: "tool", text: `⏺ Todos(${done}/${todos.length})`, payload: { kind: "todos", todos } }];
|
|
206
|
+
}
|
|
207
|
+
if (item.type === "web_search" && item.query?.trim()) {
|
|
208
|
+
return [{ stream: "tool", text: `⏺ WebSearch(${clip(item.query.trim(), TOOL_INPUT_CAP)})` }];
|
|
209
|
+
}
|
|
210
|
+
if (item.type === "mcp_tool_call" && (item.tool || item.server)) {
|
|
211
|
+
const label = [item.server, item.tool].filter(Boolean).join(".");
|
|
212
|
+
return [{ stream: "tool", text: `⏺ MCP(${clip(label, TOOL_INPUT_CAP)})` }];
|
|
213
|
+
}
|
|
214
|
+
if (item.type === "error" && item.message?.trim()) {
|
|
215
|
+
return [{ stream: "error", text: item.message.trim() }];
|
|
216
|
+
}
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
56
219
|
/** 把一个 stream-json 事件转成 0..N 条 console 行(完全透传)。 */
|
|
57
220
|
export function toConsoleLines(event) {
|
|
58
221
|
const e = (event ?? {});
|
|
@@ -72,13 +235,16 @@ export function toConsoleLines(event) {
|
|
|
72
235
|
return [{ stream: "system", text: `● ${td("Claude session started")}` }];
|
|
73
236
|
}
|
|
74
237
|
if (e.type === "thread.started") {
|
|
75
|
-
return [{ stream: "system", text: "
|
|
238
|
+
return [{ stream: "system", text: `● ${td("Codex session started")}` }];
|
|
76
239
|
}
|
|
77
|
-
if (e.type === "item.completed" && e.item
|
|
78
|
-
return
|
|
240
|
+
if (e.type === "item.completed" && e.item) {
|
|
241
|
+
return codexItemChunks(e.item, td);
|
|
79
242
|
}
|
|
80
243
|
if (e.type === "turn.completed") {
|
|
81
|
-
return [{ stream: "result", text: "
|
|
244
|
+
return [{ stream: "result", text: td("Run finished") }];
|
|
245
|
+
}
|
|
246
|
+
if (e.type === "turn.failed") {
|
|
247
|
+
return [{ stream: "error", text: e.error?.message?.trim() || td("Run failed") }];
|
|
82
248
|
}
|
|
83
249
|
if (e.type === "result") {
|
|
84
250
|
const text = e.result?.trim() || (e.is_error ? td("Run failed") : td("Run finished"));
|
|
@@ -94,7 +260,7 @@ export function toConsoleLines(event) {
|
|
|
94
260
|
out.push({ stream: "text", text: block.text.trim() });
|
|
95
261
|
}
|
|
96
262
|
else if (block.type === "tool_use" && block.name) {
|
|
97
|
-
out.push(
|
|
263
|
+
out.push(...toolUseChunks(block.name, block.input));
|
|
98
264
|
}
|
|
99
265
|
}
|
|
100
266
|
return out;
|
|
@@ -119,7 +285,7 @@ export function toConsoleLines(event) {
|
|
|
119
285
|
for (const call of Array.isArray(e.tool_calls) ? e.tool_calls : []) {
|
|
120
286
|
const name = call.function?.name;
|
|
121
287
|
if (name)
|
|
122
|
-
out.push(
|
|
288
|
+
out.push(...toolUseChunks(name, parseKimiToolArgs(call.function?.arguments)));
|
|
123
289
|
}
|
|
124
290
|
return out;
|
|
125
291
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { formatDaemonText, } from "./i18n.js";
|
|
2
|
+
const PROFILE_MESSAGE = "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}}'.";
|
|
3
|
+
const MANUAL_MESSAGE = "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>'.";
|
|
4
|
+
export class DaemonAlreadyRunningError extends Error {
|
|
5
|
+
ownerPid;
|
|
6
|
+
agentsRoot;
|
|
7
|
+
journalPath;
|
|
8
|
+
serverUrl;
|
|
9
|
+
profileName;
|
|
10
|
+
constructor(details) {
|
|
11
|
+
super(`Daemon is already running (PID=${details.ownerPid})`, { cause: details.cause });
|
|
12
|
+
this.name = "DaemonAlreadyRunningError";
|
|
13
|
+
this.ownerPid = details.ownerPid;
|
|
14
|
+
this.agentsRoot = details.agentsRoot;
|
|
15
|
+
this.journalPath = details.journalPath;
|
|
16
|
+
this.serverUrl = details.serverUrl;
|
|
17
|
+
if (details.profileName !== undefined)
|
|
18
|
+
this.profileName = details.profileName;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function formatDaemonStartupError(error, lang) {
|
|
22
|
+
if (!(error instanceof DaemonAlreadyRunningError))
|
|
23
|
+
return null;
|
|
24
|
+
return formatDaemonText(lang, error.profileName === undefined ? MANUAL_MESSAGE : PROFILE_MESSAGE, {
|
|
25
|
+
ownerPid: error.ownerPid,
|
|
26
|
+
agentsRoot: error.agentsRoot,
|
|
27
|
+
journalPath: error.journalPath,
|
|
28
|
+
...(error.profileName === undefined ? {} : { profileName: error.profileName }),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -1,11 +1,44 @@
|
|
|
1
|
+
import { isJobObjectSupported } from "./win32-job-object.js";
|
|
2
|
+
/** 灰度开关:默认关。只有显式打开且运行时探测成功,win32 才翻为 supported。 */
|
|
3
|
+
const GRAYSCALE_ENV = "CREW_WINDOWS_JOB_OBJECT";
|
|
4
|
+
function grayscaleEnabled() {
|
|
5
|
+
const flag = process.env[GRAYSCALE_ENV];
|
|
6
|
+
return flag === "1" || flag === "true";
|
|
7
|
+
}
|
|
8
|
+
let cachedProbe;
|
|
9
|
+
/**
|
|
10
|
+
* 默认探测:灰度关 → 一律 false(行为与今日一致)。灰度开 → 同步探测 koffi/kernel32 一次并缓存。
|
|
11
|
+
* 同步实现(createRequire),因此 `executionBackendCapability` 保持同步,不波及 serve.ts 的同步调用链。
|
|
12
|
+
*/
|
|
13
|
+
function defaultJobObjectProbe() {
|
|
14
|
+
if (!grayscaleEnabled())
|
|
15
|
+
return false;
|
|
16
|
+
if (cachedProbe === undefined)
|
|
17
|
+
cachedProbe = isJobObjectSupported();
|
|
18
|
+
return cachedProbe;
|
|
19
|
+
}
|
|
20
|
+
const WINDOWS_UNAVAILABLE_REASON = "protocol-v1 is disabled until a Windows Job Object backend owns every runtime process";
|
|
1
21
|
/** Durable execution needs ownership that survives daemon crashes, not only a best-effort kill. */
|
|
2
|
-
export function executionBackendCapability(platform = process.platform) {
|
|
22
|
+
export function executionBackendCapability(platform = process.platform, probe = defaultJobObjectProbe) {
|
|
3
23
|
if (platform === "win32") {
|
|
24
|
+
if (probe())
|
|
25
|
+
return { supported: true, backend: "windows-job-object" };
|
|
4
26
|
return {
|
|
5
27
|
supported: false,
|
|
6
28
|
backend: "windows-job-object-unavailable",
|
|
7
|
-
reason:
|
|
29
|
+
reason: WINDOWS_UNAVAILABLE_REASON,
|
|
8
30
|
};
|
|
9
31
|
}
|
|
10
32
|
return { supported: true, backend: "posix-process-group" };
|
|
11
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* supervisor child 是否应通过 Job Object 拥有 runtime 树。仅当 durable + win32 + Job Object 后端已选中时为真。
|
|
36
|
+
* legacy(process-lifetime)恒为 false —— 保证灰度关/koffi 不可用时 Windows legacy 执行完全不碰 Job,
|
|
37
|
+
* 维持今日的 taskkill 语义与 fail-closed 降级承诺。
|
|
38
|
+
*/
|
|
39
|
+
export function ownsRuntimeViaJobObject(ownershipMode, platform = process.platform, probe) {
|
|
40
|
+
if (ownershipMode !== "durable")
|
|
41
|
+
return false;
|
|
42
|
+
const backend = executionBackendCapability(platform, probe);
|
|
43
|
+
return backend.supported && backend.backend === "windows-job-object";
|
|
44
|
+
}
|