@nowcrew/daemon 0.6.66 → 0.6.67

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/dist/agent-ability/materializer.js +20 -1
  2. package/dist/agent-ability/resolver.js +7 -10
  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 +3 -1
  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 +232 -0
  55. package/dist/supervised-runtime.js +3 -0
  56. package/dist/workspace.js +24 -1
  57. package/package.json +1 -1
@@ -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 });
@@ -9,8 +9,8 @@ import { abilityWorkspaceProjectionPath, DaemonAbilityLockSchema, DaemonAbilityM
9
9
  import { PROJECT_MANIFEST_SOURCE_PATH, ProjectDependencyManifestSchema, } from "../project-dependency-manifest.js";
10
10
  const runFile = promisify(execFile);
11
11
  const MAX_FILES = 5_000;
12
- export const AGENT_ABILITY_MAX_TOTAL_BYTES = 200 * 1024 * 1024;
13
- export const AGENT_ABILITY_MAX_FILE_BYTES = 100 * 1024 * 1024;
12
+ const MAX_TOTAL_BYTES = 25 * 1024 * 1024;
13
+ const MAX_FILE_BYTES = 2 * 1024 * 1024;
14
14
  const MAX_INSTRUCTIONS_BYTES = 96 * 1024;
15
15
  const MAX_MEMORY_BYTES = 64 * 1024;
16
16
  const AUTHORING_ONLY_ROOTS = new Set([".claude"]);
@@ -114,11 +114,10 @@ export const collectAbilityFiles = async (root) => {
114
114
  if (info.isDirectory())
115
115
  await visit(absolute);
116
116
  else if (info.isFile()) {
117
- if (info.size > AGENT_ABILITY_MAX_FILE_BYTES) {
117
+ if (info.size > MAX_FILE_BYTES)
118
118
  throw new Error(`ability_file_size_exceeded:${relative(root, absolute)}`);
119
- }
120
119
  totalBytes += info.size;
121
- if (totalBytes > AGENT_ABILITY_MAX_TOTAL_BYTES)
120
+ if (totalBytes > MAX_TOTAL_BYTES)
122
121
  throw new Error("ability_total_size_exceeded");
123
122
  if (files.length >= MAX_FILES)
124
123
  throw new Error("ability_file_count_exceeded");
@@ -252,7 +251,7 @@ export const validateAbilityDependencyDeclarations = (manifest, lock) => {
252
251
  throw new Error("ability_lock_has_undeclared_dependency");
253
252
  }
254
253
  };
255
- export const scanAbilityFiles = (files, manifest) => {
254
+ const scan = (files, manifest) => {
256
255
  const findings = [];
257
256
  const secrets = [
258
257
  /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/u,
@@ -260,9 +259,7 @@ export const scanAbilityFiles = (files, manifest) => {
260
259
  /\bAK(?:ID|IA)[A-Z0-9]{12,}\b/u,
261
260
  ];
262
261
  const executable = new Set([".exe", ".dll", ".dylib", ".so", ".bin", ".class", ".jar"]);
263
- const allowedBinaryDocuments = new Set([
264
- ".pdf", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".xlsx", ".xls", ".docx", ".pptx", ".ttf", ".otf",
265
- ]);
262
+ const allowedBinaryDocuments = new Set([".pdf", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".xlsx", ".xls", ".docx", ".pptx"]);
266
263
  for (const file of files) {
267
264
  let text;
268
265
  try {
@@ -385,7 +382,7 @@ export async function resolveAbilityRelease(agentsRoot, command) {
385
382
  }
386
383
  const externalPaths = await verifyExternalDependencies(cacheRoot, resolved.root, lock);
387
384
  files = await collectAbilityFiles(resolved.root);
388
- const security = scanAbilityFiles(files, manifest);
385
+ const security = scan(files, manifest);
389
386
  const kinds = new Set(lock.assets.map((asset) => asset.kind));
390
387
  const memory = [];
391
388
  for (const [layer, mounts] of [["release-policy", manifest.spec.memory.policy], ["release-seed", manifest.spec.memory.seeds]]) {
@@ -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;
@@ -775,7 +775,7 @@ export async function runExecution(config, input, dependencies) {
775
775
  const baseSystemPromptBudget = spec.agent.abilityRelease === undefined
776
776
  ? systemPromptBudget
777
777
  : Math.max(0, Math.min(systemPromptBudget, Math.max(32 * 1024, systemPromptBudget - 168 * 1024)));
778
- const systemPromptWithLocalFacts = withLocalExecutionFacts(spec.instructions.systemPrompt, baseSystemPromptBudget);
778
+ const systemPromptWithLocalFacts = withLocalExecutionFacts(spec.instructions.systemPrompt, baseSystemPromptBudget, { collaborationTools: config.codexCollaborationTools && spec.runtime.name === "codex" });
779
779
  const boundedSystemPrompt = (context) => appendAgentMemoryContext(typeof systemPromptWithLocalFacts === "string"
780
780
  ? systemPromptWithLocalFacts
781
781
  : systemPromptWithLocalFacts(context), recalledMemory, baseSystemPromptBudget);
@@ -801,6 +801,7 @@ export async function runExecution(config, input, dependencies) {
801
801
  name: spec.runtime.name,
802
802
  ...(spec.runtime.model === undefined ? {} : { model: spec.runtime.model }),
803
803
  ...(spec.runtime.reasoning === undefined ? {} : { reasoning: spec.runtime.reasoning }),
804
+ codexCollaborationTools: config.codexCollaborationTools,
804
805
  },
805
806
  effectivePermission: permission,
806
807
  captureFinal: spec.reporting.captureFinal,
@@ -826,6 +827,7 @@ export async function runExecution(config, input, dependencies) {
826
827
  softTokens: config.sessionSoftTokens,
827
828
  maxTurns: config.sessionMaxTurns,
828
829
  },
830
+ localMemoryV2Enabled: config.localMemoryV2Handles?.has(spec.agent.handle) === true,
829
831
  };
830
832
  const result = await Promise.race([
831
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
  }
@@ -31,6 +31,7 @@ import { diffMemoryPruneNotes, evaluateMemoryPrunePostcondition, inspectMemoryPr
31
31
  import { createLocalMemoryTelemetry, logLocalMemoryContextPrepareFailure, logLocalMemoryDiagnosticsFailure, } from "./local-memory-telemetry.js";
32
32
  import { CodexStartupStageParser } from "./codex-startup-stage.js";
33
33
  import { formatRuntimeStartupError } from "./runtime-startup-error.js";
34
+ import { commitLocalMemoryResultAfterExit, initializeLocalMemoryV2ForWorkspace, isLocalMemoryV2Eligible, localMemoryResultPath as makeLocalMemoryResultPath, renderLocalMemoryResultInstructions, } from "./local-memory/runtime.js";
34
35
  function memoryPruneFileFactFields(fileFacts) {
35
36
  const fields = {};
36
37
  for (const [label, fact] of Object.entries(fileFacts)) {
@@ -77,6 +78,7 @@ const RUNTIME_RUNNING_CALLBACK_TIMEOUT_MS = 30_000;
77
78
  const MEMORY_PRUNE_DIAGNOSTICS_TIMEOUT_MS = 2_000;
78
79
  const LOCAL_MEMORY_DIAGNOSTICS_TIMEOUT_MS = 250;
79
80
  const CLAUDE_INSTRUCTION_WARNING = "claude_additional_directory_instructions_unverified";
81
+ const CODEX_COLLABORATION_EXEC_WARNING = "codex_collaboration_disabled_on_exec";
80
82
  const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
81
83
  const RESERVED_ENV = new Set([
82
84
  "PATH",
@@ -277,6 +279,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
277
279
  }, "workspace_prepare", error);
278
280
  throw error;
279
281
  }
282
+ const localMemoryV2Initialized = await initializeLocalMemoryV2ForWorkspace(workspace, input.localMemoryV2Enabled === true);
280
283
  let materialized = null;
281
284
  let knownAttachmentDirectory = null;
282
285
  const startupReservationState = { current: null };
@@ -286,6 +289,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
286
289
  let memoryPruneFailurePhase = "diagnostics_before";
287
290
  let memoryPruneBeforeSnapshot = null;
288
291
  let memoryPruneSharedWriteKey = null;
292
+ let localMemoryResultPath = null;
289
293
  let executionWorkspace = workspace;
290
294
  let localMemoryTelemetry = null;
291
295
  const inheritedEnv = { ...process.env };
@@ -371,7 +375,6 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
371
375
  nearBudget,
372
376
  ...(input.projectContext === undefined ? {} : { projectContext: input.projectContext }),
373
377
  };
374
- const systemPrompt = resolvePrompt(input.systemPrompt, promptContext);
375
378
  if (input.attachments && input.attachments.length > 0) {
376
379
  if (dependencies.cancellation?.isRequested())
377
380
  throw new RuntimeCancelledError();
@@ -405,6 +408,29 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
405
408
  const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
406
409
  const wakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
407
410
  memoryPruneTraceId = parseMemoryPruneTraceId(wakePrompt);
411
+ if (input.localMemoryV2Enabled === true && memoryPruneTraceId !== null
412
+ && isLocalMemoryV2Eligible({
413
+ enabledForHandle: true,
414
+ agentHomePreexisted: workspace.agentHomePreexisted === true,
415
+ memorySeedCreated: executionWorkspace.memorySeedCreated === true && (localMemoryV2Initialized || workspace.localMemoryV2Initialized !== false),
416
+ localMemoryV2Initialized,
417
+ memoryPrune: true,
418
+ })) {
419
+ localMemoryResultPath = makeLocalMemoryResultPath(executionWorkspace.runDir, input.executionId);
420
+ }
421
+ const baseSystemPrompt = resolvePrompt(input.systemPrompt, promptContext);
422
+ const structuredMemoryInstructions = localMemoryResultPath === null
423
+ ? null
424
+ : renderLocalMemoryResultInstructions(localMemoryResultPath);
425
+ const structuredPromptFits = structuredMemoryInstructions === null
426
+ || input.maxSystemPromptBytes === undefined
427
+ || Buffer.byteLength(baseSystemPrompt, "utf8") + Buffer.byteLength(structuredMemoryInstructions, "utf8")
428
+ <= input.maxSystemPromptBytes;
429
+ if (!structuredPromptFits)
430
+ localMemoryResultPath = null;
431
+ const systemPrompt = localMemoryResultPath === null
432
+ ? baseSystemPrompt
433
+ : `${baseSystemPrompt}${structuredMemoryInstructions ?? ""}`;
408
434
  try {
409
435
  localMemoryTelemetry = createLocalMemoryTelemetry({
410
436
  executionId: input.executionId,
@@ -513,7 +539,13 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
513
539
  ability_release_id: input.abilityRelease?.releaseId,
514
540
  ability_root_commit: input.abilityRelease?.rootCommit,
515
541
  });
516
- const launchRuntime = dependencies.launchRuntime ?? launchLegacyRuntime;
542
+ const warn = dependencies.warnRuntime ?? warnRuntimeLocally;
543
+ const launchRuntime = dependencies.launchRuntime ?? (async (launchReq) => {
544
+ if (launchReq.runtime === "codex" && launchReq.codexCollaborationTools === true) {
545
+ warn(CODEX_COLLABORATION_EXEC_WARNING);
546
+ }
547
+ return launchLegacyRuntime(launchReq);
548
+ });
517
549
  if (dependencies.cancellation?.isRequested())
518
550
  throw new RuntimeCancelledError();
519
551
  let activeProjectSkillRuntimeRoot;
@@ -582,7 +614,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
582
614
  }
583
615
  else {
584
616
  runtimeEnv = withoutClaudeInstructionFlag(childEnv);
585
- (dependencies.warnRuntime ?? warnRuntimeLocally)(CLAUDE_INSTRUCTION_WARNING);
617
+ warn(CLAUDE_INSTRUCTION_WARNING);
586
618
  callbacks.onConsole?.({
587
619
  stream: "system",
588
620
  text: `[runtime-warning] ${CLAUDE_INSTRUCTION_WARNING}`,
@@ -606,6 +638,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
606
638
  ...(launchReasoning === undefined ? {} : { reasoning: launchReasoning }),
607
639
  ...(launchSessionId === null ? {} : { sessionId: launchSessionId }),
608
640
  resume: resuming,
641
+ ...(runtime.codexCollaborationTools === undefined
642
+ ? {}
643
+ : { codexCollaborationTools: runtime.codexCollaborationTools }),
609
644
  ...(attachmentPlan.nativeImagePaths.length > 0
610
645
  ? { imagePaths: attachmentPlan.nativeImagePaths }
611
646
  : {}),
@@ -892,6 +927,24 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
892
927
  const { exitCode, spawnError, terminationSignal } = runtimeExit;
893
928
  localMemoryTelemetry?.markRuntimeExited(exitCode);
894
929
  memoryPruneRuntimeExitCode = exitCode;
930
+ if (localMemoryResultPath !== null) {
931
+ const committed = await commitLocalMemoryResultAfterExit({
932
+ resultPath: localMemoryResultPath,
933
+ agentDir: executionWorkspace.dir,
934
+ executionId: input.executionId,
935
+ pruneTraceId: memoryPruneTraceId,
936
+ exitCode,
937
+ });
938
+ if (committed.status === "failed") {
939
+ dslog("local_memory.commit_failed", "结构化本地记忆候选未落地,保留结果文件", {
940
+ level: "WARN",
941
+ execution_id: input.executionId,
942
+ agent_handle: input.handle,
943
+ error_code: committed.error,
944
+ result_path: localMemoryResultPath,
945
+ });
946
+ }
947
+ }
895
948
  const errorTail = [
896
949
  stderrTail.trim(),
897
950
  spawnError === undefined