@nowcrew/daemon 0.6.99-beta.2 → 0.6.99-beta.3

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/cloud-report-upload.js +1 -10
  2. package/dist/computer-service.js +5 -30
  3. package/dist/execution-journal.js +3 -1
  4. package/dist/execution-protocol.js +1 -1
  5. package/dist/execution-runner.js +4 -1
  6. package/dist/local-execution-prompt.js +8 -3
  7. package/dist/local-executor.js +64 -39
  8. package/dist/local-memory/commit-telemetry.js +30 -0
  9. package/dist/local-memory/execution-workspace.js +62 -0
  10. package/dist/local-memory/full-index-search.js +47 -0
  11. package/dist/local-memory/index-budget.js +9 -3
  12. package/dist/local-memory/initialization-identity.js +36 -0
  13. package/dist/local-memory/inventory-cli.js +52 -8
  14. package/dist/local-memory/inventory-source.js +15 -8
  15. package/dist/local-memory/inventory-v2.js +362 -0
  16. package/dist/local-memory/legacy-adapter.js +233 -0
  17. package/dist/local-memory/migration-access.js +242 -0
  18. package/dist/local-memory/migration-cli.js +54 -0
  19. package/dist/local-memory/migration-io.js +199 -0
  20. package/dist/local-memory/migration-pending.js +9 -0
  21. package/dist/local-memory/migration-plan.js +432 -0
  22. package/dist/local-memory/migration-schema.js +55 -0
  23. package/dist/local-memory/migration-state-invariants.js +65 -0
  24. package/dist/local-memory/migration-store.js +414 -0
  25. package/dist/local-memory/migration-tool-entry.js +21 -0
  26. package/dist/local-memory/pending-query.js +5 -2
  27. package/dist/local-memory/pending-schema.js +1 -1
  28. package/dist/local-memory/pending.js +16 -2
  29. package/dist/local-memory/prompt.js +21 -5
  30. package/dist/local-memory/read-cli.js +98 -76
  31. package/dist/local-memory/rollout.js +79 -11
  32. package/dist/local-memory/runtime.js +181 -13
  33. package/dist/local-memory/source-path.js +13 -0
  34. package/dist/local-memory/storage-format.js +249 -0
  35. package/dist/local-memory/write.js +189 -21
  36. package/dist/local-memory/writer-mode.js +13 -0
  37. package/dist/local-memory-diagnostics.js +96 -15
  38. package/dist/local-memory-telemetry.js +20 -3
  39. package/dist/machine-info.js +3 -0
  40. package/dist/marketplace-protocol.js +1 -1
  41. package/dist/marketplace-remote.js +1 -1
  42. package/dist/marketplace-runner.js +1 -1
  43. package/dist/memory-migration-controller.js +89 -0
  44. package/dist/memory-migration-inspect.js +108 -0
  45. package/dist/memory-migration-journal.js +14 -0
  46. package/dist/memory-migration-protocol.js +14 -0
  47. package/dist/memory-migration-runtime.js +305 -0
  48. package/dist/memory-prune-diagnostics.js +60 -4
  49. package/dist/message-file-preview.js +11 -1
  50. package/dist/prompt.js +9 -17
  51. package/dist/runner.js +1 -1
  52. package/dist/runtimes/claude-thread-runner.js +25 -4
  53. package/dist/serve.js +38 -3
  54. package/dist/token.js +1 -0
  55. package/dist/workspace-control-handler.js +16 -3
  56. package/dist/workspace.js +13 -2
  57. package/package.json +1 -1
@@ -1,6 +1,5 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { homedir, hostname, platform } from "node:os";
3
- import { join } from "node:path";
4
3
  const ALIYUN_MARKER = /alibaba|aliyun|alicloud/iu;
5
4
  function readMarker(path) {
6
5
  try {
@@ -29,15 +28,7 @@ export function detectCloudProvider(env = process.env, platformName = platform()
29
28
  }
30
29
  export function findOssUploadScript(env = process.env, exists = existsSync) {
31
30
  const explicit = env.CREW_OSS_UPLOAD_SCRIPT?.trim();
32
- if (explicit)
33
- return exists(explicit) ? explicit : undefined;
34
- const home = env.HOME?.trim() || homedir();
35
- const candidates = [
36
- join(home, ".claude", "skills", "oss-upload", "scripts", "oss_upload.py"),
37
- join(home, ".codex", "skills", "oss-upload", "scripts", "oss_upload.py"),
38
- join(home, ".agents", "skills", "oss-upload", "scripts", "oss_upload.py"),
39
- ];
40
- return candidates.find(exists);
31
+ return explicit && exists(explicit) ? explicit : undefined;
41
32
  }
42
33
  export function cloudReportUploadFacts(env = process.env, platformName = platform(), hostName = hostname(), readMarkerValue, exists) {
43
34
  const provider = detectCloudProvider(env, platformName, hostName, readMarkerValue);
@@ -12,8 +12,6 @@ import { powershellCommandArgs } from "./powershell-command.js";
12
12
  const execFileAsync = promisify(execFile);
13
13
  const WINDOWS_STOP_TIMEOUT_MS = 5_000;
14
14
  const WINDOWS_STOP_POLL_MS = 100;
15
- const DARWIN_STOP_TIMEOUT_MS = 30_000;
16
- const DARWIN_STOP_POLL_MS = 100;
17
15
  /** Windows cannot spawn .cmd/.bat shims directly with shell:false; route them through cmd.exe explicitly. */
18
16
  export function systemCommandInvocation(command, args, platform = process.platform, comSpec = process.env.ComSpec ?? process.env.COMSPEC ?? "cmd.exe") {
19
17
  if (platform === "win32" && /\.(?:cmd|bat)$/iu.test(command)) {
@@ -188,32 +186,6 @@ async function checked(runner, command, args, action) {
188
186
  }
189
187
  return result;
190
188
  }
191
- async function stopLaunchdService(spec, initial, runner) {
192
- const assertQueryable = (result) => {
193
- // launchctl uses 113 for an absent service. Other failures cannot prove it stopped.
194
- if (result.exitCode !== 0 && result.exitCode !== 113) {
195
- throw new Error(`launchd status failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.exitCode}`}`);
196
- }
197
- };
198
- assertQueryable(initial);
199
- if (initial.exitCode === 113)
200
- return;
201
- const target = `${spec.managerDomain}/${spec.id}`;
202
- await checked(runner, "launchctl", ["bootout", target], "launchd stop");
203
- const deadline = Date.now() + DARWIN_STOP_TIMEOUT_MS;
204
- while (true) {
205
- const current = await runner("launchctl", ["print", target]);
206
- assertQueryable(current);
207
- if (current.exitCode === 113)
208
- return;
209
- // bootout returns while the job is SIGTERMed and still owns its installation lease.
210
- // Wait for removal, not merely a non-running state, before installing or bootstrapping.
211
- if (Date.now() >= deadline) {
212
- throw new Error(`launchd service '${spec.id}' did not unload within ${DARWIN_STOP_TIMEOUT_MS}ms`);
213
- }
214
- await new Promise((resolve) => setTimeout(resolve, DARWIN_STOP_POLL_MS));
215
- }
216
- }
217
189
  function sameWindowsPath(left, right) {
218
190
  return left !== undefined
219
191
  && left.replaceAll("/", "\\").toLowerCase() === right.replaceAll("/", "\\").toLowerCase();
@@ -384,7 +356,9 @@ export async function uninstallService(spec, runner = systemCommandRunner) {
384
356
  }
385
357
  if (spec.platform === "darwin") {
386
358
  const status = await serviceStatus(spec, runner);
387
- await stopLaunchdService(spec, status, runner);
359
+ if (status.loaded) {
360
+ await checked(runner, "launchctl", ["bootout", `${spec.managerDomain}/${spec.id}`], "launchd bootout");
361
+ }
388
362
  }
389
363
  else {
390
364
  const disable = await runner("systemctl", ["--user", "disable", "--now", spec.id]);
@@ -411,7 +385,8 @@ export async function serviceAction(spec, action, runner = systemCommandRunner)
411
385
  await checked(runner, "launchctl", ["bootstrap", spec.managerDomain, spec.descriptorPath], "launchd start");
412
386
  }
413
387
  else if (action === "stop") {
414
- await stopLaunchdService(spec, status, runner);
388
+ if (status.loaded)
389
+ await checked(runner, "launchctl", ["bootout", target], "launchd stop");
415
390
  }
416
391
  else {
417
392
  if (status.loaded)
@@ -8,7 +8,7 @@ import { provePosixProcessGroupStopped } from "./execution-posix-stop-proof.js";
8
8
  export { JournalLockedError, JournalLockCorruptionError } from "./execution-journal-lock.js";
9
9
  const ExecutionIdSchema = z.string().uuid();
10
10
  const TimestampSchema = z.string().datetime({ offset: true });
11
- const RuntimeSchema = z.enum(["claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness", "pi"]);
11
+ const RuntimeSchema = z.enum(["claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness", "memory_migration", "pi"]);
12
12
  const RUNTIME_READY_SUFFIX = ".runtime-ready";
13
13
  const RawJournalEntrySchema = z.object({
14
14
  executionId: ExecutionIdSchema,
@@ -523,6 +523,8 @@ export function createExecutionJournal(agentsRoot, options = {}) {
523
523
  }
524
524
  };
525
525
  const pruneInternal = async () => {
526
+ if (options.retainCompleted)
527
+ return;
526
528
  const protectedExecutionIds = new Set(options.protectedExecutionIds?.() ?? []);
527
529
  const entries = await readAll();
528
530
  const acknowledged = entries
@@ -319,7 +319,7 @@ const RawExecutionCompletedSchema = z.object({
319
319
  providerErrorCategory: ProviderErrorCategorySchema.optional(),
320
320
  providerHttpStatus: ProviderHttpStatusSchema.optional(),
321
321
  providerErrorCode: ProviderErrorCodeSchema.optional(),
322
- runtime: RuntimeSchema,
322
+ runtime: z.union([RuntimeSchema, z.literal("memory_migration")]),
323
323
  model: z.string().optional(),
324
324
  resumed: z.boolean(),
325
325
  finalText: z.string().optional(),
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { isLocalMemoryV2Enabled, localMemoryV2Environment, resolveReposRoot } from "./config.js";
4
+ import { detectLocalMemoryStorageVersion } from "./local-memory/storage-format.js";
4
5
  import { DaemonToServerExecutionFrameSchema, ExecutionCompletedSchema, ExecutionRejectedSchema, ExecutionStartSchema, } from "./execution-protocol.js";
5
6
  import { JournalConflictError } from "./execution-journal.js";
6
7
  import { boundExecutionFrame } from "./execution-event-limit.js";
@@ -846,8 +847,9 @@ export async function runExecution(config, input, dependencies) {
846
847
  const localMemoryGuardInBase = localMemoryV2Environment(config.serverUrl) === "custom";
847
848
  const boundedSystemPrompt = (context) => {
848
849
  const enabled = context.workspace.localMemoryV2Enabled ?? localMemoryV2Enabled;
849
- const systemPromptWithLocalFacts = withLocalExecutionFacts(`${spec.instructions.systemPrompt}${enabled && localMemoryGuardInBase ? renderLocalMemoryV2WriteGuard() : ""}`, baseSystemPromptBudget, {
850
+ const systemPromptWithLocalFacts = withLocalExecutionFacts(`${spec.instructions.systemPrompt}${enabled && localMemoryGuardInBase && detectLocalMemoryStorageVersion(context.workspace.memory) !== 2 ? renderLocalMemoryV2WriteGuard() : ""}`, baseSystemPromptBudget, {
850
851
  ...(selfTraining ? { isolateLocalMemory: true } : {}),
852
+ memoryQuery: spec.instructions.wakePrompt,
851
853
  collaborationTools: config.codexCollaborationTools && spec.runtime.name === "codex",
852
854
  });
853
855
  const base = typeof systemPromptWithLocalFacts === "string"
@@ -887,6 +889,7 @@ export async function runExecution(config, input, dependencies) {
887
889
  serverUrl: config.serverUrl,
888
890
  token: credential.token,
889
891
  agentId: credential.agentId,
892
+ ...(dependencies.controlIdentity === undefined ? {} : dependencies.controlIdentity),
890
893
  agentsRoot: config.agentsRoot,
891
894
  reposRoot: resolveReposRoot(config.agentsRoot, config.reposRoot),
892
895
  ...(config.qaReportRoot === undefined ? {} : { qaReportRoot: config.qaReportRoot }),
@@ -2,8 +2,9 @@
2
2
  * 本机执行提示词的合成与预算裁剪。
3
3
  *
4
4
  * server 提示词是权威部分,绝不裁剪;项目工作区提示词是绑定执行的强制部分,放不下就直接失败;
5
- * 只有本机事实(CREW_HOME / work-log / MEMORY 注入)是可选增强,按整段丢弃或按字节截断。
5
+ * 本机事实与旧格式预览可按预算省略;schema2身份和置顶规则必须保留,放不下则拒绝执行。
6
6
  */
7
+ import { detectLocalMemoryStorageVersion } from "./local-memory/storage-format.js";
7
8
  import { buildCollaborationToolsPrompt, buildRuntimeWorkspacePrompt, capMemoryForInject, capWorkLogForInject, } from "./prompt.js";
8
9
  const trainingProjectJson = (project) => JSON.stringify({
9
10
  repositoryKey: project.repositoryKey,
@@ -57,10 +58,14 @@ export function withLocalExecutionFacts(serverPrompt, maxBytes, options = {}) {
57
58
  }
58
59
  const remaining = maxBytes - requiredBytes - Math.max(0, reservedPromptBytes);
59
60
  const factsBytes = Buffer.byteLength(localFacts, "utf8");
60
- if (factsBytes > remaining)
61
+ if (factsBytes > remaining) {
62
+ if (!options.isolateLocalMemory && detectLocalMemoryStorageVersion(workspace.memory) === 2) {
63
+ throw new Error("local_memory_identity_budget_exceeded");
64
+ }
61
65
  return `${serverPrompt}${collaborationPrompt}${mandatoryWorkspacePrompt}`;
66
+ }
62
67
  const memoryHeading = "\n\n## Injected MEMORY.md (bounded local context)\n";
63
- const memory = options.isolateLocalMemory ? "" : capMemoryForInject(workspace.memory, undefined, remaining - factsBytes - Buffer.byteLength(memoryHeading, "utf8"));
68
+ const memory = options.isolateLocalMemory ? "" : capMemoryForInject(workspace.memory, undefined, remaining - factsBytes - Buffer.byteLength(memoryHeading, "utf8"), options.memoryQuery);
64
69
  const memorySection = memory === "" ? "" : memoryHeading + memory;
65
70
  const workLogSection = workLog === "" ? "" : `\n\n## Injected work-log (bounded local context)\n${workLog}`;
66
71
  const workLogBudget = remaining - factsBytes - Buffer.byteLength(memorySection, "utf8");
@@ -6,7 +6,7 @@ import { createInterface } from "node:readline";
6
6
  import { readFile, rm, writeFile } from "node:fs/promises";
7
7
  import { join } from "node:path";
8
8
  import { cleanupSelfTrainingRunDir } from "./self-training-workspace.js";
9
- import { prepareWorkspace, rotateAgentSession, safeKey, } from "./workspace.js";
9
+ import { rotateAgentSession, safeKey, } from "./workspace.js";
10
10
  import { CLAUDE_ADDITIONAL_DIRECTORY_INSTRUCTIONS_ENV, isClaudeAdditionalDirectoryInstructionsSupported, probeClaudeVersion, spawnClaude, } from "./runtimes/claude.js";
11
11
  import { spawnCodex } from "./runtimes/codex.js";
12
12
  import { DEEPSEEK_CODEX_MODEL, DEEPSEEK_CODEX_REASONING_LEVELS, materializeDeepSeekCodexHome, } from "./runtimes/codex-deepseek-config.js";
@@ -39,13 +39,20 @@ import { projectSkillRuntimeDirectories, } from "./project-skills/runtime-launch
39
39
  import { logIdentityRosterRuntimePrepared } from "./identity-roster-telemetry.js";
40
40
  import { diffMemoryPruneNotes, evaluateMemoryPrunePostcondition, inspectMemoryPruneFilesWithinDeadline, logMemoryPruneDiagnosticsFailure, memoryPruneSnapshotFields, parseMemoryPruneTraceId, } from "./memory-prune-diagnostics.js";
41
41
  import { createLocalMemoryTelemetry, logLocalMemoryContextPrepareFailure, logLocalMemoryDiagnosticsFailure, } from "./local-memory-telemetry.js";
42
+ import { LOCAL_MEMORY_DIAGNOSTICS_TIMEOUT_MS } from "./local-memory-diagnostics.js";
42
43
  import { CodexStartupStageParser } from "./codex-startup-stage.js";
43
44
  import { formatRuntimeStartupError } from "./runtime-startup-error.js";
44
45
  import { logRuntimeEnvironmentContract } from "./runtime-environment-telemetry.js";
45
- import { commitLocalMemoryResultAfterExit, initializeLocalMemoryV2ForWorkspace, isLocalMemoryV2Eligible, localMemoryResultPath as makeLocalMemoryResultPath, } from "./local-memory/runtime.js";
46
+ import { commitLocalMemoryResultAfterExit, isLocalMemoryV2Eligible, localMemoryResultPath as makeLocalMemoryResultPath, } from "./local-memory/runtime.js";
46
47
  import { prepareLocalMemoryPrompt } from "./local-memory/prompt.js";
47
- import { reportLocalMemoryV2Deferred, resolveLocalMemoryV2Rollout } from "./local-memory/rollout.js";
48
+ import { reportLocalMemoryV2Deferred } from "./local-memory/rollout.js";
49
+ import { prepareExecutionMemoryWorkspace } from "./local-memory/execution-workspace.js";
48
50
  import { localMemoryV2Environment } from "./config.js";
51
+ import { detectLocalMemoryStorageVersion } from "./local-memory/storage-format.js";
52
+ import { MEMORY_STATE_GUARD_CAPABILITY, MEMORY_STORAGE_V2_CAPABILITY } from "./memory-migration-protocol.js";
53
+ import { resolveLocalMemoryWriterMode } from "./local-memory/writer-mode.js";
54
+ import { logLocalMemoryCommitApplied } from "./local-memory/commit-telemetry.js";
55
+ import { shouldUseMigrationDeferredPending } from "./local-memory/migration-pending.js";
49
56
  export class RuntimeRunningCallbackTimeoutError extends Error {
50
57
  constructor(runtime, timeoutMs) {
51
58
  super(`${runtime} runtime-running callback timed out after ${timeoutMs}ms`);
@@ -59,7 +66,6 @@ const STDERR_TAIL_CAP = 2_000;
59
66
  // the old failure mode where a callback could hold execution slots forever.
60
67
  const RUNTIME_RUNNING_CALLBACK_TIMEOUT_MS = 30_000;
61
68
  const MEMORY_PRUNE_DIAGNOSTICS_TIMEOUT_MS = 2_000;
62
- const LOCAL_MEMORY_DIAGNOSTICS_TIMEOUT_MS = 250;
63
69
  const CLAUDE_INSTRUCTION_WARNING = "claude_additional_directory_instructions_unverified";
64
70
  const CODEX_COLLABORATION_EXEC_WARNING = "codex_collaboration_disabled_on_exec";
65
71
  function withoutClaudeInstructionFlag(env) {
@@ -211,37 +217,12 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
211
217
  : "high"
212
218
  : runtime.reasoning;
213
219
  const providerFp = providerFingerprint(runtime.name, providerConfig);
214
- let workspace;
215
- try {
216
- workspace = await awaitWithCancellation(prepareWorkspace({
217
- agentsRoot: input.launch.agentsRoot,
218
- handle: input.handle,
219
- cliPath: input.launch.cliPath,
220
- executionId: input.executionId,
221
- ...(input.keyMode === undefined ? {} : { keyMode: input.keyMode }),
222
- ...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
223
- ...(input.resumeKey === undefined ? {} : { resumeKey: input.resumeKey }),
224
- ...(input.sessionContextFingerprint === undefined
225
- ? {}
226
- : { sessionContextFingerprint: input.sessionContextFingerprint }),
227
- ...(input.launch.description ? { description: input.launch.description } : {}),
228
- ...(isolatedMaintenance ? { skipLocalMemory: true } : {}),
229
- }), dependencies.cancellation);
230
- }
231
- catch (error) {
232
- logLocalMemoryContextPrepareFailure({
233
- executionId: input.executionId,
234
- agentHandle: input.handle,
235
- ...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
236
- }, "workspace_prepare", error);
237
- throw error;
238
- }
239
- const memoryRollout = await resolveLocalMemoryV2Rollout({ workspace, handle: input.handle,
240
- serverUrl: input.launch.serverUrl, requested: input.localMemoryV2Enabled === true,
241
- selfTraining: isolatedMaintenance, ...(input.launch.description ? { description: input.launch.description } : {}) });
242
- workspace = { ...workspace, localMemoryV2Enabled: memoryRollout.enabled };
220
+ const preparedMemory = await prepareExecutionMemoryWorkspace(input, isolatedMaintenance, dependencies.cancellation);
221
+ let workspace = preparedMemory.workspace;
222
+ const memoryRollout = preparedMemory.mode;
243
223
  input = { ...input, localMemoryV2Enabled: memoryRollout.enabled };
244
- const localMemoryV2Initialized = await initializeLocalMemoryV2ForWorkspace(workspace, memoryRollout.enabled);
224
+ const localMemoryV2Initialized = preparedMemory.initialized;
225
+ let localMemoryWriterMode = resolveLocalMemoryWriterMode(workspace, memoryRollout);
245
226
  let materialized = null;
246
227
  let knownAttachmentDirectory = null;
247
228
  const startupReservationState = { current: null };
@@ -252,6 +233,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
252
233
  let memoryPruneBeforeSnapshot = null;
253
234
  let memoryPruneSharedWriteKey = null;
254
235
  let localMemoryResultPath = null;
236
+ let migrationDeferredPending = false;
255
237
  let environmentSnapshot;
256
238
  let executionWorkspace = workspace;
257
239
  let localMemoryTelemetry = null;
@@ -390,29 +372,41 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
390
372
  const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
391
373
  let baseWakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
392
374
  memoryPruneTraceId = isolatedMaintenance ? null : parseMemoryPruneTraceId(baseWakePrompt);
393
- if (memoryRollout.deferred)
394
- reportLocalMemoryV2Deferred(input.executionId, input.handle, memoryPruneTraceId !== null || input.maintenancePurpose === "memory_prune");
375
+ localMemoryWriterMode = resolveLocalMemoryWriterMode(executionWorkspace, memoryRollout);
376
+ migrationDeferredPending = shouldUseMigrationDeferredPending({ pruneTraceId: memoryPruneTraceId,
377
+ writerMode: localMemoryWriterMode, rolloutEnabled: memoryRollout.enabled,
378
+ legacyFallback: memoryRollout.legacyFallback,
379
+ failure: memoryRollout.failure, memory: executionWorkspace.memory });
380
+ if (memoryRollout.deferred || migrationDeferredPending)
381
+ reportLocalMemoryV2Deferred(input.executionId, input.handle, memoryPruneTraceId !== null || input.maintenancePurpose === "memory_prune", memoryRollout.legacyFallback, memoryRollout.failure, migrationDeferredPending);
382
+ if (memoryPruneTraceId !== null && localMemoryWriterMode === "blocked" && !migrationDeferredPending) {
383
+ throw new Error(memoryRollout.failure === "inspection_failed" ? "local_memory_inspection_failed" : "local_memory_v2_requires_migration");
384
+ }
395
385
  if (memoryPruneTraceId !== null) {
396
386
  executionWorkspace = await resolveMemoryPruneWorkLog(executionWorkspace, input);
397
387
  promptContext = { ...promptContext, workspace: executionWorkspace };
398
388
  baseWakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
399
389
  }
400
- if (input.localMemoryV2Enabled === true && memoryPruneTraceId !== null
390
+ if ((localMemoryWriterMode === "v2" && memoryPruneTraceId !== null
401
391
  && isLocalMemoryV2Eligible({
402
392
  enabledForHandle: true,
403
393
  agentHomePreexisted: workspace.agentHomePreexisted === true,
404
394
  memorySeedCreated: executionWorkspace.memorySeedCreated === true && (localMemoryV2Initialized || workspace.localMemoryV2Initialized !== false),
405
395
  localMemoryV2Initialized,
406
396
  memoryPrune: true,
407
- })) {
397
+ })) || migrationDeferredPending) {
408
398
  localMemoryResultPath = makeLocalMemoryResultPath(executionWorkspace.runDir, input.executionId);
409
399
  }
410
400
  const memoryPrompt = await prepareLocalMemoryPrompt({
411
401
  agentDir: executionWorkspace.dir, executionId: input.executionId, agentHandle: input.handle,
412
402
  enabled: input.localMemoryV2Enabled === true, query: baseWakePrompt,
413
403
  resultPath: localMemoryResultPath, maxBytes: input.maxSystemPromptBytes,
414
- strictRollout: !isolatedMaintenance && localMemoryV2Environment(input.launch.serverUrl) === "dev",
404
+ storageVersion: detectLocalMemoryStorageVersion(executionWorkspace.memory),
405
+ strictRollout: !isolatedMaintenance && !memoryRollout.legacyFallback && (localMemoryV2Environment(input.launch.serverUrl) === "dev"
406
+ || detectLocalMemoryStorageVersion(executionWorkspace.memory) === 2),
415
407
  deferred: memoryRollout.deferred,
408
+ ...(memoryRollout.legacyFallback ? { legacyFallback: true } : {}),
409
+ ...(migrationDeferredPending ? { migrationDeferred: true } : {}),
416
410
  renderBase: (reservedPromptBytes) => resolvePrompt(input.systemPrompt, { ...promptContext, reservedPromptBytes }),
417
411
  });
418
412
  localMemoryResultPath = memoryPrompt.resultPath;
@@ -528,6 +522,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
528
522
  localMemoryTelemetry?.contextPrepared({
529
523
  systemPrompt: effectiveSystemPrompt,
530
524
  memory: executionWorkspace.memory,
525
+ memoryQuery: baseWakePrompt,
531
526
  resumed: resuming,
532
527
  ...(executionWorkspace.memorySeedCreated === undefined
533
528
  ? {}
@@ -909,6 +904,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
909
904
  exitCode,
910
905
  workLogPath: executionWorkspace.workLogPath,
911
906
  expectedWorkLogSha256: workLogSha256(executionWorkspace.workLog),
907
+ ...(localMemoryWriterMode === "v2" ? {
908
+ writerCapabilities: [MEMORY_STORAGE_V2_CAPABILITY, MEMORY_STATE_GUARD_CAPABILITY],
909
+ requiredStorageVersion: 2,
910
+ } : {}),
911
+ ...(migrationDeferredPending ? { migrationDeferredPending: true } : {}),
912
912
  });
913
913
  const committed = exitCode !== 0 ? await commit() : await withWorkLogCommit(workLogExecutionKey(input.launch.agentsRoot, input.handle, input.taskKey), commit, dependencies.cancellation);
914
914
  if (committed.status === "failed" || committed.status === "missing") {
@@ -922,6 +922,14 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
922
922
  });
923
923
  throw new Error(`local_memory_commit_failed: ${reason}`);
924
924
  }
925
+ if (committed.status === "applied") {
926
+ if (localMemoryWriterMode === "v2" && (committed.storageVersionBefore !== 2 || committed.storageVersionAfter !== 2
927
+ || committed.committedState !== true)) {
928
+ throw new Error("local_memory_commit_failed: local_memory_v2_commit_state_invalid");
929
+ }
930
+ logLocalMemoryCommitApplied({ executionId: input.executionId, agentHandle: input.handle,
931
+ pruneTraceId: memoryPruneTraceId, committed });
932
+ }
925
933
  memoryPruneFailurePhase = "runtime_finalize";
926
934
  }
927
935
  const errorTail = [
@@ -1052,6 +1060,23 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
1052
1060
  after_incomplete_file_count: noteDiff.after_manifest.incomplete_file_count,
1053
1061
  changes: serializedChanges.json,
1054
1062
  });
1063
+ const pendingDiff = diffMemoryPruneNotes(memoryPruneBeforeSnapshot.pending_manifest
1064
+ ?? { files: [], total_count: 0, omitted_count: 0, truncated: false }, snapshot.pending_manifest
1065
+ ?? { files: [], total_count: 0, omitted_count: 0, truncated: false });
1066
+ const serializedPendingChanges = boundedDiagnosticJsonArray(pendingDiff.changes);
1067
+ const pendingChangesTruncated = pendingDiff.truncated || serializedPendingChanges.truncated;
1068
+ dslog("memory_prune.pending_changed", "长期记忆 pending 候选变化已评估", {
1069
+ execution_id: input.executionId,
1070
+ agent_handle: input.handle,
1071
+ task_key: input.taskKey,
1072
+ prune_trace_id: memoryPruneTraceId,
1073
+ change_count: pendingDiff.change_count,
1074
+ changes_truncated: pendingChangesTruncated,
1075
+ evidence_complete: pendingDiff.evidence_complete && !pendingChangesTruncated,
1076
+ before_total_count: pendingDiff.before_manifest.total_count,
1077
+ after_total_count: pendingDiff.after_manifest.total_count,
1078
+ changes: serializedPendingChanges.json,
1079
+ });
1055
1080
  const postcondition = evaluateMemoryPrunePostcondition(memoryPruneBeforeSnapshot, snapshot, {
1056
1081
  executorCompleted: memoryPruneExecutorCompleted,
1057
1082
  ...(memoryPruneRuntimeExitCode === undefined
@@ -0,0 +1,30 @@
1
+ import { dslog } from "../slog.js";
2
+ function boundedIds(ids) {
3
+ return {
4
+ value: (ids ?? []).slice(0, 100).join(","),
5
+ count: ids?.length ?? 0,
6
+ truncated: (ids?.length ?? 0) > 100,
7
+ };
8
+ }
9
+ export function logLocalMemoryCommitApplied(input) {
10
+ const { committed } = input;
11
+ const created = boundedIds(committed.createdIds);
12
+ const updated = boundedIds(committed.updatedIds);
13
+ const statusChanged = boundedIds(committed.statusChangedIds);
14
+ const pending = boundedIds(committed.pendingIds);
15
+ dslog("local_memory.commit_applied", "结构化本地记忆提交已验证", {
16
+ execution_id: input.executionId, agent_handle: input.agentHandle, prune_trace_id: input.pruneTraceId,
17
+ storage_version_before: committed.storageVersionBefore,
18
+ storage_version_after: committed.storageVersionAfter,
19
+ committed_state: committed.committedState,
20
+ writer_mode: committed.pendingReason === "legacy_requires_migration"
21
+ ? "pending_only"
22
+ : committed.storageVersionAfter === 2 && committed.committedState ? "v2" : "legacy",
23
+ created_id_count: created.count, created_ids: created.value, created_ids_truncated: created.truncated,
24
+ updated_id_count: updated.count, updated_ids: updated.value, updated_ids_truncated: updated.truncated,
25
+ status_changed_id_count: statusChanged.count, status_changed_ids: statusChanged.value,
26
+ status_changed_ids_truncated: statusChanged.truncated,
27
+ pending_id_count: pending.count, pending_ids: pending.value, pending_ids_truncated: pending.truncated,
28
+ pending_reason: committed.pendingReason,
29
+ });
30
+ }
@@ -0,0 +1,62 @@
1
+ import { prepareWorkspace } from "../workspace.js";
2
+ import { awaitWithCancellation } from "../runtime-cancellation.js";
3
+ import { logLocalMemoryContextPrepareFailure } from "../local-memory-telemetry.js";
4
+ import { resolveLocalMemoryV2Rollout } from "./rollout.js";
5
+ import { initializeLocalMemoryV2ForWorkspace } from "./runtime.js";
6
+ import { withMemoryAccess } from "./migration-access.js";
7
+ import { join } from "node:path";
8
+ import { readFile } from "node:fs/promises";
9
+ // A fresh Agent may receive the daemon's full ten-wide execution burst. The
10
+ // first turn publishes schema2 durably; the remaining turns must wait to reuse
11
+ // that committed fact instead of timing out on the generic short IO window.
12
+ const EXECUTION_MEMORY_ACCESS_WAIT_MS = 30_000;
13
+ export async function prepareExecutionMemoryWorkspace(input, isolated, cancellation) {
14
+ try {
15
+ const prepare = async (context) => {
16
+ const prepared = await prepareWorkspace({
17
+ agentsRoot: input.launch.agentsRoot, handle: input.handle, cliPath: input.launch.cliPath,
18
+ executionId: input.executionId,
19
+ ...(input.keyMode === undefined ? {} : { keyMode: input.keyMode }),
20
+ ...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
21
+ ...(input.resumeKey === undefined ? {} : { resumeKey: input.resumeKey }),
22
+ ...(input.sessionContextFingerprint === undefined ? {} : { sessionContextFingerprint: input.sessionContextFingerprint }),
23
+ ...(input.launch.description ? { description: input.launch.description } : {}),
24
+ ...(isolated ? { skipLocalMemory: true } : {}),
25
+ });
26
+ const workspace = context?.created ? { ...prepared, agentHomePreexisted: false } : prepared;
27
+ const initialMode = await resolveLocalMemoryV2Rollout({ workspace, handle: input.handle, serverUrl: input.launch.serverUrl,
28
+ agentId: input.launch.agentId, requested: input.localMemoryV2Enabled === true, selfTraining: isolated,
29
+ ...(input.launch.description ? { description: input.launch.description } : {}) });
30
+ const initializationBinding = input.launch.workspaceId !== undefined && input.launch.machineId !== undefined
31
+ ? { workspaceId: input.launch.workspaceId, machineId: input.launch.machineId }
32
+ : undefined;
33
+ const initialized = await initializeLocalMemoryV2ForWorkspace(workspace, initialMode.enabled, input.launch.agentId, initializationBinding);
34
+ const memory = initialized && !workspace.localMemoryV2Initialized
35
+ ? await readFile(join(workspace.dir, "MEMORY.md"), "utf8")
36
+ : workspace.memory;
37
+ const currentWorkspace = { ...workspace, memory };
38
+ const mode = initialized && !workspace.localMemoryV2Initialized
39
+ ? await resolveLocalMemoryV2Rollout({ workspace: currentWorkspace, handle: input.handle,
40
+ serverUrl: input.launch.serverUrl, agentId: input.launch.agentId,
41
+ requested: input.localMemoryV2Enabled === true, selfTraining: isolated,
42
+ ...(input.launch.description ? { description: input.launch.description } : {}) })
43
+ : initialMode;
44
+ return { workspace: { ...currentWorkspace, localMemoryV2Enabled: mode.enabled }, mode, initialized };
45
+ };
46
+ return await awaitWithCancellation(isolated ? prepare()
47
+ : withMemoryAccess(join(input.launch.agentsRoot, input.handle), prepare, {
48
+ create: true,
49
+ waitMs: EXECUTION_MEMORY_ACCESS_WAIT_MS,
50
+ initializedRecoveryIdentity: {
51
+ agentId: input.launch.agentId,
52
+ ...(input.launch.workspaceId === undefined ? {} : { workspaceId: input.launch.workspaceId }),
53
+ ...(input.launch.machineId === undefined ? {} : { machineId: input.launch.machineId }),
54
+ },
55
+ }), cancellation);
56
+ }
57
+ catch (error) {
58
+ logLocalMemoryContextPrepareFailure({ executionId: input.executionId, agentHandle: input.handle,
59
+ ...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }) }, "workspace_prepare", error);
60
+ throw error;
61
+ }
62
+ }
@@ -0,0 +1,47 @@
1
+ import { parseFullMemoryIndex, LOCAL_MEMORY_GENERATED_INDEX_PATTERN } from "./storage-format.js";
2
+ const excerpt = (text, size) => Array.from(text).slice(0, size).join("");
3
+ /** 查询完整元数据;只在明确的历史入口返回非有效记录。正文仍按引用和哈希分页读取。 */
4
+ export function searchFullMemoryIndex(memory, sha256, input, outputBytes) {
5
+ const records = parseFullMemoryIndex(memory);
6
+ const block = LOCAL_MEMORY_GENERATED_INDEX_PATTERN.exec(memory);
7
+ const prefix = memory.slice(0, block.index);
8
+ let line = (prefix.match(/\n/gu) ?? []).length + 1;
9
+ let offset = Array.from(prefix).length;
10
+ const positions = new Map();
11
+ for (const part of block[0].matchAll(/[^\n]*(?:\n|$)/gu)) {
12
+ const id = /^- id: ([A-Za-z0-9_-]+)\r?\n?$/u.exec(part[0])?.[1];
13
+ if (id)
14
+ positions.set(id, { line, offset });
15
+ line += 1;
16
+ offset += Array.from(part[0]).length;
17
+ }
18
+ const terms = input.query.normalize("NFKC").toLowerCase().split(/\s+/u).filter(Boolean);
19
+ const candidates = records.filter(entry => {
20
+ const active = entry.status === "active" || entry.status === "pinned";
21
+ const content = `${entry.id} ${entry.title} ${entry.summary} ${entry.triggers.join(" ")}`.normalize("NFKC").toLowerCase();
22
+ return active !== input.history && terms.every(term => content.includes(term));
23
+ });
24
+ const matches = [];
25
+ let bytes = 512, hasMore = false;
26
+ for (const entry of candidates) {
27
+ const position = positions.get(entry.id);
28
+ if (!position)
29
+ throw new Error("memory_storage_position_invalid");
30
+ if (position.line <= input.afterLine)
31
+ continue;
32
+ const item = { ...position, id: entry.id, status: entry.status, revision: entry.revision,
33
+ file: entry.file, anchor: entry.anchor, heading: excerpt(entry.title, 96),
34
+ title: excerpt(entry.title, 120), summary: excerpt(entry.summary, 240), text: excerpt(entry.summary, 240) };
35
+ const size = Buffer.byteLength(JSON.stringify(item), "utf8") + 1;
36
+ if (matches.length >= input.limit || bytes + size > outputBytes) {
37
+ hasMore = true;
38
+ break;
39
+ }
40
+ matches.push(item);
41
+ bytes += size;
42
+ }
43
+ if (hasMore && matches.length === 0)
44
+ throw new Error("memory_output_too_large");
45
+ return { file: "MEMORY.md", storageVersion: 2, sha256, matches, matchCount: candidates.length,
46
+ hasMore, complete: !hasMore, nextAfterLine: hasMore ? matches.at(-1).line : null };
47
+ }
@@ -50,14 +50,17 @@ export function selectLocalMemoryIndexEntries(entries, budget, options = {}) {
50
50
  .map((entry) => entry.id);
51
51
  const protectedIds = options.protectedIds ?? new Set();
52
52
  const protectedEntries = projectable.filter((entry) => protectedIds.has(entry.id)).sort(compareEntries);
53
- const candidates = projectable.filter((entry) => !protectedIds.has(entry.id)).sort(compareEntries);
53
+ const candidates = projectable.filter((entry) => !protectedIds.has(entry.id)).sort((left, right) => (options.relevance?.(right) ?? 0) - (options.relevance?.(left) ?? 0) || compareEntries(left, right));
54
54
  const selected = [];
55
55
  let renderedLength = 0;
56
+ let renderedBytes = 0;
56
57
  for (const entry of protectedEntries) {
57
58
  selected.push(entry);
58
59
  renderedLength += cardLength(entry, selected.length > 1);
60
+ renderedBytes += Buffer.byteLength(renderLocalMemoryIndexCard(entry), "utf8") + (selected.length > 1 ? 1 : 0);
59
61
  }
60
- if (selected.length > budget.entryCap || renderedLength > budget.indexCodePointCap) {
62
+ if (selected.length > budget.entryCap || renderedLength > budget.indexCodePointCap
63
+ || renderedBytes > (budget.indexByteCap ?? Infinity)) {
61
64
  return {
62
65
  entries: selected,
63
66
  omittedIds: [...omittedIds, ...candidates.map((entry) => entry.id)],
@@ -65,12 +68,15 @@ export function selectLocalMemoryIndexEntries(entries, budget, options = {}) {
65
68
  };
66
69
  }
67
70
  for (const entry of candidates) {
68
- if (selected.length >= budget.entryCap || renderedLength + cardLength(entry, selected.length > 0) > budget.indexCodePointCap) {
71
+ const bytes = Buffer.byteLength(renderLocalMemoryIndexCard(entry), "utf8") + (selected.length > 0 ? 1 : 0);
72
+ if (selected.length >= budget.entryCap || renderedLength + cardLength(entry, selected.length > 0) > budget.indexCodePointCap
73
+ || renderedBytes + bytes > (budget.indexByteCap ?? Infinity)) {
69
74
  omittedIds.push(entry.id);
70
75
  continue;
71
76
  }
72
77
  selected.push(entry);
73
78
  renderedLength += cardLength(entry, selected.length > 1);
79
+ renderedBytes += bytes;
74
80
  }
75
81
  return {
76
82
  entries: selected,
@@ -0,0 +1,36 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, realpath } from "node:fs/promises";
3
+ export const LEGACY_LOCAL_INITIALIZATION_IDENTITY = "local-initialized";
4
+ export async function localMemoryInitializationIdentity(agentDir, agentId, binding) {
5
+ const root = await realpath(agentDir);
6
+ const rootFingerprint = createHash("sha256").update(root).digest("hex");
7
+ const workspaceId = binding?.workspaceId ?? LEGACY_LOCAL_INITIALIZATION_IDENTITY;
8
+ const machineId = binding?.machineId ?? LEGACY_LOCAL_INITIALIZATION_IDENTITY;
9
+ // Keep the legacy digest byte-for-byte stable so an interrupted pre-upgrade
10
+ // initialization can still be inspected. Every newly authenticated daemon
11
+ // initialization includes both control-plane identities in its operation id.
12
+ const operationSeed = workspaceId === LEGACY_LOCAL_INITIALIZATION_IDENTITY
13
+ && machineId === LEGACY_LOCAL_INITIALIZATION_IDENTITY
14
+ ? `${agentId}\0${root}`
15
+ : `${workspaceId}\0${machineId}\0${agentId}\0${root}`;
16
+ const operation = createHash("sha256").update(operationSeed, "utf8").digest("hex").slice(0, 24);
17
+ return {
18
+ workspaceId,
19
+ agentId,
20
+ migrationId: `initialize-${operation}`,
21
+ sourceStorageVersion: 0,
22
+ targetStorageVersion: 2,
23
+ rootFingerprint,
24
+ ownerIdentity: String((await lstat(root)).uid),
25
+ machineId,
26
+ executionId: `initialize-${operation}`,
27
+ };
28
+ }
29
+ export function isLegacyLocalMemoryInitializationIdentity(identity) {
30
+ return identity.workspaceId === LEGACY_LOCAL_INITIALIZATION_IDENTITY
31
+ && identity.machineId === LEGACY_LOCAL_INITIALIZATION_IDENTITY;
32
+ }
33
+ export function isLocalMemoryInitializationIdentity(actual, expected) {
34
+ return Object.keys(expected)
35
+ .every(key => actual[key] === expected[key]);
36
+ }