@nowcrew/daemon 0.6.91-beta.3 → 0.6.91-beta.4

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 (39) hide show
  1. package/dist/daemon-update-controller.js +16 -8
  2. package/dist/daemon-updater.js +8 -10
  3. package/dist/execution-journal.js +3 -1
  4. package/dist/execution-protocol.js +1 -1
  5. package/dist/execution-runner.js +3 -1
  6. package/dist/host-execution-coordinator.js +12 -6
  7. package/dist/local-execution-prompt.js +8 -3
  8. package/dist/local-executor.js +14 -35
  9. package/dist/local-memory/execution-workspace.js +36 -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/migration-access.js +118 -0
  13. package/dist/local-memory/migration-cli.js +54 -0
  14. package/dist/local-memory/migration-io.js +199 -0
  15. package/dist/local-memory/migration-plan.js +278 -0
  16. package/dist/local-memory/migration-schema.js +55 -0
  17. package/dist/local-memory/migration-store.js +424 -0
  18. package/dist/local-memory/migration-tool-entry.js +21 -0
  19. package/dist/local-memory/prompt.js +20 -4
  20. package/dist/local-memory/read-cli.js +36 -1
  21. package/dist/local-memory/rollout.js +53 -8
  22. package/dist/local-memory/runtime.js +5 -2
  23. package/dist/local-memory/storage-format.js +152 -0
  24. package/dist/local-memory/write.js +108 -17
  25. package/dist/machine-info.js +3 -0
  26. package/dist/memory-migration-controller.js +84 -0
  27. package/dist/memory-migration-inspect.js +74 -0
  28. package/dist/memory-migration-journal.js +14 -0
  29. package/dist/memory-migration-protocol.js +14 -0
  30. package/dist/memory-migration-runtime.js +305 -0
  31. package/dist/message-file-preview.js +11 -1
  32. package/dist/pre-admission-rejection.js +5 -4
  33. package/dist/prompt.js +5 -1
  34. package/dist/runner.js +1 -1
  35. package/dist/serve.js +37 -4
  36. package/dist/shared-execution-slots.js +2 -0
  37. package/dist/token.js +1 -0
  38. package/dist/workspace-control-handler.js +16 -3
  39. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { dslog } from "./slog.js";
1
2
  import { z } from "zod";
2
3
  import { isDaemonReleaseVersion } from "./daemon-release-version.js";
3
4
  const DaemonUpdateMessageSchema = z.object({
@@ -42,15 +43,16 @@ export function createDaemonUpdateController(deps) {
42
43
  failed(message.updateId, preparation.errorCode);
43
44
  return;
44
45
  }
45
- let released = false;
46
- const releaseOnce = async () => {
47
- if (released)
48
- return;
49
- released = true;
50
- await preparation.release();
46
+ let releaseTask;
47
+ const releaseOnce = () => {
48
+ releaseTask ??= preparation.release().catch(error => {
49
+ releaseTask = undefined;
50
+ throw error;
51
+ });
52
+ return releaseTask;
51
53
  };
52
- deps.sendStatus({ type: "daemon:update-status", updateId: message.updateId, status: "restarting" });
53
54
  try {
55
+ deps.sendStatus({ type: "daemon:update-status", updateId: message.updateId, status: "restarting" });
54
56
  const handoff = deps.scheduleRestart(eligibility.serviceSpec);
55
57
  const state = {
56
58
  release: releaseOnce,
@@ -92,7 +94,13 @@ export function createDaemonUpdateController(deps) {
92
94
  failed(message.updateId, "update_in_progress");
93
95
  return true;
94
96
  }
95
- const done = execute(message).finally(() => {
97
+ const done = execute(message).catch(error => {
98
+ dslog("daemon.update_failed", "Daemon 更新或重启准备失败", {
99
+ level: "ERROR", update_id: message.updateId, operation: message.type,
100
+ error_message: error instanceof Error ? error.message : String(error),
101
+ });
102
+ failed(message.updateId, message.type === "daemon:restart" ? "restart_failed" : "install_failed");
103
+ }).finally(() => {
96
104
  handled.add(message.updateId);
97
105
  if (running?.id === message.updateId)
98
106
  running = null;
@@ -1,3 +1,4 @@
1
+ import { dslog } from "./slog.js";
1
2
  import { readFile } from "node:fs/promises";
2
3
  import { resolve } from "node:path";
3
4
  import { systemCommandRunner } from "./computer-service.js";
@@ -17,6 +18,8 @@ export async function prepareDaemonRestart(input) {
17
18
  const local = input.localSlots.tryAcquireExclusive();
18
19
  if (local === null)
19
20
  return { ok: false, errorCode: "runtime_busy" };
21
+ const acquiredAt = Date.now();
22
+ dslog("daemon.maintenance_barrier_acquired", "Daemon 已暂停接收执行,准备维护");
20
23
  let released = false;
21
24
  return {
22
25
  ok: true,
@@ -25,6 +28,7 @@ export async function prepareDaemonRestart(input) {
25
28
  return;
26
29
  released = true;
27
30
  local.release();
31
+ dslog("daemon.maintenance_barrier_released", "维护屏障已释放", { held_ms: Date.now() - acquiredAt });
28
32
  },
29
33
  };
30
34
  }
@@ -67,16 +71,10 @@ export async function installExactDaemonUpdate(input) {
67
71
  || installation.npmPrefix !== input.npmPrefix) {
68
72
  return { ok: false, errorCode: "ineligible" };
69
73
  }
70
- const local = input.localSlots.tryAcquireExclusive();
71
- if (local === null)
72
- return { ok: false, errorCode: "runtime_busy" };
73
- let released = false;
74
- const release = async () => {
75
- if (released)
76
- return;
77
- released = true;
78
- local.release();
79
- };
74
+ const barrier = await prepareDaemonRestart(input);
75
+ if (!barrier.ok)
76
+ return barrier;
77
+ const release = barrier.release;
80
78
  const lockedInstallation = daemonGlobalInstallation(input.currentEntryPath ?? process.argv[1] ?? "", input.platform ?? process.platform);
81
79
  if (lockedInstallation === null
82
80
  || lockedInstallation.packageRoot !== input.packageRoot
@@ -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"]);
11
+ const RuntimeSchema = z.enum(["claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness", "memory_migration"]);
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";
@@ -837,8 +838,9 @@ export async function runExecution(config, input, dependencies) {
837
838
  const localMemoryGuardInBase = localMemoryV2Environment(config.serverUrl) === "custom";
838
839
  const boundedSystemPrompt = (context) => {
839
840
  const enabled = context.workspace.localMemoryV2Enabled ?? localMemoryV2Enabled;
840
- const systemPromptWithLocalFacts = withLocalExecutionFacts(`${spec.instructions.systemPrompt}${enabled && localMemoryGuardInBase ? renderLocalMemoryV2WriteGuard() : ""}`, baseSystemPromptBudget, {
841
+ const systemPromptWithLocalFacts = withLocalExecutionFacts(`${spec.instructions.systemPrompt}${enabled && localMemoryGuardInBase && detectLocalMemoryStorageVersion(context.workspace.memory) !== 2 ? renderLocalMemoryV2WriteGuard() : ""}`, baseSystemPromptBudget, {
841
842
  ...(selfTraining ? { isolateLocalMemory: true } : {}),
843
+ memoryQuery: spec.instructions.wakePrompt,
842
844
  collaborationTools: config.codexCollaborationTools && spec.runtime.name === "codex",
843
845
  });
844
846
  const base = typeof systemPromptWithLocalFacts === "string"
@@ -286,13 +286,19 @@ export function createHostExecutionCoordinator(options = {}) {
286
286
  ...Array.from({ length: executionSlots }, (_, slot) => join(root, "slots", String(slot))),
287
287
  join(root, "memory-prune-slots", "0"),
288
288
  ];
289
- for (const target of targets) {
290
- const lease = await acquireLease(target);
291
- if (lease === null) {
292
- await retryRelease("exclusive_execution_partial", () => Promise.all(leases.map((owned) => owned.close())).then(() => undefined));
293
- return null;
289
+ try {
290
+ for (const target of targets) {
291
+ const lease = await acquireLease(target);
292
+ if (lease === null) {
293
+ await retryRelease("exclusive_execution_partial", () => Promise.all(leases.map((owned) => owned.close())).then(() => undefined));
294
+ return null;
295
+ }
296
+ leases.push(lease);
294
297
  }
295
- leases.push(lease);
298
+ }
299
+ catch (error) {
300
+ await retryRelease("exclusive_execution_partial", () => Promise.all(leases.map(owned => owned.close())).then(() => undefined));
301
+ throw error;
296
302
  }
297
303
  let releasePromise = null;
298
304
  return {
@@ -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";
@@ -42,10 +42,12 @@ import { createLocalMemoryTelemetry, logLocalMemoryContextPrepareFailure, logLoc
42
42
  import { CodexStartupStageParser } from "./codex-startup-stage.js";
43
43
  import { formatRuntimeStartupError } from "./runtime-startup-error.js";
44
44
  import { logRuntimeEnvironmentContract } from "./runtime-environment-telemetry.js";
45
- import { commitLocalMemoryResultAfterExit, initializeLocalMemoryV2ForWorkspace, isLocalMemoryV2Eligible, localMemoryResultPath as makeLocalMemoryResultPath, } from "./local-memory/runtime.js";
45
+ import { commitLocalMemoryResultAfterExit, isLocalMemoryV2Eligible, localMemoryResultPath as makeLocalMemoryResultPath, } from "./local-memory/runtime.js";
46
46
  import { prepareLocalMemoryPrompt } from "./local-memory/prompt.js";
47
- import { reportLocalMemoryV2Deferred, resolveLocalMemoryV2Rollout } from "./local-memory/rollout.js";
47
+ import { reportLocalMemoryV2Deferred } from "./local-memory/rollout.js";
48
+ import { prepareExecutionMemoryWorkspace } from "./local-memory/execution-workspace.js";
48
49
  import { localMemoryV2Environment } from "./config.js";
50
+ import { detectLocalMemoryStorageVersion } from "./local-memory/storage-format.js";
49
51
  export class RuntimeRunningCallbackTimeoutError extends Error {
50
52
  constructor(runtime, timeoutMs) {
51
53
  super(`${runtime} runtime-running callback timed out after ${timeoutMs}ms`);
@@ -211,37 +213,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
211
213
  : "high"
212
214
  : runtime.reasoning;
213
215
  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 };
216
+ const preparedMemory = await prepareExecutionMemoryWorkspace(input, isolatedMaintenance, dependencies.cancellation);
217
+ let workspace = preparedMemory.workspace;
218
+ const memoryRollout = preparedMemory.mode;
243
219
  input = { ...input, localMemoryV2Enabled: memoryRollout.enabled };
244
- const localMemoryV2Initialized = await initializeLocalMemoryV2ForWorkspace(workspace, memoryRollout.enabled);
220
+ const localMemoryV2Initialized = preparedMemory.initialized;
245
221
  let materialized = null;
246
222
  let knownAttachmentDirectory = null;
247
223
  const startupReservationState = { current: null };
@@ -391,7 +367,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
391
367
  let baseWakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
392
368
  memoryPruneTraceId = isolatedMaintenance ? null : parseMemoryPruneTraceId(baseWakePrompt);
393
369
  if (memoryRollout.deferred)
394
- reportLocalMemoryV2Deferred(input.executionId, input.handle, memoryPruneTraceId !== null || input.maintenancePurpose === "memory_prune");
370
+ reportLocalMemoryV2Deferred(input.executionId, input.handle, memoryPruneTraceId !== null || input.maintenancePurpose === "memory_prune", memoryRollout.legacyFallback);
395
371
  if (memoryPruneTraceId !== null) {
396
372
  executionWorkspace = await resolveMemoryPruneWorkLog(executionWorkspace, input);
397
373
  promptContext = { ...promptContext, workspace: executionWorkspace };
@@ -411,8 +387,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
411
387
  agentDir: executionWorkspace.dir, executionId: input.executionId, agentHandle: input.handle,
412
388
  enabled: input.localMemoryV2Enabled === true, query: baseWakePrompt,
413
389
  resultPath: localMemoryResultPath, maxBytes: input.maxSystemPromptBytes,
414
- strictRollout: !isolatedMaintenance && localMemoryV2Environment(input.launch.serverUrl) === "dev",
390
+ storageVersion: detectLocalMemoryStorageVersion(executionWorkspace.memory),
391
+ strictRollout: !isolatedMaintenance && !memoryRollout.legacyFallback && (localMemoryV2Environment(input.launch.serverUrl) === "dev"
392
+ || detectLocalMemoryStorageVersion(executionWorkspace.memory) === 2),
415
393
  deferred: memoryRollout.deferred,
394
+ ...(memoryRollout.legacyFallback ? { legacyFallback: true } : {}),
416
395
  renderBase: (reservedPromptBytes) => resolvePrompt(input.systemPrompt, { ...promptContext, reservedPromptBytes }),
417
396
  });
418
397
  localMemoryResultPath = memoryPrompt.resultPath;
@@ -0,0 +1,36 @@
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
+ export async function prepareExecutionMemoryWorkspace(input, isolated, cancellation) {
9
+ try {
10
+ const prepare = async (context) => {
11
+ const prepared = await prepareWorkspace({
12
+ agentsRoot: input.launch.agentsRoot, handle: input.handle, cliPath: input.launch.cliPath,
13
+ executionId: input.executionId,
14
+ ...(input.keyMode === undefined ? {} : { keyMode: input.keyMode }),
15
+ ...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
16
+ ...(input.resumeKey === undefined ? {} : { resumeKey: input.resumeKey }),
17
+ ...(input.sessionContextFingerprint === undefined ? {} : { sessionContextFingerprint: input.sessionContextFingerprint }),
18
+ ...(input.launch.description ? { description: input.launch.description } : {}),
19
+ ...(isolated ? { skipLocalMemory: true } : {}),
20
+ });
21
+ const workspace = context?.created ? { ...prepared, agentHomePreexisted: false } : prepared;
22
+ const mode = await resolveLocalMemoryV2Rollout({ workspace, handle: input.handle, serverUrl: input.launch.serverUrl,
23
+ requested: input.localMemoryV2Enabled === true, selfTraining: isolated,
24
+ ...(input.launch.description ? { description: input.launch.description } : {}) });
25
+ const initialized = await initializeLocalMemoryV2ForWorkspace(workspace, mode.enabled);
26
+ return { workspace: { ...workspace, localMemoryV2Enabled: mode.enabled }, mode, initialized };
27
+ };
28
+ return await awaitWithCancellation(isolated ? prepare()
29
+ : withMemoryAccess(join(input.launch.agentsRoot, input.handle), prepare, { create: true }), cancellation);
30
+ }
31
+ catch (error) {
32
+ logLocalMemoryContextPrepareFailure({ executionId: input.executionId, agentHandle: input.handle,
33
+ ...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }) }, "workspace_prepare", error);
34
+ throw error;
35
+ }
36
+ }
@@ -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,118 @@
1
+ import { lstat, mkdir, open, readFile, realpath } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { z } from "zod";
4
+ import { createJournalLease, createJournalLeaseRegistry, JournalLockedError } from "../execution-journal-lock.js";
5
+ import { defaultProcessController } from "../execution-journal.js";
6
+ import { createHash } from "node:crypto";
7
+ import { MigrationStateSchema } from "./migration-schema.js";
8
+ import { detectLocalMemoryStorageVersion, parseFullMemoryIndex } from "./storage-format.js";
9
+ import { readMemoryInventorySource, inventoryLimits } from "./inventory-source.js";
10
+ const StateGate = z.object({
11
+ schemaVersion: z.literal(1), migrationId: z.string().min(1),
12
+ phase: z.enum(["prepared", "applying", "committed", "rolled_back", "repair_required"]),
13
+ }).passthrough();
14
+ async function checkedDirectory(parent, name) {
15
+ const directory = join(parent, name);
16
+ await mkdir(directory, { recursive: true, mode: 0o700 });
17
+ const stat = await lstat(directory);
18
+ if (!stat.isDirectory() || stat.isSymbolicLink() || await realpath(directory) !== directory) {
19
+ throw new Error("memory_migration_path_invalid");
20
+ }
21
+ return directory;
22
+ }
23
+ export async function assertMemoryAccessAllowed(agentDir) {
24
+ let state = null;
25
+ try {
26
+ const source = await readMemoryInventorySource(agentDir, ".crew/memory/migration.v1.json", inventoryLimits.fileBytes);
27
+ state = StateGate.parse(JSON.parse(source.content));
28
+ if (state.phase === "applying" || state.phase === "repair_required")
29
+ throw new Error("memory_migration_in_progress");
30
+ }
31
+ catch (error) {
32
+ if (error instanceof Error && error.message === "memory_migration_in_progress")
33
+ throw error;
34
+ if (error.code !== "ENOENT")
35
+ throw new Error("memory_migration_state_invalid");
36
+ }
37
+ let source;
38
+ try {
39
+ source = await readMemoryInventorySource(agentDir, "MEMORY.md", inventoryLimits.fileBytes);
40
+ }
41
+ catch (error) {
42
+ if (error.code === "ENOENT" && state === null)
43
+ return;
44
+ if (error instanceof Error && error.message === "read_limit_exceeded")
45
+ throw new Error("memory_file_too_large");
46
+ throw error;
47
+ }
48
+ const version = detectLocalMemoryStorageVersion(source.content);
49
+ if (version === 2 || state?.phase === "committed") {
50
+ if (!state)
51
+ throw new Error("memory_migration_state_missing");
52
+ const verified = MigrationStateSchema.parse(state);
53
+ if (verified.phase !== "committed" || verified.storageVersion !== 2 || version !== 2
54
+ || verified.identity.rootFingerprint !== createHash("sha256").update(await realpath(agentDir)).digest("hex")) {
55
+ throw new Error("memory_migration_state_invalid");
56
+ }
57
+ parseFullMemoryIndex(source.content);
58
+ }
59
+ }
60
+ /** 复用现有进程身份锁;仅保护一次本地IO,不持锁等待模型,避免子CLI自锁。 */
61
+ export async function withMemoryAccess(agentDir, operation, options = {}) {
62
+ let created = false;
63
+ if (options.create) {
64
+ created = await lstat(agentDir).then(() => false, (e) => { if (e.code === "ENOENT")
65
+ return true; throw e; });
66
+ await mkdir(agentDir, { recursive: true, mode: 0o700 });
67
+ }
68
+ const root = await realpath(agentDir);
69
+ if ((await lstat(agentDir)).isSymbolicLink())
70
+ throw new Error("memory_migration_path_invalid");
71
+ const crew = await checkedDirectory(root, ".crew");
72
+ const memory = await checkedDirectory(crew, "memory");
73
+ const directory = await checkedDirectory(memory, "access.lock");
74
+ const lease = createJournalLease({ directory, currentPid: process.pid,
75
+ captureCurrentIdentity: async () => {
76
+ const identity = await defaultProcessController.inspectIdentity(process.pid);
77
+ if (!identity)
78
+ throw new Error("memory_migration_process_unknown");
79
+ return identity;
80
+ },
81
+ inspectIdentity: pid => defaultProcessController.inspectIdentity(pid),
82
+ registry: createJournalLeaseRegistry(),
83
+ syncDirectory: async (path) => {
84
+ if (process.platform === "win32")
85
+ return;
86
+ const handle = await open(path, "r");
87
+ try {
88
+ await handle.sync();
89
+ }
90
+ finally {
91
+ await handle.close();
92
+ }
93
+ },
94
+ });
95
+ const deadline = Date.now() + (options.waitMs ?? 2000);
96
+ for (;;) {
97
+ try {
98
+ await lease.acquire();
99
+ break;
100
+ }
101
+ catch (error) {
102
+ if (!(error instanceof JournalLockedError))
103
+ throw error;
104
+ if (Date.now() >= deadline)
105
+ throw new Error("memory_migration_access_busy");
106
+ await new Promise(resolve => setTimeout(resolve, 25));
107
+ }
108
+ }
109
+ try {
110
+ if (!options.maintenance)
111
+ await assertMemoryAccessAllowed(root);
112
+ lease.assertUsable();
113
+ return await operation({ created });
114
+ }
115
+ finally {
116
+ await lease.close();
117
+ }
118
+ }
@@ -0,0 +1,54 @@
1
+ import { isAbsolute, resolve } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { z } from 'zod';
4
+ import { applyMemoryMigration, prepareMemoryMigration, recoverMemoryMigration, statusMemoryMigration } from './migration-store.js';
5
+ import { MigrationError, MigrationHistorySchema, MigrationIdentitySchema, MigrationIntentReceiptSchema, MigrationProcessStoppedReceiptSchema, MigrationRecordMappingSchema } from './migration-schema.js';
6
+ const base = z.object({ agentDir: z.string().refine(isAbsolute), identity: MigrationIdentitySchema });
7
+ export const MigrationCommandSchema = z.discriminatedUnion('command', [
8
+ base.extend({ command: z.literal('prepare'), serverHistory: MigrationHistorySchema, recordMappings: z.array(MigrationRecordMappingSchema).max(10000).optional() }).strict(),
9
+ base.extend({ command: z.literal('status') }).strict(),
10
+ base.extend({ command: z.literal('apply'), intentReceipt: MigrationIntentReceiptSchema }).strict(),
11
+ base.extend({ command: z.literal('recover'), serverHistory: MigrationHistorySchema.optional(), intentReceipt: MigrationIntentReceiptSchema.optional(), stoppedReceipt: MigrationProcessStoppedReceiptSchema.optional(), cancelled: z.boolean() }).strict(),
12
+ ]);
13
+ export function summarizeMemoryMigration(state) {
14
+ return { migrationId: state?.migrationId ?? null, phase: state?.phase ?? null, storageVersion: state?.storageVersion ?? null, sourceDigest: state?.sourceDigest ?? null, planDigest: state?.planDigest ?? null,
15
+ issues: state?.issues ?? [], result: state === null ? 'missing' : state.phase === 'repair_required' ? 'repair_required' : 'success' };
16
+ }
17
+ /** The caller owns the execution/maintenance lease. This boundary never accepts or reads credentials. */
18
+ export async function runMemoryMigrationCommand(raw) {
19
+ let migrationId = null;
20
+ try {
21
+ const input = MigrationCommandSchema.parse(raw);
22
+ migrationId = input.identity.migrationId;
23
+ const state = input.command === 'prepare' ? await prepareMemoryMigration(input) : input.command === 'apply' ? await applyMemoryMigration(input)
24
+ : input.command === 'recover' ? await recoverMemoryMigration(input) : await statusMemoryMigration(input);
25
+ return summarizeMemoryMigration(state);
26
+ }
27
+ catch (error) {
28
+ const reason = error instanceof MigrationError ? error.code : error instanceof z.ZodError ? 'migration_input_invalid' : 'migration_operation_failed';
29
+ return { ...summarizeMemoryMigration(null), migrationId, result: reason === 'migration_needs_review' ? 'needs_review' : reason.includes('repair_required') ? 'repair_required' : 'failed',
30
+ issues: error instanceof MigrationError && error.issues.length ? error.issues : [{ path: '', reason }] };
31
+ }
32
+ }
33
+ async function main() {
34
+ try {
35
+ const chunks = [];
36
+ let bytes = 0;
37
+ for await (const chunk of process.stdin) {
38
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
39
+ bytes += buffer.length;
40
+ if (bytes > 8 * 1024 * 1024)
41
+ throw new Error('input_limit');
42
+ chunks.push(buffer);
43
+ }
44
+ const result = await runMemoryMigrationCommand(JSON.parse(Buffer.concat(chunks).toString('utf8')));
45
+ process.stdout.write(`${JSON.stringify(result)}\n`);
46
+ process.exitCode = result.result === 'success' || result.result === 'missing' ? 0 : 1;
47
+ }
48
+ catch {
49
+ process.stdout.write(`${JSON.stringify({ ...summarizeMemoryMigration(null), result: 'failed', issues: [{ path: '', reason: 'migration_input_invalid' }] })}\n`);
50
+ process.exitCode = 1;
51
+ }
52
+ }
53
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url))
54
+ void main();