@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.
- package/dist/agent-ability/materializer.js +20 -1
- package/dist/agent-ability/resolver.js +7 -10
- 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 +3 -1
- 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 +232 -0
- package/dist/supervised-runtime.js +3 -0
- package/dist/workspace.js +24 -1
- 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
|
-
|
|
13
|
-
|
|
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 >
|
|
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 >
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
@@ -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
|
|
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
|
}
|
package/dist/local-executor.js
CHANGED
|
@@ -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
|
|
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
|
-
(
|
|
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
|