@nowcrew/daemon 0.6.16 → 0.6.17
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/agent-ability/runtime-context.js +7 -1
- package/dist/atomic-private-write.js +54 -1
- package/dist/automatic-install-target.js +40 -11
- package/dist/console.js +9 -0
- package/dist/control-plane-url.js +2 -2
- package/dist/daemon-migration-controller.js +198 -0
- package/dist/daemon-migration-wiring.js +22 -0
- package/dist/daemon-update-eligibility.js +1 -1
- package/dist/directory-projection-identity.js +32 -0
- package/dist/directory-projection.js +922 -0
- package/dist/execution-protocol.js +78 -11
- package/dist/execution-runner.js +50 -2
- package/dist/i18n.js +1 -0
- package/dist/local-execution-prompt.js +57 -0
- package/dist/local-executor.js +99 -40
- package/dist/machine-info.js +45 -9
- package/dist/normalize.js +5 -0
- package/dist/profile-layout.js +41 -0
- package/dist/project-skills/controller.js +74 -14
- package/dist/project-skills/execution-adapter.js +11 -0
- package/dist/project-skills/initialized-reconciler.js +20 -0
- package/dist/project-skills/projection-set-switch.js +419 -0
- package/dist/project-skills/projection-state-domain.js +153 -0
- package/dist/project-skills/projection-state-store.js +841 -0
- package/dist/project-skills/projection-state-transaction.js +318 -0
- package/dist/project-skills/projection-state.js +3 -0
- package/dist/project-skills/reconciler.js +299 -68
- package/dist/project-skills/runtime-warning.js +6 -0
- package/dist/project-skills/scanner.js +30 -1
- package/dist/project-skills/types.js +9 -0
- package/dist/project-workspaces/resolver.js +179 -0
- package/dist/project-workspaces/types.js +1 -0
- package/dist/prompt.js +40 -0
- package/dist/runtimes/claude.js +235 -4
- package/dist/runtimes/codex-app-server-runner.js +92 -23
- package/dist/runtimes/codex-contract.js +123 -0
- package/dist/runtimes/codex.js +2 -0
- package/dist/serve.js +31 -17
- package/dist/session.js +3 -0
- package/dist/supervised-runtime.js +12 -4
- package/dist/workspace.js +14 -5
- package/package.json +1 -1
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { once } from "node:events";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
2
3
|
import { createInterface } from "node:readline";
|
|
3
4
|
import { parseArgs } from "node:util";
|
|
4
5
|
import { pathToFileURL } from "node:url";
|
|
5
6
|
import spawn from "cross-spawn";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
import { signalSupervisorTree } from "../execution-supervisor.js";
|
|
9
|
+
import { assertCodexNativeWorkspaceVersion, assertCodexProjectWorkspaceResponse, assertCodexSkillsListResponse, boundedCodexPlan, codexProjectWorkspaceUnsupported, } from "./codex-contract.js";
|
|
8
10
|
import { startFirstProgressWatchdog } from "./progress-watchdog.js";
|
|
9
11
|
const RunnerInputSchema = z.object({
|
|
10
12
|
systemPrompt: z.string().min(1),
|
|
@@ -15,6 +17,9 @@ const RunnerInputSchema = z.object({
|
|
|
15
17
|
sessionId: z.string().min(1).optional(),
|
|
16
18
|
imagePaths: z.array(z.string().min(1)).optional(),
|
|
17
19
|
projectRootMarkers: z.array(z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/u)).max(16).optional(),
|
|
20
|
+
cwd: z.string().min(1).optional(),
|
|
21
|
+
workspaceRoots: z.array(z.string().min(1)).optional(),
|
|
22
|
+
skillRoots: z.array(z.string().min(1)).optional(),
|
|
18
23
|
resume: z.boolean(),
|
|
19
24
|
}).strict();
|
|
20
25
|
const RPC_TIMEOUT_MS = 30_000;
|
|
@@ -65,12 +70,8 @@ export function codexRpcTimeoutMs(method) {
|
|
|
65
70
|
return method === "initialize" ? INITIALIZE_RPC_TIMEOUT_MS : RPC_TIMEOUT_MS;
|
|
66
71
|
}
|
|
67
72
|
function safeErrorMessage(error, secrets) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (secret)
|
|
71
|
-
message = message.replaceAll(secret, "[prompt redacted]");
|
|
72
|
-
}
|
|
73
|
-
return message.slice(0, ERROR_MESSAGE_CAP);
|
|
73
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
74
|
+
return sanitizeCodexDiagnostic(message, secrets).slice(0, ERROR_MESSAGE_CAP);
|
|
74
75
|
}
|
|
75
76
|
function sensitiveEnvironmentValues(env) {
|
|
76
77
|
return Object.entries(env)
|
|
@@ -79,13 +80,34 @@ function sensitiveEnvironmentValues(env) {
|
|
|
79
80
|
.map(([, value]) => value);
|
|
80
81
|
}
|
|
81
82
|
function redactionSecrets(input, env) {
|
|
82
|
-
const
|
|
83
|
+
const pathValues = [
|
|
84
|
+
process.cwd(),
|
|
85
|
+
...(input.imagePaths ?? []),
|
|
86
|
+
...(input.cwd === undefined ? [] : [input.cwd]),
|
|
87
|
+
...(input.workspaceRoots ?? []),
|
|
88
|
+
...(input.skillRoots ?? []),
|
|
89
|
+
];
|
|
90
|
+
const realpathAliases = pathValues.flatMap((value) => {
|
|
91
|
+
try {
|
|
92
|
+
return [realpathSync.native(value)];
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
const values = [
|
|
99
|
+
input.systemPrompt,
|
|
100
|
+
input.wakePrompt,
|
|
101
|
+
...pathValues,
|
|
102
|
+
...realpathAliases,
|
|
103
|
+
...sensitiveEnvironmentValues(env),
|
|
104
|
+
];
|
|
83
105
|
return [...new Set(values.flatMap((value) => [
|
|
84
106
|
value,
|
|
85
107
|
...value.split(/\r?\n/).map((line) => line.trim()),
|
|
86
108
|
]).filter(Boolean))];
|
|
87
109
|
}
|
|
88
|
-
export function
|
|
110
|
+
export function sanitizeCodexDiagnostic(text, secrets) {
|
|
89
111
|
let redacted = text;
|
|
90
112
|
for (const secret of [...secrets].sort((left, right) => right.length - left.length)) {
|
|
91
113
|
if (secret)
|
|
@@ -93,9 +115,13 @@ export function redactCodexStderr(text, secrets) {
|
|
|
93
115
|
}
|
|
94
116
|
return redacted
|
|
95
117
|
.replace(/(Bearer\s+)[^\s"']+/gi, "$1[redacted]")
|
|
96
|
-
.replace(/\bsk-[A-Za-z0-9_-]{6,}\b/
|
|
97
|
-
.replace(/((?:api[_-]?key|access[_-]?token|auth(?:orization)?|password|secret)\s*[=:]\s*)[^\s"']+/gi, "$1[redacted]")
|
|
118
|
+
.replace(/\bsk[-_][A-Za-z0-9_-]{6,}\b/gi, "[redacted]")
|
|
119
|
+
.replace(/((?:api[_-]?key|access[_-]?token|auth[_-]?token|token|auth(?:orization)?|password|secret)\s*[=:]\s*)[^\s"']+/gi, "$1[redacted]")
|
|
120
|
+
.replace(/(^|[\s"'(=:[{])(?:[A-Za-z]:[\\/]|\\\\)[^\s"'`<>|]+/g, "$1[redacted-path]")
|
|
121
|
+
.replace(/(^|[\s"'(=:[{])\/[^\s"'`<>|]+/g, "$1[redacted-path]");
|
|
98
122
|
}
|
|
123
|
+
/** @deprecated Use the shared diagnostic sanitizer for every runner-owned error surface. */
|
|
124
|
+
export const redactCodexStderr = sanitizeCodexDiagnostic;
|
|
99
125
|
class StderrCapture {
|
|
100
126
|
secrets;
|
|
101
127
|
tail = "";
|
|
@@ -110,7 +136,7 @@ class StderrCapture {
|
|
|
110
136
|
redactedTail() {
|
|
111
137
|
const pending = this.omittingLine
|
|
112
138
|
? STDERR_LINE_OMITTED
|
|
113
|
-
:
|
|
139
|
+
: sanitizeCodexDiagnostic(this.pendingLine, this.secrets);
|
|
114
140
|
return `${this.tail}${pending}`.slice(-STDERR_TAIL_CAP).trim();
|
|
115
141
|
}
|
|
116
142
|
append(chunk) {
|
|
@@ -143,7 +169,7 @@ class StderrCapture {
|
|
|
143
169
|
this.omittingLine = false;
|
|
144
170
|
}
|
|
145
171
|
emit(text) {
|
|
146
|
-
const redacted =
|
|
172
|
+
const redacted = sanitizeCodexDiagnostic(text, this.secrets);
|
|
147
173
|
process.stderr.write(redacted);
|
|
148
174
|
this.tail = `${this.tail}${redacted}`.slice(-STDERR_TAIL_CAP);
|
|
149
175
|
}
|
|
@@ -177,10 +203,19 @@ export function codexApprovalResponse(method, permission) {
|
|
|
177
203
|
}
|
|
178
204
|
/** Translate app-server v2 notifications into the daemon's existing runtime event contract. */
|
|
179
205
|
export function mapCodexNotification(method, params) {
|
|
180
|
-
const value = params;
|
|
206
|
+
const value = (params !== null && typeof params === "object" ? params : {});
|
|
181
207
|
if (method === "thread/started" && value.thread?.id) {
|
|
182
208
|
return [{ type: "thread.started", thread_id: value.thread.id }];
|
|
183
209
|
}
|
|
210
|
+
const plan = method === "turn/plan/updated" ? boundedCodexPlan(value.plan) : null;
|
|
211
|
+
if (plan !== null && typeof value.threadId === "string" && typeof value.turnId === "string") {
|
|
212
|
+
return [{
|
|
213
|
+
type: "turn.plan.updated",
|
|
214
|
+
thread_id: value.threadId,
|
|
215
|
+
turn_id: value.turnId,
|
|
216
|
+
plan,
|
|
217
|
+
}];
|
|
218
|
+
}
|
|
184
219
|
if (method !== "item/completed" || value.item === undefined)
|
|
185
220
|
return [];
|
|
186
221
|
if (value.item.type === "agentMessage" && value.item.text) {
|
|
@@ -221,14 +256,16 @@ class CodexRpcClient {
|
|
|
221
256
|
permission;
|
|
222
257
|
onNotification;
|
|
223
258
|
onFatal;
|
|
259
|
+
diagnosticSecrets;
|
|
224
260
|
nextId = 1;
|
|
225
261
|
pending = new Map();
|
|
226
262
|
closedError = null;
|
|
227
|
-
constructor(child, permission, onNotification, onFatal) {
|
|
263
|
+
constructor(child, permission, onNotification, onFatal, diagnosticSecrets) {
|
|
228
264
|
this.child = child;
|
|
229
265
|
this.permission = permission;
|
|
230
266
|
this.onNotification = onNotification;
|
|
231
267
|
this.onFatal = onFatal;
|
|
268
|
+
this.diagnosticSecrets = diagnosticSecrets;
|
|
232
269
|
if (child.stdin === null || child.stdout === null) {
|
|
233
270
|
throw new Error("Codex app-server did not expose stdio");
|
|
234
271
|
}
|
|
@@ -272,7 +309,7 @@ class CodexRpcClient {
|
|
|
272
309
|
message = JSON.parse(line);
|
|
273
310
|
}
|
|
274
311
|
catch {
|
|
275
|
-
process.stderr.write(`Codex app-server emitted invalid JSON: ${line.slice(0, 500)}\n
|
|
312
|
+
process.stderr.write(sanitizeCodexDiagnostic(`Codex app-server emitted invalid JSON: ${line.slice(0, 500)}\n`, this.diagnosticSecrets));
|
|
276
313
|
return;
|
|
277
314
|
}
|
|
278
315
|
if (message.id !== undefined && message.method !== undefined) {
|
|
@@ -416,9 +453,9 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
416
453
|
if (method === "error" && params.willRetry === false) {
|
|
417
454
|
const detail = params.error?.message;
|
|
418
455
|
if (detail)
|
|
419
|
-
process.stderr.write(`Codex turn error: ${detail.slice(0, ERROR_MESSAGE_CAP)}\n
|
|
456
|
+
process.stderr.write(sanitizeCodexDiagnostic(`Codex turn error: ${detail.slice(0, ERROR_MESSAGE_CAP)}\n`, secrets));
|
|
420
457
|
}
|
|
421
|
-
}, completionReject);
|
|
458
|
+
}, completionReject, secrets);
|
|
422
459
|
let treeStopPromise = null;
|
|
423
460
|
const ensureChildTreeStopped = () => {
|
|
424
461
|
treeStopPromise ??= stopChildTree(child);
|
|
@@ -440,30 +477,62 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
440
477
|
let activeStage = "initialize";
|
|
441
478
|
let stageStartedAt = Date.now();
|
|
442
479
|
try {
|
|
443
|
-
await rpc.request("initialize", {
|
|
480
|
+
const initialized = await rpc.request("initialize", {
|
|
444
481
|
clientInfo: { name: "nowcrew-daemon", version: "1" },
|
|
445
482
|
capabilities: { experimentalApi: true, requestAttestation: false },
|
|
446
483
|
}, initializeTimeoutMs);
|
|
484
|
+
if (input.workspaceRoots !== undefined || input.skillRoots !== undefined) {
|
|
485
|
+
assertCodexNativeWorkspaceVersion(initialized);
|
|
486
|
+
}
|
|
447
487
|
logStage(activeStage, "ok", attempt, stageStartedAt);
|
|
448
488
|
rpc.notify("initialized");
|
|
449
489
|
activeStage = input.resume ? "thread_resume" : "thread_start";
|
|
450
490
|
stageStartedAt = Date.now();
|
|
491
|
+
const threadCwd = input.cwd ?? process.cwd();
|
|
451
492
|
const threadParams = {
|
|
452
|
-
cwd:
|
|
493
|
+
cwd: threadCwd,
|
|
494
|
+
...(input.workspaceRoots === undefined
|
|
495
|
+
? {}
|
|
496
|
+
: { runtimeWorkspaceRoots: input.workspaceRoots }),
|
|
453
497
|
approvalPolicy: "never",
|
|
454
498
|
sandbox: sandboxMode(input.effectivePermission),
|
|
455
499
|
developerInstructions: input.systemPrompt,
|
|
456
500
|
...(input.model === undefined ? {} : { model: input.model }),
|
|
457
501
|
};
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
502
|
+
let thread;
|
|
503
|
+
const nativeContractRequired = input.workspaceRoots !== undefined || input.skillRoots !== undefined;
|
|
504
|
+
try {
|
|
505
|
+
thread = input.resume && input.sessionId !== undefined
|
|
506
|
+
? await rpc.request("thread/resume", { threadId: input.sessionId, ...threadParams })
|
|
507
|
+
: await rpc.request("thread/start", threadParams);
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
if (nativeContractRequired)
|
|
511
|
+
throw codexProjectWorkspaceUnsupported();
|
|
512
|
+
throw error;
|
|
513
|
+
}
|
|
514
|
+
if (nativeContractRequired) {
|
|
515
|
+
thread = assertCodexProjectWorkspaceResponse(thread, {
|
|
516
|
+
cwd: threadCwd,
|
|
517
|
+
workspaceRoots: input.workspaceRoots ?? [],
|
|
518
|
+
});
|
|
519
|
+
}
|
|
461
520
|
logStage(activeStage, "ok", attempt, stageStartedAt);
|
|
462
521
|
threadId = thread.thread.id;
|
|
463
522
|
if (announcedThreadId !== threadId) {
|
|
464
523
|
announcedThreadId = threadId;
|
|
465
524
|
await jsonLine({ type: "thread.started", thread_id: threadId });
|
|
466
525
|
}
|
|
526
|
+
if (input.skillRoots !== undefined) {
|
|
527
|
+
try {
|
|
528
|
+
await rpc.request("skills/extraRoots/set", { extraRoots: input.skillRoots });
|
|
529
|
+
const listed = await rpc.request("skills/list", { cwds: [threadCwd], forceReload: true });
|
|
530
|
+
assertCodexSkillsListResponse(listed, threadCwd);
|
|
531
|
+
}
|
|
532
|
+
catch {
|
|
533
|
+
throw codexProjectWorkspaceUnsupported();
|
|
534
|
+
}
|
|
535
|
+
}
|
|
467
536
|
firstProgress = startFirstProgressWatchdog(() => {
|
|
468
537
|
completionReject(new Error("Codex produced no semantic progress within the startup window"));
|
|
469
538
|
void cancel();
|
|
@@ -501,7 +570,7 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
501
570
|
if (completed.turn?.status === "completed")
|
|
502
571
|
return { code: 0, initializeTimedOut: false };
|
|
503
572
|
const detail = completed.turn?.error?.message ?? `turn status ${completed.turn?.status ?? "unknown"}`;
|
|
504
|
-
process.stderr.write(`Codex turn failed: ${detail.slice(0, ERROR_MESSAGE_CAP)}\n
|
|
573
|
+
process.stderr.write(sanitizeCodexDiagnostic(`Codex turn failed: ${detail.slice(0, ERROR_MESSAGE_CAP)}\n`, secrets));
|
|
505
574
|
return {
|
|
506
575
|
code: completed.turn?.status === "interrupted" ? 130 : 1,
|
|
507
576
|
initializeTimedOut: false,
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { CODEX_NATIVE_WORKSPACE_MIN_VERSION } from "./codex.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export const CODEX_PROJECT_WORKSPACE_UNSUPPORTED = "codex_project_workspace_unsupported";
|
|
4
|
+
export const CODEX_PLAN_ENTRY_LIMIT = 16;
|
|
5
|
+
// 16 steps remain inside the server's 4,000 UTF-16-unit plan payload cap even
|
|
6
|
+
// when every retained code point is a surrogate pair (for example, emoji).
|
|
7
|
+
export const CODEX_PLAN_STEP_TEXT_LIMIT = 120;
|
|
8
|
+
const NativeInitializeResponseSchema = z.object({
|
|
9
|
+
userAgent: z.string().min(1),
|
|
10
|
+
codexHome: z.string().min(1),
|
|
11
|
+
platformFamily: z.string().min(1),
|
|
12
|
+
platformOs: z.string().min(1),
|
|
13
|
+
}).passthrough();
|
|
14
|
+
const NativeThreadWorkspaceSchema = z.object({
|
|
15
|
+
thread: z.object({ id: z.string().min(1) }).passthrough(),
|
|
16
|
+
cwd: z.string().min(1),
|
|
17
|
+
runtimeWorkspaceRoots: z.array(z.string().min(1)),
|
|
18
|
+
instructionSources: z.array(z.string().min(1)),
|
|
19
|
+
}).strict();
|
|
20
|
+
const SkillMetadataSchema = z.object({
|
|
21
|
+
name: z.string().min(1),
|
|
22
|
+
description: z.string(),
|
|
23
|
+
shortDescription: z.string().optional(),
|
|
24
|
+
interface: z.unknown().optional(),
|
|
25
|
+
dependencies: z.unknown().optional(),
|
|
26
|
+
path: z.string().min(1),
|
|
27
|
+
scope: z.string().min(1),
|
|
28
|
+
enabled: z.boolean(),
|
|
29
|
+
}).passthrough();
|
|
30
|
+
const SkillsListResponseSchema = z.object({
|
|
31
|
+
data: z.array(z.object({
|
|
32
|
+
cwd: z.string().min(1),
|
|
33
|
+
skills: z.array(SkillMetadataSchema),
|
|
34
|
+
errors: z.array(z.object({
|
|
35
|
+
path: z.string(),
|
|
36
|
+
message: z.string(),
|
|
37
|
+
}).passthrough()),
|
|
38
|
+
}).passthrough()),
|
|
39
|
+
}).passthrough();
|
|
40
|
+
const stringArrayEquals = (value, expected) => Array.isArray(value)
|
|
41
|
+
&& value.length === expected.length
|
|
42
|
+
&& value.every((entry, index) => typeof entry === "string" && entry === expected[index]);
|
|
43
|
+
const versionTuple = (version) => {
|
|
44
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/u.exec(version);
|
|
45
|
+
if (match === null)
|
|
46
|
+
return null;
|
|
47
|
+
const [, major = "0", minor = "0", patch = "0"] = match;
|
|
48
|
+
const tuple = [Number(major), Number(minor), Number(patch)];
|
|
49
|
+
return tuple.every(Number.isSafeInteger) ? tuple : null;
|
|
50
|
+
};
|
|
51
|
+
const versionAtLeast = (actual, minimum) => actual[0] > minimum[0]
|
|
52
|
+
|| (actual[0] === minimum[0] && (actual[1] > minimum[1]
|
|
53
|
+
|| (actual[1] === minimum[1] && actual[2] >= minimum[2])));
|
|
54
|
+
/** Native roots require the app-server version returned by initialize, not a separate shell probe. */
|
|
55
|
+
export function assertCodexNativeWorkspaceVersion(response) {
|
|
56
|
+
const parsed = NativeInitializeResponseSchema.safeParse(response);
|
|
57
|
+
if (!parsed.success)
|
|
58
|
+
throw codexProjectWorkspaceUnsupported();
|
|
59
|
+
const match = /(?:^|[/\s])(\d+\.\d+\.\d+)(?=$|[\s(])/u.exec(parsed.data.userAgent);
|
|
60
|
+
const actual = match === null ? null : versionTuple(match[1] ?? "");
|
|
61
|
+
const minimum = versionTuple(CODEX_NATIVE_WORKSPACE_MIN_VERSION);
|
|
62
|
+
if (actual === null || minimum === null || !versionAtLeast(actual, minimum)) {
|
|
63
|
+
throw codexProjectWorkspaceUnsupported();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const PLAN_STATUSES = new Set(["pending", "inProgress", "completed"]);
|
|
67
|
+
const clipCharacters = (value, limit) => [...value.trim().replace(/\s+/gu, " ")].slice(0, limit).join("");
|
|
68
|
+
/** Treat plan updates as bounded user-visible data; any malformed retained step rejects the event. */
|
|
69
|
+
export function boundedCodexPlan(raw) {
|
|
70
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
71
|
+
return null;
|
|
72
|
+
const plan = [];
|
|
73
|
+
for (const entry of raw.slice(0, CODEX_PLAN_ENTRY_LIMIT)) {
|
|
74
|
+
if (entry === null || typeof entry !== "object")
|
|
75
|
+
return null;
|
|
76
|
+
const value = entry;
|
|
77
|
+
if (typeof value.step !== "string"
|
|
78
|
+
|| typeof value.status !== "string"
|
|
79
|
+
|| !PLAN_STATUSES.has(value.status))
|
|
80
|
+
return null;
|
|
81
|
+
const step = clipCharacters(value.step, CODEX_PLAN_STEP_TEXT_LIMIT);
|
|
82
|
+
if (step === "")
|
|
83
|
+
return null;
|
|
84
|
+
plan.push(Object.freeze({ step, status: value.status }));
|
|
85
|
+
}
|
|
86
|
+
return Object.freeze(plan);
|
|
87
|
+
}
|
|
88
|
+
export function codexPlanText(plan) {
|
|
89
|
+
const marker = {
|
|
90
|
+
pending: " ", inProgress: "~", completed: "x",
|
|
91
|
+
};
|
|
92
|
+
return plan.map(({ step, status }) => `- [${marker[status]}] ${step}`).join("\n");
|
|
93
|
+
}
|
|
94
|
+
/** Bound launches require the experimental workspace fields to round-trip before any model turn. */
|
|
95
|
+
export function assertCodexProjectWorkspaceResponse(response, expected) {
|
|
96
|
+
const raw = response !== null && typeof response === "object"
|
|
97
|
+
? response
|
|
98
|
+
: {};
|
|
99
|
+
// Validate a strict projection so additions to the upstream response remain forward-compatible.
|
|
100
|
+
const parsed = NativeThreadWorkspaceSchema.safeParse({
|
|
101
|
+
thread: raw.thread,
|
|
102
|
+
cwd: raw.cwd,
|
|
103
|
+
runtimeWorkspaceRoots: raw.runtimeWorkspaceRoots,
|
|
104
|
+
instructionSources: raw.instructionSources,
|
|
105
|
+
});
|
|
106
|
+
if (!parsed.success
|
|
107
|
+
|| parsed.data.cwd !== expected.cwd
|
|
108
|
+
|| !stringArrayEquals(parsed.data.runtimeWorkspaceRoots, expected.workspaceRoots)) {
|
|
109
|
+
throw codexProjectWorkspaceUnsupported();
|
|
110
|
+
}
|
|
111
|
+
return parsed.data;
|
|
112
|
+
}
|
|
113
|
+
export function assertCodexSkillsListResponse(response, requestedCwd) {
|
|
114
|
+
const parsed = SkillsListResponseSchema.safeParse(response);
|
|
115
|
+
if (!parsed.success
|
|
116
|
+
|| !parsed.data.data.some((entry) => entry.cwd === requestedCwd)
|
|
117
|
+
|| parsed.data.data.some((entry) => entry.errors.length > 0)) {
|
|
118
|
+
throw codexProjectWorkspaceUnsupported();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export function codexProjectWorkspaceUnsupported() {
|
|
122
|
+
return new Error(CODEX_PROJECT_WORKSPACE_UNSUPPORTED);
|
|
123
|
+
}
|
package/dist/runtimes/codex.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
// cross-spawn:win32 上 npm CLI 是 .cmd shim,node 原生 spawn 不带 shell 无法执行(ENOENT/EINVAL)
|
|
5
5
|
import spawn from "cross-spawn";
|
|
6
|
+
/** Minimum CLI whose real app-server contract supports NowCrew native project workspaces. */
|
|
7
|
+
export const CODEX_NATIVE_WORKSPACE_MIN_VERSION = "0.148.0";
|
|
6
8
|
// Codex CLI 原生 model_reasoning_effort 档位(codex 0.135.0 实测:非法值时 config 解析报错枚举这六档)。
|
|
7
9
|
// 注意:codex 对非法值是硬失败(进程直接退出),所以必须白名单过滤;白名单外(含 "default"、
|
|
8
10
|
// claude 专属的 "max")回落 CODEX_DEFAULT_EFFORT。
|
package/dist/serve.js
CHANGED
|
@@ -8,7 +8,7 @@ import { randomUUID } from "node:crypto";
|
|
|
8
8
|
import { initSlog, dslog, setSlogDefaults, drainSpool, flushSlog } from "./slog.js";
|
|
9
9
|
import { mergeRunAgentResults, reportScheduledStartFailure, runAgent } from "./runner.js";
|
|
10
10
|
import { buildOriginDecisionRetryPrompt, buildScheduledPrompt } from "./prompt.js";
|
|
11
|
-
import { collectMachineHello, cliVersion, daemonVersion, detectExecutionRuntimesWithSignal, } from "./machine-info.js";
|
|
11
|
+
import { collectMachineHello, cliVersion, daemonCapabilityBindings, daemonVersion, detectExecutionRuntimesWithSignal, } from "./machine-info.js";
|
|
12
12
|
import { conservativeExecutionRuntimes, createRuntimeProbeCoordinator, } from "./runtime-probe.js";
|
|
13
13
|
import { listWorkspace, readWorkspaceFile } from "./workspace-fs.js";
|
|
14
14
|
import { listSkills } from "./skills.js";
|
|
@@ -21,7 +21,7 @@ import { runWithOriginDecisionGuard } from "./origin-decision.js";
|
|
|
21
21
|
import { createExecutionJournal } from "./execution-journal.js";
|
|
22
22
|
import { createExecutionTelemetryJournal } from "./execution-telemetry-journal.js";
|
|
23
23
|
import { ExecutionRejectedSchema, ExecutionSnapshotSchema, LegacyAgentStartSchema, ServerToDaemonExecutionFrameSchema, } from "./execution-protocol.js";
|
|
24
|
-
import { hashExecutionSpec, runExecution, } from "./execution-runner.js";
|
|
24
|
+
import { hashExecutionSpec, projectWorkspaceCapabilityRejection, runExecution, } from "./execution-runner.js";
|
|
25
25
|
import { awaitWithCancellation, createRuntimeCancellation, RuntimeCancelledError, } from "./runtime-cancellation.js";
|
|
26
26
|
import { createShutdownDeadline, readTestShutdownConfiguration } from "./shutdown-deadline.js";
|
|
27
27
|
import { closeWebSocketWithinDeadline } from "./websocket-shutdown.js";
|
|
@@ -40,10 +40,12 @@ import { createProjectSkillsController, } from "./project-skills/controller.js";
|
|
|
40
40
|
import { PROJECT_SKILLS_CAPABILITY } from "./project-skills/types.js";
|
|
41
41
|
import { createAgentProjectionCoordinator } from "./project-skills/agent-projection-coordinator.js";
|
|
42
42
|
import { createAgentAbilityRuntime } from "./agent-ability/runtime.js";
|
|
43
|
-
import { createProjectSkillsReconciler,
|
|
43
|
+
import { createProjectSkillsReconciler, } from "./project-skills/reconciler.js";
|
|
44
|
+
import { initializedProjectSkillsReconciler } from "./project-skills/initialized-reconciler.js";
|
|
44
45
|
import { parseMemoryPruneTraceId } from "./memory-prune-diagnostics.js";
|
|
45
46
|
import { handleRuntimeProbeFrame, probeRuntimeHealth } from "./runtime-health.js";
|
|
46
47
|
import { buildControlPlaneUrl } from "./control-plane-url.js";
|
|
48
|
+
import { createServeMigrationController, handleMigrationControlMessage } from "./daemon-migration-wiring.js";
|
|
47
49
|
export { buildControlPlaneUrl } from "./control-plane-url.js";
|
|
48
50
|
// normalize.ts 的活动种类 → activity 枚举
|
|
49
51
|
const ACTIVITY_MAP = {
|
|
@@ -104,6 +106,16 @@ export function serve(config, opts = {}) {
|
|
|
104
106
|
catch { /* reconnect/timeout reconciliation handles a lost status frame */ }
|
|
105
107
|
},
|
|
106
108
|
});
|
|
109
|
+
const migrationController = createServeMigrationController({
|
|
110
|
+
config,
|
|
111
|
+
...(opts.profileName === undefined ? {} : { profileName: opts.profileName }),
|
|
112
|
+
isBusy: () => executionRuns.size > 0 || legacyRuns.size > 0,
|
|
113
|
+
sendStatus: (frame) => { try {
|
|
114
|
+
if (ws?.readyState === WebSocket.OPEN)
|
|
115
|
+
ws.send(JSON.stringify(frame));
|
|
116
|
+
}
|
|
117
|
+
catch { /* reconnect */ } },
|
|
118
|
+
});
|
|
107
119
|
const projectionCoordinator = createAgentProjectionCoordinator();
|
|
108
120
|
const agentAbilityRuntime = createAgentAbilityRuntime(config.agentsRoot, {
|
|
109
121
|
...opts.agentAbility,
|
|
@@ -127,6 +139,7 @@ export function serve(config, opts = {}) {
|
|
|
127
139
|
catch { /* 下一次 ready 或项目操作会重新发送完整快照 */ }
|
|
128
140
|
},
|
|
129
141
|
reconcile: (handle, bindings) => projectSkillsReconciler.reconcile(handle, bindings),
|
|
142
|
+
ensureSnapshot: (handle, snapshot) => projectSkillsReconciler.ensureSnapshot(handle, snapshot),
|
|
130
143
|
});
|
|
131
144
|
let projectSkillsStatus = "initializing";
|
|
132
145
|
let projectSkillsInitialization = null;
|
|
@@ -172,18 +185,7 @@ export function serve(config, opts = {}) {
|
|
|
172
185
|
projectSkillsInitialization = attempt;
|
|
173
186
|
return attempt;
|
|
174
187
|
};
|
|
175
|
-
const
|
|
176
|
-
async reconcile(handle, bindings) {
|
|
177
|
-
return await ensureProjectSkillsInitialized()
|
|
178
|
-
? projectSkillsReconciler.reconcile(handle, bindings)
|
|
179
|
-
: Promise.reject(new ProjectProjectionError("skill_projection_failed"));
|
|
180
|
-
},
|
|
181
|
-
async prepareAndLaunch(agentsRoot, handle, bindings, launch) {
|
|
182
|
-
return await ensureProjectSkillsInitialized()
|
|
183
|
-
? projectSkillsReconciler.prepareAndLaunch(agentsRoot, handle, bindings, launch)
|
|
184
|
-
: Promise.reject(new ProjectProjectionError("skill_projection_failed"));
|
|
185
|
-
},
|
|
186
|
-
};
|
|
188
|
+
const initializedProjectSkills = initializedProjectSkillsReconciler(ensureProjectSkillsInitialized, projectSkillsReconciler);
|
|
187
189
|
const safeExecutionSend = (frame) => {
|
|
188
190
|
try {
|
|
189
191
|
if (ws?.readyState !== WebSocket.OPEN)
|
|
@@ -281,6 +283,7 @@ export function serve(config, opts = {}) {
|
|
|
281
283
|
reservation.release();
|
|
282
284
|
};
|
|
283
285
|
let connectedAt = 0; // 本次 WS 连接建立时刻(断开日志算在线时长用)
|
|
286
|
+
const capabilities = daemonCapabilityBindings(process.platform, opts.machineInfo?.capabilities);
|
|
284
287
|
initSlog(config.serverUrl, config.machineToken, { daemonVersion: daemonVersion(), cliVersion: cliVersion(), ...(opts.profileName === undefined ? {} : { profileName: opts.profileName }), agentsRoot: config.agentsRoot });
|
|
285
288
|
dslog("daemon.start", "daemon 常驻模式启动", { server_url: config.serverUrl, runtime: config.runtimeBin });
|
|
286
289
|
// 并行调度:同一 agent 可并行处理多个【不同任务】(线程/频道),每任务隔离 cwd+work-log。
|
|
@@ -293,7 +296,7 @@ export function serve(config, opts = {}) {
|
|
|
293
296
|
function connect() {
|
|
294
297
|
if (stopped)
|
|
295
298
|
return;
|
|
296
|
-
const wsUrl = buildControlPlaneUrl(config.serverUrl, config.machineToken, process.platform, undefined, projectSkillsStatus === "ready");
|
|
299
|
+
const wsUrl = buildControlPlaneUrl(config.serverUrl, config.machineToken, process.platform, undefined, projectSkillsStatus === "ready", capabilities.controlPlaneUrl);
|
|
297
300
|
ws = createWebSocket(wsUrl);
|
|
298
301
|
ws.on("open", () => {
|
|
299
302
|
const openedSocket = ws;
|
|
@@ -307,9 +310,11 @@ export function serve(config, opts = {}) {
|
|
|
307
310
|
const { detectInstalled, detectExecutable = detectExecutionRuntimesWithSignal } = opts.machineInfo ?? {};
|
|
308
311
|
const helloPromise = collectMachineHello(config.agentsRoot, config.executionLimits, process.platform, {
|
|
309
312
|
...(detectInstalled ? { detectInstalled } : {}),
|
|
313
|
+
...(opts.profileName === undefined ? {} : { profileName: opts.profileName }),
|
|
310
314
|
// First hello must not wait for third-party handshakes; optional transports arrive in the refresh.
|
|
311
315
|
detectExecutable: async (installed) => conservativeExecutionRuntimes(installed),
|
|
312
316
|
additionalCapabilities: () => managedDaemonCapabilities(updateEligibility),
|
|
317
|
+
capabilities: capabilities.machineHello,
|
|
313
318
|
});
|
|
314
319
|
runtimeFacts = helloPromise
|
|
315
320
|
.then(async (hello) => {
|
|
@@ -390,6 +395,10 @@ export function serve(config, opts = {}) {
|
|
|
390
395
|
});
|
|
391
396
|
return;
|
|
392
397
|
}
|
|
398
|
+
if (rawType === "daemon:migrate") {
|
|
399
|
+
void handleMigrationControlMessage(migrationController, decoded).catch((error) => dslog("daemon.migration_failed", "daemon 布局迁移失败", { level: "ERROR", error_message: error.message }));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
393
402
|
if (rawType.startsWith("execution:")) {
|
|
394
403
|
const parsedExecution = ServerToDaemonExecutionFrameSchema.safeParse(decoded);
|
|
395
404
|
if (!parsedExecution.success) {
|
|
@@ -451,6 +460,9 @@ export function serve(config, opts = {}) {
|
|
|
451
460
|
return;
|
|
452
461
|
}
|
|
453
462
|
const spec = frame;
|
|
463
|
+
const workspaceRejection = projectWorkspaceCapabilityRejection(spec, capabilities.executionRunner, new Date().toISOString());
|
|
464
|
+
if (workspaceRejection !== null)
|
|
465
|
+
return void safeExecutionSend(workspaceRejection);
|
|
454
466
|
const hash = hashExecutionSpec(spec);
|
|
455
467
|
const knownHash = knownExecutionHashes.get(spec.executionId);
|
|
456
468
|
if (knownHash !== undefined) {
|
|
@@ -587,7 +599,8 @@ export function serve(config, opts = {}) {
|
|
|
587
599
|
}
|
|
588
600
|
const execution = executeProtocol(config, spec, {
|
|
589
601
|
...opts.execution?.dependencies,
|
|
590
|
-
|
|
602
|
+
capabilities: capabilities.executionRunner,
|
|
603
|
+
projectSkills: opts.execution?.dependencies?.projectSkills ?? initializedProjectSkills,
|
|
591
604
|
abilityRelease: opts.execution?.dependencies?.abilityRelease ?? agentAbilityRuntime.materializer,
|
|
592
605
|
...(agentMemory === undefined ? {} : { agentMemory }),
|
|
593
606
|
journal: executionJournal,
|
|
@@ -1154,6 +1167,7 @@ export function serve(config, opts = {}) {
|
|
|
1154
1167
|
const pending = [
|
|
1155
1168
|
webSocketClosed,
|
|
1156
1169
|
updateController.drain(),
|
|
1170
|
+
...(migrationController === null ? [] : [migrationController.drain()]),
|
|
1157
1171
|
executionFrameQueue,
|
|
1158
1172
|
...executionRuns.values(),
|
|
1159
1173
|
...[...legacyRuns.values()].map((run) => run.done),
|
package/dist/session.js
CHANGED
|
@@ -29,6 +29,9 @@ export async function readSession(runDir) {
|
|
|
29
29
|
turns: typeof o.turns === "number" ? o.turns : 0,
|
|
30
30
|
model: typeof o.model === "string" ? o.model : null,
|
|
31
31
|
providerFingerprint: typeof o.providerFingerprint === "string" ? o.providerFingerprint : null,
|
|
32
|
+
...(typeof o.sessionContextFingerprint === "string"
|
|
33
|
+
? { sessionContextFingerprint: o.sessionContextFingerprint }
|
|
34
|
+
: {}),
|
|
32
35
|
...(typeof o.contextTokens === "number" && o.contextTokens >= 0 ? { contextTokens: o.contextTokens } : {}),
|
|
33
36
|
...(typeof o.lastExitOk === "boolean" ? { lastExitOk: o.lastExitOk } : {}),
|
|
34
37
|
};
|
|
@@ -13,6 +13,10 @@ export function supervisorLaunch(request) {
|
|
|
13
13
|
...(request.reasoning === undefined ? {} : { reasoning: request.reasoning }),
|
|
14
14
|
};
|
|
15
15
|
if (request.runtime === "claude") {
|
|
16
|
+
const additionalDirectories = request.claudeAdditionalDirectories
|
|
17
|
+
?? (request.agentRoot === undefined
|
|
18
|
+
? undefined
|
|
19
|
+
: [join(request.agentRoot, ".crew", "claude-skills"), request.agentRoot]);
|
|
16
20
|
return {
|
|
17
21
|
command: request.bin,
|
|
18
22
|
args: buildClaudeArgs({
|
|
@@ -21,10 +25,7 @@ export function supervisorLaunch(request) {
|
|
|
21
25
|
cwd: request.cwd,
|
|
22
26
|
env: request.env,
|
|
23
27
|
systemPromptPath: request.systemPromptPath,
|
|
24
|
-
...(
|
|
25
|
-
projectSkillsDirectory: join(request.agentRoot, ".crew", "claude-skills"),
|
|
26
|
-
agentRootDirectory: request.agentRoot,
|
|
27
|
-
}),
|
|
28
|
+
...(additionalDirectories === undefined ? {} : { additionalDirectories }),
|
|
28
29
|
...(request.sessionId === undefined ? {} : {
|
|
29
30
|
sessionId: request.sessionId,
|
|
30
31
|
resume: request.resume,
|
|
@@ -54,6 +55,13 @@ export function supervisorLaunch(request) {
|
|
|
54
55
|
...(request.agentRoot === undefined ? {} : {
|
|
55
56
|
projectRootMarkers: [".git", ".nowwork-root"],
|
|
56
57
|
}),
|
|
58
|
+
...(request.workspaceRoots === undefined ? {} : {
|
|
59
|
+
cwd: request.cwd,
|
|
60
|
+
workspaceRoots: request.workspaceRoots,
|
|
61
|
+
}),
|
|
62
|
+
...(request.codexSkillRoots === undefined ? {} : {
|
|
63
|
+
skillRoots: request.codexSkillRoots,
|
|
64
|
+
}),
|
|
57
65
|
resume: request.resume,
|
|
58
66
|
}),
|
|
59
67
|
};
|
package/dist/workspace.js
CHANGED
|
@@ -88,14 +88,23 @@ export async function prepareWorkspace(input) {
|
|
|
88
88
|
}
|
|
89
89
|
const workLog = (await exists(workLogPath)) ? await readFile(workLogPath, "utf8") : "";
|
|
90
90
|
// resumeKey 可跨不同 task cwd 维持同一底层会话;协议键用 opaque 映射,legacy 保持 safeKey。
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
// 未绑定项目时保留原有分支,避免改变旧 sessionDir 与 import 语义。
|
|
92
|
+
let sessionDir;
|
|
93
|
+
if (input.sessionContextFingerprint === undefined) {
|
|
94
|
+
sessionDir = input.resumeKey
|
|
95
|
+
? join(crewDir, "sessions", workspaceKey(input.resumeKey, input.keyMode))
|
|
96
|
+
: runDir;
|
|
97
|
+
if (input.resumeKey)
|
|
98
|
+
await mkdir(sessionDir, { recursive: true });
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const effectiveSessionKey = `${input.resumeKey ?? input.taskKey}\0${input.sessionContextFingerprint}`;
|
|
102
|
+
sessionDir = join(crewDir, "sessions", workspaceKey(effectiveSessionKey, input.keyMode));
|
|
95
103
|
await mkdir(sessionDir, { recursive: true });
|
|
104
|
+
}
|
|
96
105
|
let agentSessionId = null;
|
|
97
106
|
let sessionResume = false;
|
|
98
|
-
if (input.taskKey || input.resumeKey) {
|
|
107
|
+
if (input.taskKey || input.resumeKey || input.sessionContextFingerprint !== undefined) {
|
|
99
108
|
const sessionPath = join(sessionDir, ".session");
|
|
100
109
|
if (await exists(sessionPath)) {
|
|
101
110
|
agentSessionId = (await readFile(sessionPath, "utf8")).trim() || null;
|