@nowcrew/daemon 0.6.63 → 0.6.65

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 (57) hide show
  1. package/README.md +9 -8
  2. package/dist/agent-ability/materializer.js +20 -1
  3. package/dist/agent-ability/runtime.js +3 -0
  4. package/dist/automatic-install-target.js +15 -10
  5. package/dist/computer-cli.js +10 -1
  6. package/dist/computer-service.js +41 -1
  7. package/dist/config.js +44 -0
  8. package/dist/daemon-migration-controller.js +16 -18
  9. package/dist/execution-runner.js +16 -35
  10. package/dist/local-execution-prompt.js +14 -9
  11. package/dist/local-executor.js +56 -3
  12. package/dist/local-memory/index-budget.js +74 -0
  13. package/dist/local-memory/pending.js +111 -0
  14. package/dist/local-memory/runtime.js +87 -0
  15. package/dist/local-memory/types.js +1 -0
  16. package/dist/local-memory/write.js +448 -0
  17. package/dist/machine-info.js +3 -2
  18. package/dist/main.js +83 -0
  19. package/dist/managed-service-lifecycle.js +47 -2
  20. package/dist/managed-service-startup.js +2 -0
  21. package/dist/profile-layout.js +10 -4
  22. package/dist/project-skills/controller.js +2 -0
  23. package/dist/prompt.js +19 -1
  24. package/dist/runner.js +3 -0
  25. package/dist/runtimes/codex-app-server-runner.js +54 -14
  26. package/dist/runtimes/codex-command-safety.js +75 -0
  27. package/dist/runtimes/codex-contract.js +20 -5
  28. package/dist/runtimes/codex-turn-retry.js +69 -0
  29. package/dist/runtimes/codex.js +4 -0
  30. package/dist/serve.js +68 -3
  31. package/dist/storage-config.js +175 -0
  32. package/dist/storage-control.js +193 -0
  33. package/dist/storage-copy.js +609 -0
  34. package/dist/storage-credential-guard.js +21 -0
  35. package/dist/storage-doctor.js +228 -0
  36. package/dist/storage-identity.js +29 -0
  37. package/dist/storage-layout.js +143 -0
  38. package/dist/storage-manifest.js +855 -0
  39. package/dist/storage-migration-controller.js +417 -0
  40. package/dist/storage-migration-manifest.js +105 -0
  41. package/dist/storage-migration-policy.js +100 -0
  42. package/dist/storage-runtime-controller.js +339 -0
  43. package/dist/storage-runtime-readiness.js +184 -0
  44. package/dist/storage-runtime.js +39 -0
  45. package/dist/storage-safe-fs.js +302 -0
  46. package/dist/storage-serve-adapter.js +65 -0
  47. package/dist/storage-status-delivery.js +209 -0
  48. package/dist/storage-status-replay.js +125 -0
  49. package/dist/storage-target-confirmation.js +129 -0
  50. package/dist/storage-worker-command.js +27 -0
  51. package/dist/storage-worker-executor.js +384 -0
  52. package/dist/storage-worker-handoff.js +151 -0
  53. package/dist/storage-worker-operation.js +284 -0
  54. package/dist/storage-worker-status.js +105 -0
  55. package/dist/supervised-runtime.js +3 -0
  56. package/dist/workspace.js +24 -1
  57. package/package.json +1 -1
package/README.md CHANGED
@@ -380,6 +380,7 @@ CREW_EXECUTION_MAX_STARTING_TOTAL=10
380
380
  CREW_EXECUTION_MAX_STARTING_PER_RUNTIME=10
381
381
  CREW_EXECUTION_START_GAP_MS=500
382
382
  CREW_EXECUTION_STARTUP_TIMEOUT_MS=120000
383
+ # Retained for compatibility; accepted executions wait indefinitely for a slot.
383
384
  CREW_EXECUTION_MAX_QUEUE_WAIT_MS=300000
384
385
  CREW_EXECUTION_MAX_FIRST_OUTPUT_WAIT_MS=120000
385
386
  CREW_EXECUTION_FIRST_OUTPUT_GRACE_MS=180000
@@ -391,14 +392,14 @@ protocol-v1 and legacy work.
391
392
  Runtime startup is a separate FIFO gate: by default up to ten Claude, Codex, or Kimi processes may be
392
393
  initializing at a time, and launches are spaced by 500ms. A process leaves the startup gate
393
394
  only after its runtime-specific ready event; startup timeout cancels the owned process tree.
394
- An accepted execution may wait up to five minutes for a local and host slot. After runtime readiness,
395
- the daemon waits up to two minutes for the first model event, with a three-minute grace window for
396
- slow providers. When one of these local liveness limits expires, the supervisor is stopped and the
397
- terminal result is written to the execution journal before it is reported, so the execution remains
398
- replayable instead of being silently dropped. Queue and first-output timeouts are retried locally at
399
- most twice (three total attempts). Each retry releases its current machine/host slot and reserves a
400
- new one, which puts it behind work already waiting in the queue. A third timeout is terminal and is
401
- not requeued; other failures are not automatically retried.
395
+ An accepted execution waits indefinitely for a local and host slot unless it is explicitly cancelled.
396
+ After runtime readiness, the daemon waits up to two minutes for the first model event, with a
397
+ three-minute grace window for slow providers. When the first-output liveness limit expires, the
398
+ supervisor is stopped and the terminal result is written to the execution journal before it is
399
+ reported, so the execution remains replayable instead of being silently dropped. First-output
400
+ timeouts are retried locally at most twice (three total attempts). Each retry releases its current
401
+ machine/host slot and reserves a new one, which puts it behind work already waiting in the queue. A
402
+ third timeout is terminal and is not requeued; other failures are not automatically retried.
402
403
 
403
404
  Local policy can reduce server-requested access and limits; it cannot grant more access than requested.
404
405
  Provider credentials and configured environment are prepared locally and never carried in execution
@@ -249,6 +249,23 @@ const verifyCache = async (agentsRoot, ability) => {
249
249
  };
250
250
  const layerRelativePath = (path, layer) => path.startsWith(`${layer}/`) ? path.slice(layer.length + 1) : path;
251
251
  const readManagedCatalog = async (trainingRoot) => JSON.parse(await readFile(join(trainingRoot, "ACTIVE_RELEASE.json"), "utf8"));
252
+ const projectedSkillExists = async (trainingRoot, name) => {
253
+ const root = join(trainingRoot, "skills", safeAbilityName(name));
254
+ if (!await isRealDirectory(root))
255
+ return false;
256
+ const direct = await lstat(join(root, "SKILL.md")).catch(() => null);
257
+ if (direct?.isFile() && !direct.isSymbolicLink())
258
+ return true;
259
+ for (const entry of await readdir(root, { withFileTypes: true })) {
260
+ if (!entry.isDirectory())
261
+ continue;
262
+ const skill = await lstat(join(root, entry.name, "SKILL.md")).catch(() => null);
263
+ if (skill?.isFile() && !skill.isSymbolicLink())
264
+ return true;
265
+ }
266
+ return false;
267
+ };
268
+ const projectedSkillsComplete = async (trainingRoot, skills) => (await Promise.all(skills.map((skill) => projectedSkillExists(trainingRoot, skill.name)))).every(Boolean);
252
269
  const trainingSwitchJournalPath = (agentRoot) => join(agentRoot, ".training-switch.json");
253
270
  const trainingRollbackRoot = (agentRoot) => join(agentRoot, ".training-rollback");
254
271
  const trainingDiscardRoot = (agentRoot) => join(agentRoot, ".training-discard");
@@ -422,6 +439,7 @@ const activeTrainingRoot = async (agentsRoot, handle, ability) => {
422
439
  && catalog.releaseId === ability.releaseId
423
440
  && catalog.rootCommit === ability.rootCommit
424
441
  && catalog.artifactDigest === ability.artifactDigest
442
+ && await projectedSkillsComplete(trainingRoot, ability.skills)
425
443
  ? trainingRoot
426
444
  : null;
427
445
  };
@@ -463,7 +481,8 @@ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
463
481
  }
464
482
  if (previousCatalog.releaseId === ability.releaseId
465
483
  && previousCatalog.artifactDigest === ability.artifactDigest
466
- && previousCatalog.projectionDigest === projectionDigest)
484
+ && previousCatalog.projectionDigest === projectionDigest
485
+ && await projectedSkillsComplete(trainingRoot, ability.skills))
467
486
  return trainingRoot;
468
487
  }
469
488
  await mkdir(agentRoot, { recursive: true });
@@ -32,6 +32,9 @@ export function createAgentAbilityRuntime(agentsRoot, options = {}) {
32
32
  else if (type === "agent:ability:resolve") {
33
33
  result = await controller.handle(input);
34
34
  }
35
+ else if (options.isAdmissionClosed?.() === true) {
36
+ result = { ok: false, error: "storage_migration_quiescing" };
37
+ }
35
38
  else {
36
39
  const parsed = AbilityApplyCommandSchema.safeParse(input);
37
40
  if (!parsed.success)
@@ -3,12 +3,12 @@ import { profileLayout, validateLocalInstanceName } from "./profile-layout.js";
3
3
  const PROFILE_NAME = /^[a-z0-9][a-z0-9_-]{0,47}$/;
4
4
  const KNOWN_TARGETS = {
5
5
  "nowwork-in.nowcoder.com": {
6
- profile: "in",
6
+ environment: "production",
7
7
  daemonDirectory: "daemon",
8
8
  agentsDirectory: "agents",
9
9
  },
10
10
  "nowwork-dev.nowcoder.com": {
11
- profile: "dev",
11
+ environment: "dev",
12
12
  daemonDirectory: "daemon-dev",
13
13
  agentsDirectory: "agents-dev",
14
14
  },
@@ -42,32 +42,37 @@ function requireCustomProfile(hostname, value) {
42
42
  if (!PROFILE_NAME.test(value)) {
43
43
  throw new Error("Invalid custom profile; expected a lowercase name of 1-48 letters, digits, '_' or '-'");
44
44
  }
45
- if (value === "in" || value === "dev") {
45
+ if (value === "in" || value === "dev" || value === "production") {
46
46
  throw new Error(`Profile '${value}' is reserved for a known NowWork hostname`);
47
47
  }
48
48
  return value;
49
49
  }
50
50
  function knownTargetForProfile(environment, requestedProfile, input, parsed) {
51
51
  const paths = input.platform === "win32" ? win32 : posix;
52
- const legacyProfile = environment;
52
+ // Keep the no-flag target readable by old daemons; any workspace-scoped profile below
53
+ // uses the canonical environment token (production, never in).
54
+ const legacyProfile = environment === "production" ? "in" : environment;
53
55
  if (requestedProfile === undefined || requestedProfile === legacyProfile) {
54
56
  return Object.freeze({
55
57
  kind: "known",
56
58
  profile: legacyProfile,
57
59
  serverUrl: normalizedServerUrl(parsed),
58
- daemonHome: paths.join(input.userHome, ".crew", environment === "in" ? "daemon" : "daemon-dev"),
59
- agentsRoot: paths.join(input.userHome, ".crew", environment === "in" ? "agents" : "agents-dev"),
60
+ daemonHome: paths.join(input.userHome, ".crew", environment === "production" ? "daemon" : "daemon-dev"),
61
+ agentsRoot: paths.join(input.userHome, ".crew", environment === "production" ? "agents" : "agents-dev"),
60
62
  npmPrefix: paths.join(input.userHome, ".crew", "daemon-runtimes", legacyProfile),
61
63
  });
62
64
  }
63
65
  if (requestedProfile === "in" || requestedProfile === "dev") {
64
- throw new Error(`Server hostname '${new URL(input.serverUrl).hostname}' requires profile '${legacyProfile}'`);
66
+ throw new Error(`Server hostname '${new URL(input.serverUrl).hostname}' requires profile '${environment}'`);
65
67
  }
66
68
  const prefix = `nw-${environment}-`;
67
- if (!requestedProfile.startsWith(prefix)) {
69
+ const legacyPrefix = environment === "production" ? "nw-in-" : prefix;
70
+ if (!requestedProfile.startsWith(prefix) && !requestedProfile.startsWith(legacyPrefix)) {
68
71
  throw new Error(`Server hostname '${new URL(input.serverUrl).hostname}' requires profile '${prefix}<local-name>'`);
69
72
  }
70
- const localInstanceName = validateLocalInstanceName(requestedProfile.slice(prefix.length));
73
+ const localInstanceName = validateLocalInstanceName(requestedProfile.startsWith(prefix)
74
+ ? requestedProfile.slice(prefix.length)
75
+ : requestedProfile.slice(legacyPrefix.length));
71
76
  const layout = profileLayout({
72
77
  environment,
73
78
  localInstanceName,
@@ -91,7 +96,7 @@ export function resolveAutomaticInstallTarget(input) {
91
96
  const paths = input.platform === "win32" ? win32 : posix;
92
97
  const known = KNOWN_TARGETS[hostname];
93
98
  if (known !== undefined) {
94
- return knownTargetForProfile(known.profile, input.requestedProfile, input, parsed);
99
+ return knownTargetForProfile(known.environment, input.requestedProfile, input, parsed);
95
100
  }
96
101
  const profile = requireCustomProfile(hostname, input.requestedProfile);
97
102
  return Object.freeze({
@@ -82,6 +82,14 @@ function translatedError(error, td) {
82
82
  }
83
83
  return td(error.message);
84
84
  }
85
+ function storageServiceEnvironment(env) {
86
+ return {
87
+ ...(env.NOWWORK_HOME === undefined ? {} : { NOWWORK_HOME: env.NOWWORK_HOME }),
88
+ ...(env.NOWWORK_ENV_TAG === undefined ? {} : { NOWWORK_ENV_TAG: env.NOWWORK_ENV_TAG }),
89
+ ...(env.NOWWORK_STORAGE_POLICY_PATH === undefined
90
+ ? {} : { NOWWORK_STORAGE_POLICY_PATH: env.NOWWORK_STORAGE_POLICY_PATH }),
91
+ };
92
+ }
85
93
  export async function runComputerCommand(argv, overrides = {}) {
86
94
  const command = argv[0];
87
95
  if (!command || !COMPUTER_COMMANDS.has(command))
@@ -202,6 +210,7 @@ export async function runComputerCommand(argv, overrides = {}) {
202
210
  ...deps,
203
211
  profile: restartProfile.name,
204
212
  profileHome: home,
213
+ environment: storageServiceEnvironment(deps.env),
205
214
  })
206
215
  : null;
207
216
  if (spec && restartProfile) {
@@ -272,7 +281,7 @@ export async function runComputerCommand(argv, overrides = {}) {
272
281
  throw new Error(td("Service lifecycle requires the built daemon entry (.js), not a TypeScript development entry"));
273
282
  }
274
283
  const spec = buildServiceSpec({
275
- ...deps, profile: name, profileHome: home,
284
+ ...deps, profile: name, profileHome: home, environment: storageServiceEnvironment(deps.env),
276
285
  });
277
286
  const installation = daemonGlobalInstallation(deps.entryPath, deps.platform);
278
287
  const managedRecord = installation !== null && spec.platform !== "win32"
@@ -53,6 +53,7 @@ export function windowsCommandArg(value) {
53
53
  }
54
54
  export function buildServiceSpec(input) {
55
55
  const { platform, profile, userHome } = input;
56
+ const environment = input.environment ?? process.env;
56
57
  if (platform !== "darwin" && platform !== "linux" && platform !== "win32") {
57
58
  throw new Error(`Computer services are not supported on platform '${platform}'`);
58
59
  }
@@ -72,6 +73,16 @@ export function buildServiceSpec(input) {
72
73
  throw new Error("Cannot install launchd service without a numeric uid");
73
74
  const id = `com.nowcrew.daemon.${profile}${identitySuffix}`;
74
75
  const args = daemonCommand.map((arg) => ` <string>${xml(arg)}</string>`).join("\n");
76
+ const environmentEntries = [
77
+ ["NOWWORK_HOME", environment.NOWWORK_HOME],
78
+ ["NOWWORK_ENV_TAG", environment.NOWWORK_ENV_TAG],
79
+ ["NOWWORK_STORAGE_POLICY_PATH", environment.NOWWORK_STORAGE_POLICY_PATH],
80
+ ].flatMap(([key, value]) => value === undefined
81
+ ? []
82
+ : [` <key>${key}</key><string>${xml(assertSingleLine(value, key))}</string>`]);
83
+ const environmentBlock = environmentEntries.length === 0
84
+ ? ""
85
+ : ` <key>EnvironmentVariables</key>\n <dict>\n${environmentEntries.join("\n")}\n </dict>\n`;
75
86
  return {
76
87
  platform,
77
88
  profile,
@@ -79,7 +90,7 @@ export function buildServiceSpec(input) {
79
90
  descriptorPath: resolve(userHome, "Library", "LaunchAgents", `${id}.plist`),
80
91
  daemonCommand,
81
92
  managerDomain: `gui/${input.uid}`,
82
- descriptor: `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n <key>Label</key><string>${xml(id)}</string>\n <key>ProgramArguments</key>\n <array>\n${args}\n </array>\n <key>KeepAlive</key><true/>\n <key>ProcessType</key><string>Background</string>\n</dict>\n</plist>\n`,
93
+ descriptor: `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n <key>Label</key><string>${xml(id)}</string>\n <key>ProgramArguments</key>\n <array>\n${args}\n </array>\n${environmentBlock} <key>KeepAlive</key><true/>\n <key>ProcessType</key><string>Background</string>\n</dict>\n</plist>\n`,
83
94
  };
84
95
  }
85
96
  if (platform === "linux") {
@@ -266,6 +277,35 @@ export async function installService(spec, runner = systemCommandRunner) {
266
277
  throw error;
267
278
  }
268
279
  }
280
+ export async function replaceInstalledServiceDescriptor(spec, previousDescriptor, runner = systemCommandRunner) {
281
+ if (spec.platform === "win32" || spec.descriptorPath === null || spec.descriptor === null) {
282
+ throw new Error("Installed service descriptor replacement requires launchd or systemd");
283
+ }
284
+ const current = await readServiceDescriptor(spec);
285
+ if (current !== previousDescriptor) {
286
+ throw new Error(`Installed descriptor for '${spec.id}' changed before replacement`);
287
+ }
288
+ const status = await serviceStatus(spec, runner);
289
+ if (!status.installed)
290
+ throw new Error(`Service '${spec.id}' is not installed`);
291
+ if (status.running || status.loaded)
292
+ await serviceAction(spec, "stop", runner);
293
+ try {
294
+ await writePrivateAtomic(spec.descriptorPath, spec.descriptor);
295
+ if (spec.platform === "linux") {
296
+ await checked(runner, "systemctl", ["--user", "daemon-reload"], "systemd reload");
297
+ await checked(runner, "systemctl", ["--user", "enable", spec.id], "systemd enable");
298
+ }
299
+ if (status.running)
300
+ await serviceAction(spec, "start", runner);
301
+ }
302
+ catch (error) {
303
+ await writePrivateAtomic(spec.descriptorPath, previousDescriptor).catch(() => undefined);
304
+ if (status.running)
305
+ await serviceAction(spec, "start", runner).catch(() => undefined);
306
+ throw error;
307
+ }
308
+ }
269
309
  export async function ensureServiceEnabled(spec, runner = systemCommandRunner) {
270
310
  if (spec.platform === "linux") {
271
311
  if (spec.descriptorPath === null || !await pathExists(spec.descriptorPath)) {
package/dist/config.js CHANGED
@@ -59,6 +59,40 @@ function defaultCliPath() {
59
59
  return resolve(here, "../../cli/dist/main.js");
60
60
  }
61
61
  }
62
+ // 临时 dev 验收名单。测试完成后删除此常量,恢复未配置时默认关闭的行为。
63
+ const TEMP_LOCAL_MEMORY_V2_TEST_HANDLES = [
64
+ "memory-v2-test-a",
65
+ "memory-v2-test-b",
66
+ "memory-v2-test-c",
67
+ ];
68
+ function isTemporaryDevServer(serverUrl) {
69
+ try {
70
+ const hostname = new URL(serverUrl).hostname.toLowerCase();
71
+ return hostname === "localhost"
72
+ || hostname === "127.0.0.1"
73
+ || hostname === "::1"
74
+ || hostname === "nowwork-dev.nowcoder.com";
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ function localMemoryV2Handles(env, serverUrl) {
81
+ const configured = env.CREW_LOCAL_MEMORY_V2_HANDLES;
82
+ if (configured === undefined) {
83
+ return isTemporaryDevServer(serverUrl)
84
+ ? new Set(TEMP_LOCAL_MEMORY_V2_TEST_HANDLES)
85
+ : new Set();
86
+ }
87
+ const raw = configured.trim();
88
+ if (raw === "")
89
+ return new Set();
90
+ const handles = raw.split(",").map((value) => value.trim()).filter(Boolean);
91
+ if (handles.some((handle) => !/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(handle))) {
92
+ throw new ConfigError("CREW_LOCAL_MEMORY_V2_HANDLES contains an invalid agent handle");
93
+ }
94
+ return new Set(handles);
95
+ }
62
96
  export const DEFAULT_SERVER_URL = "http://127.0.0.1:3001";
63
97
  function codexHomeModeEnv(env) {
64
98
  const value = env.CREW_CODEX_HOME_MODE;
@@ -68,6 +102,14 @@ function codexHomeModeEnv(env) {
68
102
  return "private";
69
103
  throw new ConfigError("CREW_CODEX_HOME_MODE must be shared or private");
70
104
  }
105
+ function codexCollaborationToolsEnv(env) {
106
+ const value = (env.CREW_CODEX_COLLABORATION_TOOLS ?? "").trim().toLowerCase();
107
+ if (value === "" || value === "0" || value === "false" || value === "off")
108
+ return false;
109
+ if (value === "1" || value === "true" || value === "on")
110
+ return true;
111
+ throw new ConfigError("CREW_CODEX_COLLABORATION_TOOLS must be one of 1/true/on/0/false/off");
112
+ }
71
113
  export function loadConfig(env = process.env) {
72
114
  const serverUrl = (env.CREW_SERVER_URL ?? DEFAULT_SERVER_URL).replace(/\/+$/, "");
73
115
  const machineToken = env.CREW_MACHINE_TOKEN ?? "";
@@ -99,6 +141,7 @@ export function loadConfig(env = process.env) {
99
141
  cliPath: env.CREW_CLI_PATH ?? defaultCliPath(),
100
142
  runtimeBin: env.CREW_RUNTIME ?? "claude",
101
143
  codexHomeMode: codexHomeModeEnv(env),
144
+ codexCollaborationTools: codexCollaborationToolsEnv(env),
102
145
  dangerous: env.CREW_RUNTIME_SAFE !== "1", // 默认开启 (headless agent 在自有 workspace 内运行)
103
146
  resume: env.CREW_RESUME !== "off" && env.CREW_RESUME !== "0", // 默认开启;一键回退现状用 CREW_RESUME=off
104
147
  resumeWarmMs: env.CREW_RESUME_WARM_MS != null ? Number(env.CREW_RESUME_WARM_MS) : 3_600_000, // 默认 1h
@@ -108,6 +151,7 @@ export function loadConfig(env = process.env) {
108
151
  sessionMaxTurns: env.CREW_SESSION_MAX_TURNS != null ? Number(env.CREW_SESSION_MAX_TURNS) : 30,
109
152
  productName: env.CREW_PRODUCT_NAME ?? "nowwork",
110
153
  agentMemory: loadAgentMemoryConfig(env),
154
+ localMemoryV2Handles: localMemoryV2Handles(env, serverUrl),
111
155
  executionLimits,
112
156
  };
113
157
  }
@@ -10,6 +10,7 @@ import { buildServiceSpec, captureExactService, serviceStatus, systemCommandRunn
10
10
  import { readManagedServiceRegistry } from "./managed-service-registry.js";
11
11
  import { uninstallManagedService } from "./managed-service-lifecycle.js";
12
12
  import { resolveAutomaticInstallTarget } from "./automatic-install-target.js";
13
+ import { buildStorageMigrationMappings, createSafeServiceManifest, CredentialMaterialError, } from "./storage-migration-manifest.js";
13
14
  const MigrationMessageSchema = z.object({
14
15
  type: z.literal("daemon:migrate"),
15
16
  migrationId: z.string().uuid(),
@@ -47,33 +48,30 @@ async function hashedFiles(root, directory = root) {
47
48
  return files;
48
49
  }
49
50
  async function backupAndCopy(input, id, targetHome, targetAgents, service) {
51
+ // Validate and project service identity before creating any backup bytes. A descriptor containing
52
+ // a token must stop the migration before it can be copied into the local backup root.
53
+ const safeService = createSafeServiceManifest({
54
+ spec: service.spec,
55
+ snapshot: service.snapshot,
56
+ managedRecord: service.managedRecord,
57
+ });
50
58
  const backup = migrationBackupRoot(input, id);
51
59
  await mkdir(backup, { recursive: true, mode: 0o700 });
52
60
  await cp(input.currentDaemonHome, resolve(backup, "daemon-home"), { recursive: true, force: false });
53
61
  await cp(input.currentAgentsRoot, resolve(backup, "agents-root"), { recursive: true, force: false });
54
62
  await cp(input.currentDaemonHome, targetHome, { recursive: true, force: false });
55
63
  await cp(input.currentAgentsRoot, targetAgents, { recursive: true, force: false });
56
- const files = [
57
- ...(await hashedFiles(resolve(backup, "daemon-home"))).map((file) => ({ ...file, path: `daemon-home/${file.path}` })),
58
- ...(await hashedFiles(resolve(backup, "agents-root"))).map((file) => ({ ...file, path: `agents-root/${file.path}` })),
59
- ];
64
+ const daemonFiles = await hashedFiles(resolve(backup, "daemon-home"));
65
+ const agentFiles = await hashedFiles(resolve(backup, "agents-root"));
66
+ const mappings = buildStorageMigrationMappings({ daemonFiles, agentFiles });
60
67
  await writeFile(resolve(backup, "manifest.json"), `${JSON.stringify({
61
- version: 1,
68
+ version: 2,
62
69
  migrationId: id,
63
70
  profile: input.profileName,
64
71
  daemonHome: input.currentDaemonHome,
65
72
  agentsRoot: input.currentAgentsRoot,
66
- service: {
67
- id: service.spec.id,
68
- platform: service.spec.platform,
69
- descriptorPath: service.spec.descriptorPath,
70
- descriptor: service.spec.descriptor,
71
- daemonCommand: service.spec.daemonCommand,
72
- installed: service.snapshot.installed,
73
- running: service.snapshot.running,
74
- managedRecord: service.managedRecord,
75
- },
76
- files,
73
+ service: safeService,
74
+ mappings,
77
75
  }, null, 2)}\n`, { mode: 0o600 });
78
76
  return backup;
79
77
  }
@@ -147,7 +145,7 @@ export function createDaemonMigrationController(input) {
147
145
  }
148
146
  }
149
147
  catch (error) {
150
- sendFailure(message.migrationId, "target_conflict", error);
148
+ sendFailure(message.migrationId, error instanceof CredentialMaterialError ? "credential_material" : "target_conflict", error);
151
149
  return;
152
150
  }
153
151
  input.sendStatus({ type: "daemon:migration-status", migrationId: message.migrationId, status: "backing_up" });
@@ -158,7 +156,7 @@ export function createDaemonMigrationController(input) {
158
156
  }
159
157
  catch (error) {
160
158
  await Promise.allSettled([rm(targetPath, { recursive: true, force: true }), rm(targetAgents, { recursive: true, force: true })]);
161
- sendFailure(message.migrationId, "backup_failed", error);
159
+ sendFailure(message.migrationId, error instanceof CredentialMaterialError ? "credential_material" : "backup_failed", error);
162
160
  return;
163
161
  }
164
162
  const install = input.install ?? installDaemonAutomatically;
@@ -299,12 +299,6 @@ class ExecutionCancelledError extends Error {
299
299
  this.name = "ExecutionCancelledError";
300
300
  }
301
301
  }
302
- class ExecutionQueueTimeoutError extends Error {
303
- constructor(timeoutMs) {
304
- super(`Execution queue wait exceeded ${timeoutMs}ms`);
305
- this.name = "ExecutionQueueTimeoutError";
306
- }
307
- }
308
302
  async function cancellable(promise, cancellation) {
309
303
  if (cancellation === undefined)
310
304
  return promise;
@@ -315,24 +309,9 @@ async function cancellable(promise, cancellation) {
315
309
  cancellation.requested.then(() => { throw new ExecutionCancelledError(); }),
316
310
  ]);
317
311
  }
318
- async function cancellableWithTimeout(promise, timeoutMs, cancellation, timeoutError) {
319
- let timer;
320
- try {
321
- return await Promise.race([
322
- cancellable(promise, cancellation),
323
- new Promise((_resolve, reject) => {
324
- timer = setTimeout(() => reject(timeoutError), timeoutMs);
325
- }),
326
- ]);
327
- }
328
- finally {
329
- if (timer !== undefined)
330
- clearTimeout(timer);
331
- }
332
- }
333
312
  function failedCompletion(spec, error, startedAt, finishedAt) {
334
313
  const message = error instanceof Error ? error.message : String(error);
335
- const errorCode = error instanceof ExecutionQueueTimeoutError || error instanceof HostReservationCancelledError
314
+ const errorCode = error instanceof HostReservationCancelledError
336
315
  ? "queue_timeout"
337
316
  : error instanceof ProjectContextUnavailableError
338
317
  ? error.code
@@ -519,7 +498,9 @@ export async function runExecution(config, input, dependencies) {
519
498
  });
520
499
  try {
521
500
  if (dependencies.slot !== undefined) {
522
- await cancellableWithTimeout(dependencies.slot.ready, config.executionLimits.maxQueueWaitMs, dependencies.cancellation, new ExecutionQueueTimeoutError(config.executionLimits.maxQueueWaitMs));
501
+ // Queueing is not a liveness failure. Keep the execution durable until its
502
+ // machine/host slot is granted or the server explicitly cancels it.
503
+ await cancellable(dependencies.slot.ready, dependencies.cancellation);
523
504
  }
524
505
  let projectContext;
525
506
  let sessionContextFingerprint;
@@ -562,17 +543,15 @@ export async function runExecution(config, input, dependencies) {
562
543
  if (cancel !== null && timeout === undefined && !dependencies.cancellation?.isRequested()) {
563
544
  const firstOutputDeadline = config.executionLimits.maxFirstOutputWaitMs
564
545
  + config.executionLimits.firstOutputGraceMs;
565
- if (effectiveTimeoutMs > firstOutputDeadline) {
566
- firstOutputTimer = setTimeout(() => {
567
- firstOutputTimedOut = true;
568
- try {
569
- void cancel().catch(rejectCancellationFailure);
570
- }
571
- catch (error) {
572
- rejectCancellationFailure(error);
573
- }
574
- }, firstOutputDeadline);
575
- }
546
+ firstOutputTimer = setTimeout(() => {
547
+ firstOutputTimedOut = true;
548
+ try {
549
+ void cancel().catch(rejectCancellationFailure);
550
+ }
551
+ catch (error) {
552
+ rejectCancellationFailure(error);
553
+ }
554
+ }, firstOutputDeadline);
576
555
  timeout = setTimeout(() => {
577
556
  timedOut = true;
578
557
  try {
@@ -796,7 +775,7 @@ export async function runExecution(config, input, dependencies) {
796
775
  const baseSystemPromptBudget = spec.agent.abilityRelease === undefined
797
776
  ? systemPromptBudget
798
777
  : Math.max(0, Math.min(systemPromptBudget, Math.max(32 * 1024, systemPromptBudget - 168 * 1024)));
799
- const systemPromptWithLocalFacts = withLocalExecutionFacts(spec.instructions.systemPrompt, baseSystemPromptBudget);
778
+ const systemPromptWithLocalFacts = withLocalExecutionFacts(spec.instructions.systemPrompt, baseSystemPromptBudget, { collaborationTools: config.codexCollaborationTools && spec.runtime.name === "codex" });
800
779
  const boundedSystemPrompt = (context) => appendAgentMemoryContext(typeof systemPromptWithLocalFacts === "string"
801
780
  ? systemPromptWithLocalFacts
802
781
  : systemPromptWithLocalFacts(context), recalledMemory, baseSystemPromptBudget);
@@ -822,6 +801,7 @@ export async function runExecution(config, input, dependencies) {
822
801
  name: spec.runtime.name,
823
802
  ...(spec.runtime.model === undefined ? {} : { model: spec.runtime.model }),
824
803
  ...(spec.runtime.reasoning === undefined ? {} : { reasoning: spec.runtime.reasoning }),
804
+ codexCollaborationTools: config.codexCollaborationTools,
825
805
  },
826
806
  effectivePermission: permission,
827
807
  captureFinal: spec.reporting.captureFinal,
@@ -847,6 +827,7 @@ export async function runExecution(config, input, dependencies) {
847
827
  softTokens: config.sessionSoftTokens,
848
828
  maxTurns: config.sessionMaxTurns,
849
829
  },
830
+ localMemoryV2Enabled: config.localMemoryV2Handles?.has(spec.agent.handle) === true,
850
831
  };
851
832
  const result = await Promise.race([
852
833
  execute(localInput, callbacks, localDependencies),
@@ -4,7 +4,7 @@
4
4
  * server 提示词是权威部分,绝不裁剪;项目工作区提示词是绑定执行的强制部分,放不下就直接失败;
5
5
  * 只有本机事实(CREW_HOME / work-log / MEMORY 注入)是可选增强,按整段丢弃或按字节截断。
6
6
  */
7
- import { buildRuntimeWorkspacePrompt, capMemoryForInject, capWorkLogForInject, } from "./prompt.js";
7
+ import { buildCollaborationToolsPrompt, buildRuntimeWorkspacePrompt, capMemoryForInject, capWorkLogForInject, } from "./prompt.js";
8
8
  export function truncateUtf8(value, maxBytes) {
9
9
  if (maxBytes <= 0)
10
10
  return "";
@@ -30,10 +30,11 @@ function takeCompletePromptSections(sections, maxBytes) {
30
30
  }, { value: "", bytes: 0, full: true }).value;
31
31
  }
32
32
  /** v1 server instructions opt in to daemon-owned paths and bounded local workspace context. */
33
- export function withLocalExecutionFacts(serverPrompt, maxBytes) {
33
+ export function withLocalExecutionFacts(serverPrompt, maxBytes, options = {}) {
34
34
  return ({ workspace, resuming, projectContext }) => {
35
35
  const memory = capMemoryForInject(workspace.memory);
36
36
  const workLog = resuming ? "" : capWorkLogForInject(workspace.workLog);
37
+ const collaborationPrompt = options.collaborationTools === true ? buildCollaborationToolsPrompt() : "";
37
38
  const mandatoryWorkspacePrompt = buildRuntimeWorkspacePrompt(projectContext);
38
39
  const localFactSections = [`\n\n## Local execution facts
39
40
  - $CREW_HOME: ${workspace.dir}
@@ -41,17 +42,21 @@ export function withLocalExecutionFacts(serverPrompt, maxBytes) {
41
42
  ...(memory ? [`\n\n## Injected MEMORY.md (bounded local context)\n${memory}`] : []),
42
43
  ...(workLog ? [`\n\n## Injected work-log (bounded local context)\n${workLog}`] : []),
43
44
  ];
44
- const localFacts = localFactSections.join("");
45
+ const serverPromptBytes = Buffer.byteLength(serverPrompt, "utf8");
46
+ const collaborationBytes = Buffer.byteLength(collaborationPrompt, "utf8");
47
+ const mandatoryBytes = Buffer.byteLength(mandatoryWorkspacePrompt, "utf8");
48
+ const requiredBytes = serverPromptBytes + collaborationBytes + mandatoryBytes;
49
+ // 必需预算与 projectContext 无关:协作段落永远参与;无协作时与旧实现逐字节等价。
50
+ if (collaborationPrompt !== "" && requiredBytes > maxBytes) {
51
+ throw new Error("collaboration_tools_prompt_budget_exceeded");
52
+ }
45
53
  if (mandatoryWorkspacePrompt !== "") {
46
- const serverPromptBytes = Buffer.byteLength(serverPrompt, "utf8");
47
- const mandatoryBytes = Buffer.byteLength(mandatoryWorkspacePrompt, "utf8");
48
- const requiredBytes = serverPromptBytes + mandatoryBytes;
49
54
  if (requiredBytes > maxBytes)
50
55
  throw new Error("project_workspace_prompt_budget_exceeded");
51
56
  const optionalFacts = takeCompletePromptSections(localFactSections, maxBytes - requiredBytes);
52
- return `${serverPrompt}${optionalFacts}${mandatoryWorkspacePrompt}`;
57
+ return `${serverPrompt}${collaborationPrompt}${optionalFacts}${mandatoryWorkspacePrompt}`;
53
58
  }
54
- const remaining = maxBytes - Buffer.byteLength(serverPrompt, "utf8");
55
- return `${serverPrompt}${truncateUtf8(localFacts, remaining)}`;
59
+ const remaining = maxBytes - requiredBytes;
60
+ return `${serverPrompt}${collaborationPrompt}${truncateUtf8(localFactSections.join(""), remaining)}`;
56
61
  };
57
62
  }