@nowcrew/daemon 0.6.15 → 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.
Files changed (46) hide show
  1. package/dist/agent-ability/controller.js +6 -2
  2. package/dist/agent-ability/resolver.js +6 -0
  3. package/dist/agent-ability/runtime-context.js +7 -1
  4. package/dist/agent-ability/runtime.js +2 -3
  5. package/dist/atomic-private-write.js +54 -1
  6. package/dist/automatic-install-target.js +40 -11
  7. package/dist/console.js +9 -0
  8. package/dist/control-plane-url.js +2 -2
  9. package/dist/daemon-migration-controller.js +198 -0
  10. package/dist/daemon-migration-wiring.js +22 -0
  11. package/dist/daemon-update-eligibility.js +1 -1
  12. package/dist/directory-projection-identity.js +32 -0
  13. package/dist/directory-projection.js +922 -0
  14. package/dist/execution-protocol.js +78 -11
  15. package/dist/execution-runner.js +50 -2
  16. package/dist/i18n.js +1 -0
  17. package/dist/local-execution-prompt.js +57 -0
  18. package/dist/local-executor.js +99 -40
  19. package/dist/machine-info.js +45 -9
  20. package/dist/normalize.js +5 -0
  21. package/dist/profile-layout.js +41 -0
  22. package/dist/project-skills/controller.js +74 -14
  23. package/dist/project-skills/execution-adapter.js +11 -0
  24. package/dist/project-skills/initialized-reconciler.js +20 -0
  25. package/dist/project-skills/projection-set-switch.js +419 -0
  26. package/dist/project-skills/projection-state-domain.js +153 -0
  27. package/dist/project-skills/projection-state-store.js +841 -0
  28. package/dist/project-skills/projection-state-transaction.js +318 -0
  29. package/dist/project-skills/projection-state.js +3 -0
  30. package/dist/project-skills/reconciler.js +299 -68
  31. package/dist/project-skills/runtime-warning.js +6 -0
  32. package/dist/project-skills/scanner.js +30 -1
  33. package/dist/project-skills/types.js +9 -0
  34. package/dist/project-workspaces/resolver.js +179 -0
  35. package/dist/project-workspaces/types.js +1 -0
  36. package/dist/prompt.js +64 -5
  37. package/dist/runner.js +1 -0
  38. package/dist/runtimes/claude.js +235 -4
  39. package/dist/runtimes/codex-app-server-runner.js +92 -23
  40. package/dist/runtimes/codex-contract.js +123 -0
  41. package/dist/runtimes/codex.js +2 -0
  42. package/dist/serve.js +31 -17
  43. package/dist/session.js +3 -0
  44. package/dist/supervised-runtime.js +12 -4
  45. package/dist/workspace.js +14 -5
  46. 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
- let message = error instanceof Error ? error.message : String(error);
69
- for (const secret of secrets) {
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 values = [input.systemPrompt, input.wakePrompt, ...sensitiveEnvironmentValues(env)];
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 redactCodexStderr(text, secrets) {
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/g, "[redacted]")
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
- : redactCodexStderr(this.pendingLine, this.secrets);
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 = redactCodexStderr(text, this.secrets);
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: process.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
- const thread = input.resume && input.sessionId !== undefined
459
- ? await rpc.request("thread/resume", { threadId: input.sessionId, ...threadParams })
460
- : await rpc.request("thread/start", threadParams);
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
+ }
@@ -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, ProjectProjectionError, } from "./project-skills/reconciler.js";
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 initializedProjectSkillsReconciler = {
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
- projectSkills: opts.execution?.dependencies?.projectSkills ?? initializedProjectSkillsReconciler,
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
- ...(request.agentRoot === undefined ? {} : {
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
- const sessionDir = input.resumeKey
92
- ? join(crewDir, "sessions", workspaceKey(input.resumeKey, input.keyMode))
93
- : runDir;
94
- if (input.resumeKey)
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.15",
3
+ "version": "0.6.17",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",