@nowcrew/daemon 0.6.91 → 0.6.93-beta.1

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 (46) hide show
  1. package/dist/execution-journal.js +3 -1
  2. package/dist/execution-protocol.js +1 -1
  3. package/dist/execution-runner.js +3 -1
  4. package/dist/local-execution-prompt.js +8 -3
  5. package/dist/local-executor.js +64 -35
  6. package/dist/local-memory/execution-workspace.js +55 -0
  7. package/dist/local-memory/full-index-search.js +47 -0
  8. package/dist/local-memory/index-budget.js +9 -3
  9. package/dist/local-memory/initialization-identity.js +23 -0
  10. package/dist/local-memory/inventory-cli.js +23 -1
  11. package/dist/local-memory/inventory-source.js +15 -8
  12. package/dist/local-memory/legacy-adapter.js +233 -0
  13. package/dist/local-memory/migration-access.js +184 -0
  14. package/dist/local-memory/migration-cli.js +54 -0
  15. package/dist/local-memory/migration-io.js +199 -0
  16. package/dist/local-memory/migration-plan.js +283 -0
  17. package/dist/local-memory/migration-schema.js +55 -0
  18. package/dist/local-memory/migration-store.js +429 -0
  19. package/dist/local-memory/migration-tool-entry.js +21 -0
  20. package/dist/local-memory/prompt.js +20 -4
  21. package/dist/local-memory/read-cli.js +75 -77
  22. package/dist/local-memory/rollout.js +54 -8
  23. package/dist/local-memory/runtime.js +148 -13
  24. package/dist/local-memory/storage-format.js +152 -0
  25. package/dist/local-memory/write.js +125 -19
  26. package/dist/local-memory-diagnostics.js +41 -2
  27. package/dist/local-memory-telemetry.js +19 -2
  28. package/dist/local-runtime-environment.js +6 -1
  29. package/dist/machine-environment-cache.js +18 -4
  30. package/dist/machine-environment-contract.js +3 -1
  31. package/dist/machine-environment-process.js +21 -1
  32. package/dist/machine-info.js +3 -0
  33. package/dist/memory-migration-controller.js +84 -0
  34. package/dist/memory-migration-inspect.js +75 -0
  35. package/dist/memory-migration-journal.js +14 -0
  36. package/dist/memory-migration-protocol.js +14 -0
  37. package/dist/memory-migration-runtime.js +305 -0
  38. package/dist/memory-prune-diagnostics.js +60 -4
  39. package/dist/message-file-preview.js +11 -1
  40. package/dist/prompt.js +5 -1
  41. package/dist/runner.js +1 -1
  42. package/dist/serve.js +36 -3
  43. package/dist/token.js +1 -0
  44. package/dist/workspace-control-handler.js +16 -3
  45. package/dist/workspace.js +10 -2
  46. package/package.json +1 -1
@@ -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";
@@ -838,8 +839,9 @@ export async function runExecution(config, input, dependencies) {
838
839
  const localMemoryGuardInBase = localMemoryV2Environment(config.serverUrl) === "custom";
839
840
  const boundedSystemPrompt = (context) => {
840
841
  const enabled = context.workspace.localMemoryV2Enabled ?? localMemoryV2Enabled;
841
- const systemPromptWithLocalFacts = withLocalExecutionFacts(`${spec.instructions.systemPrompt}${enabled && localMemoryGuardInBase ? renderLocalMemoryV2WriteGuard() : ""}`, baseSystemPromptBudget, {
842
+ const systemPromptWithLocalFacts = withLocalExecutionFacts(`${spec.instructions.systemPrompt}${enabled && localMemoryGuardInBase && detectLocalMemoryStorageVersion(context.workspace.memory) !== 2 ? renderLocalMemoryV2WriteGuard() : ""}`, baseSystemPromptBudget, {
842
843
  ...(selfTraining ? { isolateLocalMemory: true } : {}),
844
+ memoryQuery: spec.instructions.wakePrompt,
843
845
  collaborationTools: config.codexCollaborationTools && spec.runtime.name === "codex",
844
846
  });
845
847
  const base = typeof systemPromptWithLocalFacts === "string"
@@ -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,13 @@ 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";
51
+ import { MEMORY_STATE_GUARD_CAPABILITY, MEMORY_STORAGE_V2_CAPABILITY } from "./memory-migration-protocol.js";
49
52
  export class RuntimeRunningCallbackTimeoutError extends Error {
50
53
  constructor(runtime, timeoutMs) {
51
54
  super(`${runtime} runtime-running callback timed out after ${timeoutMs}ms`);
@@ -211,37 +214,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
211
214
  : "high"
212
215
  : runtime.reasoning;
213
216
  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 };
217
+ const preparedMemory = await prepareExecutionMemoryWorkspace(input, isolatedMaintenance, dependencies.cancellation);
218
+ let workspace = preparedMemory.workspace;
219
+ const memoryRollout = preparedMemory.mode;
243
220
  input = { ...input, localMemoryV2Enabled: memoryRollout.enabled };
244
- const localMemoryV2Initialized = await initializeLocalMemoryV2ForWorkspace(workspace, memoryRollout.enabled);
221
+ const localMemoryV2Initialized = preparedMemory.initialized;
245
222
  let materialized = null;
246
223
  let knownAttachmentDirectory = null;
247
224
  const startupReservationState = { current: null };
@@ -391,7 +368,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
391
368
  let baseWakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
392
369
  memoryPruneTraceId = isolatedMaintenance ? null : parseMemoryPruneTraceId(baseWakePrompt);
393
370
  if (memoryRollout.deferred)
394
- reportLocalMemoryV2Deferred(input.executionId, input.handle, memoryPruneTraceId !== null || input.maintenancePurpose === "memory_prune");
371
+ reportLocalMemoryV2Deferred(input.executionId, input.handle, memoryPruneTraceId !== null || input.maintenancePurpose === "memory_prune", memoryRollout.legacyFallback);
395
372
  if (memoryPruneTraceId !== null) {
396
373
  executionWorkspace = await resolveMemoryPruneWorkLog(executionWorkspace, input);
397
374
  promptContext = { ...promptContext, workspace: executionWorkspace };
@@ -411,8 +388,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
411
388
  agentDir: executionWorkspace.dir, executionId: input.executionId, agentHandle: input.handle,
412
389
  enabled: input.localMemoryV2Enabled === true, query: baseWakePrompt,
413
390
  resultPath: localMemoryResultPath, maxBytes: input.maxSystemPromptBytes,
414
- strictRollout: !isolatedMaintenance && localMemoryV2Environment(input.launch.serverUrl) === "dev",
391
+ storageVersion: detectLocalMemoryStorageVersion(executionWorkspace.memory),
392
+ strictRollout: !isolatedMaintenance && !memoryRollout.legacyFallback && (localMemoryV2Environment(input.launch.serverUrl) === "dev"
393
+ || detectLocalMemoryStorageVersion(executionWorkspace.memory) === 2),
415
394
  deferred: memoryRollout.deferred,
395
+ ...(memoryRollout.legacyFallback ? { legacyFallback: true } : {}),
416
396
  renderBase: (reservedPromptBytes) => resolvePrompt(input.systemPrompt, { ...promptContext, reservedPromptBytes }),
417
397
  });
418
398
  localMemoryResultPath = memoryPrompt.resultPath;
@@ -914,6 +894,10 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
914
894
  exitCode,
915
895
  workLogPath: executionWorkspace.workLogPath,
916
896
  expectedWorkLogSha256: workLogSha256(executionWorkspace.workLog),
897
+ ...(input.localMemoryV2Enabled === true ? {
898
+ writerCapabilities: [MEMORY_STORAGE_V2_CAPABILITY, MEMORY_STATE_GUARD_CAPABILITY],
899
+ requiredStorageVersion: 2,
900
+ } : {}),
917
901
  });
918
902
  const committed = exitCode !== 0 ? await commit() : await withWorkLogCommit(workLogExecutionKey(input.launch.agentsRoot, input.handle, input.taskKey), commit, dependencies.cancellation);
919
903
  if (committed.status === "failed" || committed.status === "missing") {
@@ -927,6 +911,34 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
927
911
  });
928
912
  throw new Error(`local_memory_commit_failed: ${reason}`);
929
913
  }
914
+ if (committed.status === "applied") {
915
+ if (input.localMemoryV2Enabled === true
916
+ && (committed.storageVersionBefore !== 2 || committed.storageVersionAfter !== 2
917
+ || committed.committedState !== true)) {
918
+ throw new Error("local_memory_commit_failed: local_memory_v2_commit_state_invalid");
919
+ }
920
+ const boundedIds = (ids) => ({
921
+ value: (ids ?? []).slice(0, 100).join(","),
922
+ count: ids?.length ?? 0,
923
+ truncated: (ids?.length ?? 0) > 100,
924
+ });
925
+ const created = boundedIds(committed.createdIds);
926
+ const updated = boundedIds(committed.updatedIds);
927
+ const statusChanged = boundedIds(committed.statusChangedIds);
928
+ const pending = boundedIds(committed.pendingIds);
929
+ dslog("local_memory.commit_applied", "结构化本地记忆提交已验证", {
930
+ execution_id: input.executionId, agent_handle: input.handle, prune_trace_id: memoryPruneTraceId,
931
+ storage_version_before: committed.storageVersionBefore,
932
+ storage_version_after: committed.storageVersionAfter,
933
+ committed_state: committed.committedState,
934
+ writer_mode: committed.storageVersionAfter === 2 && committed.committedState ? "v2" : "legacy",
935
+ created_id_count: created.count, created_ids: created.value, created_ids_truncated: created.truncated,
936
+ updated_id_count: updated.count, updated_ids: updated.value, updated_ids_truncated: updated.truncated,
937
+ status_changed_id_count: statusChanged.count, status_changed_ids: statusChanged.value,
938
+ status_changed_ids_truncated: statusChanged.truncated,
939
+ pending_id_count: pending.count, pending_ids: pending.value, pending_ids_truncated: pending.truncated,
940
+ });
941
+ }
930
942
  memoryPruneFailurePhase = "runtime_finalize";
931
943
  }
932
944
  const errorTail = [
@@ -1050,6 +1062,23 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
1050
1062
  after_incomplete_file_count: noteDiff.after_manifest.incomplete_file_count,
1051
1063
  changes: serializedChanges.json,
1052
1064
  });
1065
+ const pendingDiff = diffMemoryPruneNotes(memoryPruneBeforeSnapshot.pending_manifest
1066
+ ?? { files: [], total_count: 0, omitted_count: 0, truncated: false }, snapshot.pending_manifest
1067
+ ?? { files: [], total_count: 0, omitted_count: 0, truncated: false });
1068
+ const serializedPendingChanges = boundedDiagnosticJsonArray(pendingDiff.changes);
1069
+ const pendingChangesTruncated = pendingDiff.truncated || serializedPendingChanges.truncated;
1070
+ dslog("memory_prune.pending_changed", "长期记忆 pending 候选变化已评估", {
1071
+ execution_id: input.executionId,
1072
+ agent_handle: input.handle,
1073
+ task_key: input.taskKey,
1074
+ prune_trace_id: memoryPruneTraceId,
1075
+ change_count: pendingDiff.change_count,
1076
+ changes_truncated: pendingChangesTruncated,
1077
+ evidence_complete: pendingDiff.evidence_complete && !pendingChangesTruncated,
1078
+ before_total_count: pendingDiff.before_manifest.total_count,
1079
+ after_total_count: pendingDiff.after_manifest.total_count,
1080
+ changes: serializedPendingChanges.json,
1081
+ });
1053
1082
  const postcondition = evaluateMemoryPrunePostcondition(memoryPruneBeforeSnapshot, snapshot, {
1054
1083
  executorCompleted: memoryPruneExecutorCompleted,
1055
1084
  ...(memoryPruneRuntimeExitCode === undefined
@@ -0,0 +1,55 @@
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 initialized = await initializeLocalMemoryV2ForWorkspace(workspace, initialMode.enabled, input.launch.agentId);
31
+ const memory = initialized && !workspace.localMemoryV2Initialized
32
+ ? await readFile(join(workspace.dir, "MEMORY.md"), "utf8")
33
+ : workspace.memory;
34
+ const currentWorkspace = { ...workspace, memory };
35
+ const mode = initialized && !workspace.localMemoryV2Initialized
36
+ ? await resolveLocalMemoryV2Rollout({ workspace: currentWorkspace, handle: input.handle,
37
+ serverUrl: input.launch.serverUrl, agentId: input.launch.agentId,
38
+ requested: input.localMemoryV2Enabled === true, selfTraining: isolated,
39
+ ...(input.launch.description ? { description: input.launch.description } : {}) })
40
+ : initialMode;
41
+ return { workspace: { ...currentWorkspace, localMemoryV2Enabled: mode.enabled }, mode, initialized };
42
+ };
43
+ return await awaitWithCancellation(isolated ? prepare()
44
+ : withMemoryAccess(join(input.launch.agentsRoot, input.handle), prepare, {
45
+ create: true,
46
+ waitMs: EXECUTION_MEMORY_ACCESS_WAIT_MS,
47
+ initializedRecoveryAgentId: input.launch.agentId,
48
+ }), cancellation);
49
+ }
50
+ catch (error) {
51
+ logLocalMemoryContextPrepareFailure({ executionId: input.executionId, agentHandle: input.handle,
52
+ ...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }) }, "workspace_prepare", error);
53
+ throw error;
54
+ }
55
+ }
@@ -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,23 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, realpath } from "node:fs/promises";
3
+ const INITIALIZED_IDENTITY = "local-initialized";
4
+ export async function localMemoryInitializationIdentity(agentDir, agentId) {
5
+ const root = await realpath(agentDir);
6
+ const rootFingerprint = createHash("sha256").update(root).digest("hex");
7
+ const operation = createHash("sha256").update(`${agentId}\0${root}`, "utf8").digest("hex").slice(0, 24);
8
+ return {
9
+ workspaceId: INITIALIZED_IDENTITY,
10
+ agentId,
11
+ migrationId: `initialize-${operation}`,
12
+ sourceStorageVersion: 0,
13
+ targetStorageVersion: 2,
14
+ rootFingerprint,
15
+ ownerIdentity: String((await lstat(root)).uid),
16
+ machineId: INITIALIZED_IDENTITY,
17
+ executionId: `initialize-${operation}`,
18
+ };
19
+ }
20
+ export function isLocalMemoryInitializationIdentity(actual, expected) {
21
+ return Object.keys(expected)
22
+ .every(key => actual[key] === expected[key]);
23
+ }
@@ -29,14 +29,35 @@ export async function runMemoryInventoryCommand(args, io) {
29
29
  const issues = [...scan.issues, ...analysis.issues];
30
30
  const referenced = new Set(analysis.entries.map((entry) => entry.sourceFields.file));
31
31
  const files = scan.sources.map(({ path, bytes, sha256 }) => ({ path, bytes, sha256 }));
32
+ const orphanNotes = files.filter((file) => file.path.startsWith("notes/") && !referenced.has(file.path)).map((file) => file.path);
33
+ const storageVersion = (() => {
34
+ try {
35
+ return detectLocalMemoryStorageVersion(scan.sources.find((source) => source.path === "MEMORY.md")?.content ?? "");
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ })();
41
+ const indexedEntries = analysis.entries.filter((entry) => entry.format === "managed").length;
42
+ const legacyEntries = analysis.entries.length - indexedEntries;
32
43
  const report = {
33
44
  schemaVersion: 1, mode: "dry-run", agentDir: scan.root, applySupported: false,
34
45
  complete, scanComplete, requiresHumanReview: true,
46
+ // 只读派生观测,不改变资格判断,也不新增持久化状态。
47
+ // A dry-run does not create a shadow index or server state. Existing
48
+ // schema1 cards therefore remain legacy; indexedEntries is only coverage
49
+ // information, not proof that the Agent entered shadow mode.
50
+ storageMode: storageVersion === 2 ? "v2_write" : "legacy",
51
+ contextMode: storageVersion === 2 ? "bounded_preview" : "legacy_context",
52
+ readerMode: storageVersion === 2 ? "v2" : "legacy",
53
+ detectedIndexVersion: storageVersion,
54
+ migrationCoverage: { detectedIndexVersion: storageVersion, storageVersion, indexedEntries, legacyEntries, unindexedFiles: orphanNotes.length,
55
+ totalEntries: analysis.entries.length, complete },
35
56
  coverage: ["MEMORY.md", "notes/**", ".crew/memory/pending/**", "根目录的 .memory-result.v1-*.json"],
36
57
  limitations: ["不扫描历史执行目录;result清单仅覆盖Agent根目录。", "两遍摘要比对不等同跨文件原子快照,正式迁移仍需停止写入并重新核对。"],
37
58
  limits: inventoryLimits, totalBytes: scan.totalBytes, files, ...analysis, issues,
38
59
  pending: files.filter((file) => file.path.startsWith(".crew/memory/pending/")).map((file) => file.path),
39
- orphanNotes: files.filter((file) => file.path.startsWith("notes/") && !referenced.has(file.path)).map((file) => file.path),
60
+ orphanNotes,
40
61
  resultFiles: files.filter((file) => file.path.startsWith(".memory-result.v1-")).map((file) => file.path),
41
62
  };
42
63
  io.out(JSON.stringify(report, null, 2) + "\n");
@@ -59,3 +80,4 @@ import { parseArgs } from "node:util";
59
80
  import { z } from "zod";
60
81
  import { inventoryLimits, scanMemorySources } from "./inventory-source.js";
61
82
  import { previewMemoryInventory } from "./inventory-preview.js";
83
+ import { detectLocalMemoryStorageVersion } from "./storage-format.js";
@@ -73,7 +73,8 @@ export async function readMemoryInventorySource(root, file, budget) {
73
73
  await handle.close();
74
74
  }
75
75
  }
76
- export async function scanMemorySources(agentDir) {
76
+ export async function scanMemorySources(agentDir, options = {}) {
77
+ const includeOperationalFiles = options.includeOperationalFiles ?? true;
77
78
  const root = await realpath(agentDir);
78
79
  if ((await lstat(agentDir)).isSymbolicLink())
79
80
  throw new Error("agent_root_symlink");
@@ -124,10 +125,14 @@ export async function scanMemorySources(agentDir) {
124
125
  }
125
126
  await visit("MEMORY.md");
126
127
  await visit("notes", true);
127
- await visit(".crew/memory/pending", true);
128
- const resultNames = (await readdir(root)).filter((name) => /^\.memory-result\.v1-.*\.json$/u.test(name)).sort();
129
- for (const name of resultNames)
130
- await visit(name);
128
+ const resultNames = includeOperationalFiles
129
+ ? (await readdir(root)).filter((name) => /^\.memory-result\.v1-.*\.json$/u.test(name)).sort()
130
+ : [];
131
+ if (includeOperationalFiles) {
132
+ await visit(".crew/memory/pending", true);
133
+ for (const name of resultNames)
134
+ await visit(name);
135
+ }
131
136
  // 第二遍比对摘要和目录清单,发现扫描期间的变化就拒绝完整性结论;这不是跨文件事务锁。
132
137
  for (const source of sources) {
133
138
  try {
@@ -150,9 +155,11 @@ export async function scanMemorySources(agentDir) {
150
155
  addIssue("source_changed", file);
151
156
  }
152
157
  }
153
- const currentResults = (await readdir(root)).filter((name) => /^\.memory-result\.v1-.*\.json$/u.test(name)).sort();
154
- if (JSON.stringify(currentResults) !== JSON.stringify(resultNames))
155
- addIssue("source_changed", ".memory-result.v1-*.json");
158
+ if (includeOperationalFiles) {
159
+ const currentResults = (await readdir(root)).filter((name) => /^\.memory-result\.v1-.*\.json$/u.test(name)).sort();
160
+ if (JSON.stringify(currentResults) !== JSON.stringify(resultNames))
161
+ addIssue("source_changed", ".memory-result.v1-*.json");
162
+ }
156
163
  if (await realpath(agentDir) !== root)
157
164
  addIssue("source_changed", "agent_root");
158
165
  return { root, sources: sources.sort((a, b) => a.path.localeCompare(b.path)), issues, totalBytes };