@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.
- package/README.md +9 -8
- package/dist/agent-ability/materializer.js +20 -1
- package/dist/agent-ability/runtime.js +3 -0
- package/dist/automatic-install-target.js +15 -10
- package/dist/computer-cli.js +10 -1
- package/dist/computer-service.js +41 -1
- package/dist/config.js +44 -0
- package/dist/daemon-migration-controller.js +16 -18
- package/dist/execution-runner.js +16 -35
- package/dist/local-execution-prompt.js +14 -9
- package/dist/local-executor.js +56 -3
- package/dist/local-memory/index-budget.js +74 -0
- package/dist/local-memory/pending.js +111 -0
- package/dist/local-memory/runtime.js +87 -0
- package/dist/local-memory/types.js +1 -0
- package/dist/local-memory/write.js +448 -0
- package/dist/machine-info.js +3 -2
- package/dist/main.js +83 -0
- package/dist/managed-service-lifecycle.js +47 -2
- package/dist/managed-service-startup.js +2 -0
- package/dist/profile-layout.js +10 -4
- package/dist/project-skills/controller.js +2 -0
- package/dist/prompt.js +19 -1
- package/dist/runner.js +3 -0
- package/dist/runtimes/codex-app-server-runner.js +54 -14
- package/dist/runtimes/codex-command-safety.js +75 -0
- package/dist/runtimes/codex-contract.js +20 -5
- package/dist/runtimes/codex-turn-retry.js +69 -0
- package/dist/runtimes/codex.js +4 -0
- package/dist/serve.js +68 -3
- package/dist/storage-config.js +175 -0
- package/dist/storage-control.js +193 -0
- package/dist/storage-copy.js +609 -0
- package/dist/storage-credential-guard.js +21 -0
- package/dist/storage-doctor.js +228 -0
- package/dist/storage-identity.js +29 -0
- package/dist/storage-layout.js +143 -0
- package/dist/storage-manifest.js +855 -0
- package/dist/storage-migration-controller.js +417 -0
- package/dist/storage-migration-manifest.js +105 -0
- package/dist/storage-migration-policy.js +100 -0
- package/dist/storage-runtime-controller.js +339 -0
- package/dist/storage-runtime-readiness.js +184 -0
- package/dist/storage-runtime.js +39 -0
- package/dist/storage-safe-fs.js +302 -0
- package/dist/storage-serve-adapter.js +65 -0
- package/dist/storage-status-delivery.js +209 -0
- package/dist/storage-status-replay.js +125 -0
- package/dist/storage-target-confirmation.js +129 -0
- package/dist/storage-worker-command.js +27 -0
- package/dist/storage-worker-executor.js +384 -0
- package/dist/storage-worker-handoff.js +151 -0
- package/dist/storage-worker-operation.js +284 -0
- package/dist/storage-worker-status.js +105 -0
- package/dist/supervised-runtime.js +3 -0
- package/dist/workspace.js +24 -1
- 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
|
|
395
|
-
the daemon waits up to two minutes for the first model event, with a
|
|
396
|
-
slow providers. When
|
|
397
|
-
terminal result is written to the execution journal before it is
|
|
398
|
-
replayable instead of being silently dropped.
|
|
399
|
-
most twice (three total attempts). Each retry releases its current
|
|
400
|
-
new one, which puts it behind work already waiting in the queue. A
|
|
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
|
-
|
|
6
|
+
environment: "production",
|
|
7
7
|
daemonDirectory: "daemon",
|
|
8
8
|
agentsDirectory: "agents",
|
|
9
9
|
},
|
|
10
10
|
"nowwork-dev.nowcoder.com": {
|
|
11
|
-
|
|
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
|
-
|
|
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 === "
|
|
59
|
-
agentsRoot: paths.join(input.userHome, ".crew", environment === "
|
|
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 '${
|
|
66
|
+
throw new Error(`Server hostname '${new URL(input.serverUrl).hostname}' requires profile '${environment}'`);
|
|
65
67
|
}
|
|
66
68
|
const prefix = `nw-${environment}-`;
|
|
67
|
-
|
|
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.
|
|
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.
|
|
99
|
+
return knownTargetForProfile(known.environment, input.requestedProfile, input, parsed);
|
|
95
100
|
}
|
|
96
101
|
const profile = requireCustomProfile(hostname, input.requestedProfile);
|
|
97
102
|
return Object.freeze({
|
package/dist/computer-cli.js
CHANGED
|
@@ -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"
|
package/dist/computer-service.js
CHANGED
|
@@ -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
|
|
57
|
-
|
|
58
|
-
|
|
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:
|
|
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
|
-
|
|
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;
|
package/dist/execution-runner.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
|
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 -
|
|
55
|
-
return `${serverPrompt}${truncateUtf8(
|
|
59
|
+
const remaining = maxBytes - requiredBytes;
|
|
60
|
+
return `${serverPrompt}${collaborationPrompt}${truncateUtf8(localFactSections.join(""), remaining)}`;
|
|
56
61
|
};
|
|
57
62
|
}
|