@nowcrew/daemon 0.6.99-beta.3 → 0.6.99

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 (60) hide show
  1. package/README.md +9 -4
  2. package/dist/cloud-report-upload.js +10 -1
  3. package/dist/computer-main.js +0 -0
  4. package/dist/computer-service.js +30 -5
  5. package/dist/execution-journal.js +1 -3
  6. package/dist/execution-protocol.js +1 -1
  7. package/dist/execution-runner.js +1 -4
  8. package/dist/local-execution-prompt.js +3 -8
  9. package/dist/local-executor.js +39 -64
  10. package/dist/local-memory/index-budget.js +3 -9
  11. package/dist/local-memory/inventory-cli.js +8 -52
  12. package/dist/local-memory/inventory-source.js +8 -15
  13. package/dist/local-memory/pending-query.js +2 -5
  14. package/dist/local-memory/pending-schema.js +1 -1
  15. package/dist/local-memory/pending.js +2 -16
  16. package/dist/local-memory/prompt.js +5 -21
  17. package/dist/local-memory/read-cli.js +76 -98
  18. package/dist/local-memory/rollout.js +11 -79
  19. package/dist/local-memory/runtime.js +13 -181
  20. package/dist/local-memory/write.js +21 -189
  21. package/dist/local-memory-diagnostics.js +15 -96
  22. package/dist/local-memory-telemetry.js +3 -20
  23. package/dist/machine-info.js +0 -3
  24. package/dist/main.js +0 -0
  25. package/dist/marketplace-protocol.js +1 -1
  26. package/dist/marketplace-remote.js +1 -1
  27. package/dist/marketplace-runner.js +1 -1
  28. package/dist/memory-prune-diagnostics.js +4 -60
  29. package/dist/message-file-preview.js +1 -11
  30. package/dist/prompt.js +17 -9
  31. package/dist/runner.js +1 -1
  32. package/dist/runtimes/claude-thread-runner.js +4 -25
  33. package/dist/serve.js +3 -38
  34. package/dist/token.js +0 -1
  35. package/dist/workspace-control-handler.js +3 -16
  36. package/dist/workspace.js +2 -13
  37. package/package.json +10 -8
  38. package/dist/local-memory/commit-telemetry.js +0 -30
  39. package/dist/local-memory/execution-workspace.js +0 -62
  40. package/dist/local-memory/full-index-search.js +0 -47
  41. package/dist/local-memory/initialization-identity.js +0 -36
  42. package/dist/local-memory/inventory-v2.js +0 -362
  43. package/dist/local-memory/legacy-adapter.js +0 -233
  44. package/dist/local-memory/migration-access.js +0 -242
  45. package/dist/local-memory/migration-cli.js +0 -54
  46. package/dist/local-memory/migration-io.js +0 -199
  47. package/dist/local-memory/migration-pending.js +0 -9
  48. package/dist/local-memory/migration-plan.js +0 -432
  49. package/dist/local-memory/migration-schema.js +0 -55
  50. package/dist/local-memory/migration-state-invariants.js +0 -65
  51. package/dist/local-memory/migration-store.js +0 -414
  52. package/dist/local-memory/migration-tool-entry.js +0 -21
  53. package/dist/local-memory/source-path.js +0 -13
  54. package/dist/local-memory/storage-format.js +0 -249
  55. package/dist/local-memory/writer-mode.js +0 -13
  56. package/dist/memory-migration-controller.js +0 -89
  57. package/dist/memory-migration-inspect.js +0 -108
  58. package/dist/memory-migration-journal.js +0 -14
  59. package/dist/memory-migration-protocol.js +0 -14
  60. package/dist/memory-migration-runtime.js +0 -305
@@ -2,12 +2,6 @@ import { createHash } from "node:crypto";
2
2
  import { constants } from "node:fs";
3
3
  import { lstat, mkdir, open, readFile, rm, writeFile } from "node:fs/promises";
4
4
  import { join } from "node:path";
5
- import { applyMemoryMigration, prepareMemoryMigration, recoverMemoryMigration } from "./migration-store.js";
6
- import { MigrationStateSchema } from "./migration-schema.js";
7
- import { detectLocalMemoryStorageVersion } from "./storage-format.js";
8
- import { isLocalMemoryInitializationIdentity, localMemoryInitializationIdentity } from "./initialization-identity.js";
9
- import { inventory } from "./migration-io.js";
10
- import { initializeExactWorkspaceSeed } from "./migration-plan.js";
11
5
  import { applyLocalMemoryResult, parseLocalMemoryResult } from "./write.js";
12
6
  import { verifyWorkLogSource } from "./work-log.js";
13
7
  export function isLocalMemoryV2Eligible(input) {
@@ -18,7 +12,7 @@ export function isLocalMemoryV2Eligible(input) {
18
12
  return input.enabledForHandle && input.memoryPrune;
19
13
  }
20
14
  const LOCAL_MEMORY_V2_MARKER = "v2-enabled";
21
- async function writeLocalMemoryV2Marker(agentDir) {
15
+ export async function initializeLocalMemoryV2(agentDir) {
22
16
  const crewDir = join(agentDir, ".crew");
23
17
  const memoryDir = join(crewDir, "memory");
24
18
  await mkdir(crewDir, { recursive: true });
@@ -37,151 +31,24 @@ async function writeLocalMemoryV2Marker(agentDir) {
37
31
  throw error;
38
32
  }
39
33
  }
40
- function initializedIntent(identity, state) {
41
- return {
42
- ...identity,
43
- kind: "server_write_intent",
44
- sourceDigest: state.sourceDigest,
45
- planDigest: state.planDigest,
46
- intentVersion: 1,
47
- // New-Agent initialization is derived from the exact pristine seed. Keep
48
- // the receipt deterministic so a crash between intent publication and the
49
- // mutable state update can be retried without creating a conflicting fact.
50
- confirmedAt: "1970-01-01T00:00:00.000Z",
51
- };
52
- }
53
- /**
54
- * Initialize a pristine new Agent through the same journaled schema2
55
- * publication path used by migration. The marker is written only after the
56
- * committed catalog and state have both been verified.
57
- */
58
- async function initializeLocalMemoryV2WithBinding(agentDir, agentId, binding) {
59
- const identity = await localMemoryInitializationIdentity(agentDir, agentId, binding);
60
- let state = await prepareMemoryMigration({
61
- agentDir,
62
- identity,
63
- serverHistory: { kind: "none", isNewAgent: true },
64
- });
65
- if (state.origin !== "initialized")
66
- throw new Error("local_memory_v2_initialization_source_invalid");
67
- if (state.phase === "prepared") {
68
- state = await applyMemoryMigration({
69
- agentDir,
70
- identity,
71
- intentReceipt: initializedIntent(identity, state),
72
- });
73
- }
74
- else if (state.phase === "applying" || state.phase === "repair_required") {
75
- state = await recoverMemoryMigration({
76
- agentDir,
77
- identity,
78
- intentReceipt: state.intentReceipt ?? initializedIntent(identity, state),
79
- stoppedReceipt: {
80
- kind: "migration_process_stopped",
81
- migrationId: identity.migrationId,
82
- machineId: identity.machineId,
83
- executionId: identity.executionId,
84
- ownerIdentity: identity.ownerIdentity,
85
- pid: null,
86
- stoppedAt: new Date().toISOString(),
87
- proof: "journal_reconciled",
88
- },
89
- cancelled: false,
90
- });
91
- }
92
- const memory = await readFile(join(agentDir, "MEMORY.md"), "utf8");
93
- if (state.phase !== "committed" || state.origin !== "initialized" || state.storageVersion !== 2
94
- || detectLocalMemoryStorageVersion(memory) !== 2) {
95
- throw new Error("local_memory_v2_initialization_incomplete");
96
- }
97
- await writeLocalMemoryV2Marker(agentDir);
98
- }
99
- /** Fresh initialization requires the identity from the authenticated ready frame. */
100
- export function initializeLocalMemoryV2(agentDir, agentId, binding) {
101
- return initializeLocalMemoryV2WithBinding(agentDir, agentId, binding);
102
- }
103
- export async function initializeLocalMemoryV2ForWorkspace(workspace, enabledForHandle, agentId, binding) {
34
+ export async function initializeLocalMemoryV2ForWorkspace(workspace, enabledForHandle) {
104
35
  const initialized = workspace.localMemoryV2Initialized === true;
105
- if (initialized)
106
- return true;
107
- // Real workspaces always report this boolean. Keep partial/mock workspace
108
- // callers side-effect free instead of probing an unrelated filesystem path.
109
- if (workspace.localMemoryV2Initialized !== false)
110
- return false;
111
- let resumable = false;
112
- let effectiveBinding = binding;
36
+ if (!enabledForHandle || initialized || workspace.localMemoryV2Initialized !== false
37
+ || workspace.agentHomePreexisted === true || workspace.memorySeedCreated !== true)
38
+ return initialized;
113
39
  try {
114
- const state = MigrationStateSchema.parse(JSON.parse(await readFile(join(workspace.dir, ".crew", "memory", "migration.v1.json"), "utf8")));
115
- const identity = await localMemoryInitializationIdentity(workspace.dir, agentId, binding);
116
- const legacyIdentity = binding === undefined
117
- ? identity
118
- : await localMemoryInitializationIdentity(workspace.dir, agentId);
119
- resumable = state.origin === "initialized"
120
- && state.phase !== "rolled_back"
121
- && (isLocalMemoryInitializationIdentity(state.identity, identity)
122
- || isLocalMemoryInitializationIdentity(state.identity, legacyIdentity));
123
- if (resumable && isLocalMemoryInitializationIdentity(state.identity, legacyIdentity))
124
- effectiveBinding = undefined;
125
- }
126
- catch (error) {
127
- if (error.code !== "ENOENT")
128
- throw error;
129
- try {
130
- const migrations = await lstat(join(workspace.dir, ".crew", "memory", "migrations"));
131
- if (!migrations.isDirectory() || migrations.isSymbolicLink()) {
132
- throw new Error("local_memory_v2_initialization_residue_invalid");
133
- }
134
- const identity = await localMemoryInitializationIdentity(workspace.dir, agentId, binding);
135
- const legacyIdentity = binding === undefined
136
- ? identity
137
- : await localMemoryInitializationIdentity(workspace.dir, agentId);
138
- let operationIdentity = identity;
139
- const operation = await lstat(join(workspace.dir, ".crew", "memory", "migrations", identity.migrationId))
140
- .catch(async (error) => {
141
- if (error.code !== "ENOENT" || identity.migrationId === legacyIdentity.migrationId)
142
- throw error;
143
- operationIdentity = legacyIdentity;
144
- return lstat(join(workspace.dir, ".crew", "memory", "migrations", legacyIdentity.migrationId));
145
- });
146
- if (!operation.isDirectory() || operation.isSymbolicLink()) {
147
- throw new Error("local_memory_v2_initialization_residue_invalid");
148
- }
149
- const current = await inventory(workspace.dir);
150
- const memory = current.files.get("MEMORY.md");
151
- resumable = current.files.size === 1 && memory !== undefined
152
- && current.directories.every(path => path === "notes" || path === ".crew/memory/pending")
153
- && initializeExactWorkspaceSeed(memory) !== null;
154
- if (resumable && operationIdentity.migrationId === legacyIdentity.migrationId)
155
- effectiveBinding = undefined;
156
- }
157
- catch (residueError) {
158
- if (residueError.code !== "ENOENT")
159
- throw residueError;
160
- }
40
+ await initializeLocalMemoryV2(workspace.dir);
41
+ return true;
161
42
  }
162
- const pristineFirstInitialization = enabledForHandle
163
- && workspace.localMemoryV2Initialized === false
164
- && workspace.agentHomePreexisted !== true
165
- && workspace.memorySeedCreated === true;
166
- if (!resumable && !pristineFirstInitialization)
167
- return false;
168
- // Never create another unbound sentinel. Undefined is retained solely to
169
- // finish a pre-upgrade journal already proven above.
170
- if (!resumable && effectiveBinding === undefined)
43
+ catch {
171
44
  return false;
172
- await initializeLocalMemoryV2WithBinding(workspace.dir, agentId, effectiveBinding);
173
- return true;
45
+ }
174
46
  }
175
47
  export function localMemoryResultPath(runDir, executionId) {
176
48
  const suffix = createHash("sha256").update(executionId, "utf8").digest("hex").slice(0, 32);
177
49
  return join(runDir, `.memory-result.v1-${suffix}.json`);
178
50
  }
179
- export function renderLocalMemoryResultInstructions(resultPath, storageVersion = 1, migrationDeferred = false) {
180
- const storagePolicy = migrationDeferred
181
- ? "该 Agent 的旧记忆仍在迁移审阅中。本轮只允许提交新的 create 候选;daemon 会把候选原子保存到 .crew/memory/pending/,reason=legacy_requires_migration,并提供 search-pending/read-pending 查询。候选不会进入 active,也不会自动注入普通上下文;更新、替代和 statusChanges 必须留待迁移完成后按当前 V2 ID/revision/hash 重新核对。旧 MEMORY.md、notes 和已有 active/pending 保持不变。"
182
- : storageVersion === 2
183
- ? "完整目录保存在 MEMORY.md,模型只看到有界预览;未展示记录仍然有效。涉及旧规则时先用 crew-memory search 查询完整目录,按当前 ID/revision 更新,不因预览未命中就重复新建。20条预览不是有效记录上限;完整存储超出资源上限会拒绝提交,不得自行删除或降级旧记录。已有 pending 仍是未应用候选,须重新核对授权、重复与版本后通过 changes 申请保存,不因迁移自动生效。"
184
- : "daemon 会校验、去重、限制 MEMORY.md 大小,超出索引容量的候选会保存在 pending 归档中。容量暂存的 create 可按需检索参考但不能覆盖现行规则;有空间的后续 proposed 提交可提升。未应用的 update/supersede 不进入普通召回,必须重新核对目标版本后处理。";
51
+ export function renderLocalMemoryResultInstructions(resultPath) {
185
52
  return `
186
53
 
187
54
  ## Structured memory prune (daemon-managed)
@@ -203,7 +70,7 @@ outcome 只允许 no_change、proposed、conflict、failed。新增、更新、
203
70
  \`\`\`
204
71
 
205
72
  每个 changes 项须保留示例中的全部字段。operation=create|update|supersede;kind=procedural|semantic|episodic|preference;priority=high|medium|low;detailCategory=preferences|project-rules|testing|debugging|git|incidents|lessons。create 的 memoryId/baseRevision/supersedesMemoryId 必须为 null;update 按现有索引填写 memoryId/baseRevision,supersedesMemoryId 为 null;supersede 填 supersedesMemoryId/baseRevision,memoryId 为 null。
206
- 只提交可复用、与本次任务明确相关的少量候选;不要写入凭证、token、隐私或内部控制标记。${storagePolicy}
73
+ 只提交可复用、与本次任务明确相关的少量候选;不要写入凭证、token、隐私或内部控制标记。daemon 会校验、去重、限制 MEMORY.md 大小,超出索引容量的候选会保存在 pending 归档中。容量暂存的 create 可按需检索参考但不能覆盖现行规则;有空间的后续 proposed 提交可提升。未应用的 update/supersede 不进入普通召回,必须重新核对目标版本后处理。
207
74
  写入候选文件只表示提交候选,不能提前宣称已经记住或规则已经更新;daemon 在本轮结束后才处理,候选持久化与进入有效索引也不是同一结果。本结构化轮次覆盖前文提前清理工作日志的要求:保留 $CREW_TASK_LOG,不要清空或删除;daemon 将在处理成功后清理,失败时保留日志和候选用于排查或重试。
208
75
  `;
209
76
  }
@@ -214,20 +81,6 @@ export function renderLocalMemoryV2WriteGuard() {
214
81
  ## V2 shared memory write safety
215
82
  本节覆盖前文关于普通任务直接更新共享记忆的例外规则。本 Agent 已启用结构化本地记忆 V2。普通任务不得直接编辑共享的 $CREW_HOME/MEMORY.md 或 $CREW_HOME/notes/ 文件;即使用户明确要求记住内容,也只把候选、用户授权和分类理由记录到本任务的 $CREW_TASK_LOG,留给任务结束后的 memory-prune 结构化收尾应用。只有 memory-prune 收尾提示明确要求写入 daemon 提供的 result 文件时,才按该协议提交候选。`;
216
83
  }
217
- async function localMemoryStorageFacts(agentDir) {
218
- const memory = await readFile(join(agentDir, "MEMORY.md"), "utf8");
219
- const storageVersion = detectLocalMemoryStorageVersion(memory);
220
- let committedState = false;
221
- try {
222
- const state = MigrationStateSchema.parse(JSON.parse(await readFile(join(agentDir, ".crew", "memory", "migration.v1.json"), "utf8")));
223
- committedState = state.phase === "committed" && state.storageVersion === 2;
224
- }
225
- catch (error) {
226
- if (error.code !== "ENOENT" && storageVersion === 2)
227
- throw error;
228
- }
229
- return { storageVersion, committedState };
230
- }
231
84
  function workLogFailure(error) {
232
85
  const reason = error instanceof Error ? error.message : "";
233
86
  return { status: "failed", error: reason === "local_memory_work_log_source_changed"
@@ -245,9 +98,6 @@ export async function commitLocalMemoryResultAfterExit(input) {
245
98
  return { status: "missing" };
246
99
  return { status: "failed", error: error instanceof Error ? error.message : "read_failed" };
247
100
  }
248
- let storageBefore;
249
- let storageAfter;
250
- let applied;
251
101
  try {
252
102
  const result = parseLocalMemoryResult(raw);
253
103
  if (result.outcome === "conflict" || result.outcome === "failed") {
@@ -261,25 +111,12 @@ export async function commitLocalMemoryResultAfterExit(input) {
261
111
  return workLogFailure(error);
262
112
  }
263
113
  }
264
- storageBefore = await localMemoryStorageFacts(input.agentDir);
265
- if (input.requiredStorageVersion === 2
266
- && (storageBefore.storageVersion !== 2 || !storageBefore.committedState)) {
267
- return { status: "failed", error: "local_memory_v2_state_invalid_before_commit" };
268
- }
269
- applied = await applyLocalMemoryResult({
114
+ await applyLocalMemoryResult({
270
115
  agentDir: input.agentDir,
271
116
  result,
272
117
  executionId: input.executionId,
273
118
  pruneTraceId: input.pruneTraceId,
274
- ...(input.writerCapabilities === undefined ? {} : { writerCapabilities: input.writerCapabilities }),
275
- ...(input.requiredStorageVersion === undefined ? {} : { requiredStorageVersion: input.requiredStorageVersion }),
276
- ...(input.migrationDeferredPending ? { migrationDeferredPending: true } : {}),
277
119
  });
278
- storageAfter = await localMemoryStorageFacts(input.agentDir);
279
- if (input.requiredStorageVersion === 2
280
- && (storageAfter.storageVersion !== 2 || !storageAfter.committedState)) {
281
- return { status: "failed", error: "local_memory_v2_state_invalid_after_commit" };
282
- }
283
120
  }
284
121
  catch (error) {
285
122
  return { status: "failed", error: error instanceof Error ? error.message : "apply_failed" };
@@ -310,10 +147,5 @@ export async function commitLocalMemoryResultAfterExit(input) {
310
147
  catch {
311
148
  return { status: "failed", error: "local_memory_result_cleanup_failed" };
312
149
  }
313
- return { status: "applied", storageVersionBefore: storageBefore.storageVersion,
314
- storageVersionAfter: storageAfter.storageVersion, committedState: storageAfter.committedState,
315
- createdIds: applied.createdIds, updatedIds: applied.updatedIds,
316
- statusChangedIds: applied.statusChangedIds, pendingIds: applied.pendingIds,
317
- ...(input.migrationDeferredPending
318
- ? { pendingReason: "legacy_requires_migration" } : {}) };
150
+ return { status: "applied" };
319
151
  }
@@ -6,8 +6,6 @@ import { dslog } from "../slog.js";
6
6
  import { LocalMemoryChangeSchema, LocalMemorySingleLineText, validateLocalMemoryChange, } from "./candidate.js";
7
7
  import { LOCAL_MEMORY_INDEX_CODE_POINT_CAP, LOCAL_MEMORY_INDEX_ENTRY_CAP, LOCAL_MEMORY_TOTAL_CODE_POINT_CAP, renderGeneratedIndex, selectLocalMemoryIndexEntries, } from "./index-budget.js";
8
8
  import { persistPendingMemoryCandidate, readPendingMemoryCandidates, removePendingMemoryCandidate, } from "./pending.js";
9
- import { assertLocalMemoryStorageSize, detectLocalMemoryStorageVersion, LOCAL_MEMORY_GENERATED_INDEX_PATTERN, LOCAL_MEMORY_NOTE_BYTE_CAP, LOCAL_MEMORY_READ_BYTE_CAP, parseFullMemoryIndex, replaceFullMemoryIndex, selectLocalMemoryPreview, } from "./storage-format.js";
10
- import { assertMemoryInventoryHealth, assertMemoryWriterAllowed, withMemoryAccess } from "./migration-access.js";
11
9
  const StatusChangeSchema = z.object({
12
10
  memoryId: LocalMemorySingleLineText,
13
11
  expectedRevision: z.number().int().positive(),
@@ -92,14 +90,12 @@ function parseCards(section) {
92
90
  });
93
91
  }
94
92
  export function parseLocalMemoryIndex(memory) {
95
- if (detectLocalMemoryStorageVersion(memory) === 2)
96
- return parseFullMemoryIndex(memory);
97
93
  const section = indexSection(memory);
98
94
  const generated = GENERATED_INDEX_PATTERN.exec(section)?.[0];
99
95
  return parseCards(generated ?? section);
100
96
  }
101
97
  function parseUnmarkedMemoryIndex(memory) {
102
- const section = indexSection(memory).replace(LOCAL_MEMORY_GENERATED_INDEX_PATTERN, "");
98
+ const section = indexSection(memory).replace(GENERATED_INDEX_PATTERN, "");
103
99
  return parseCards(section);
104
100
  }
105
101
  function replaceGeneratedIndex(memory, entries) {
@@ -126,36 +122,11 @@ function noteSection(id, revision, title, body) {
126
122
  }
127
123
  function notePattern(id) {
128
124
  const escaped = id.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
129
- return new RegExp(`<!--\\s*NOWWORK:MEMORY_ENTRY:BEGIN\\s+id=${escaped}(?=\\s|-->)[^>]*-->[\\s\\S]*?<!--\\s*NOWWORK:MEMORY_ENTRY:END\\s+id=${escaped}\\s*-->\\s*`, "u");
130
- }
131
- function uniqueNoteSection(content, id) {
132
- const escaped = id.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
133
- const begins = [...content.matchAll(new RegExp(`<!--\\s*NOWWORK:MEMORY_ENTRY:BEGIN\\s+id=${escaped}(?=\\s|-->)`, "gu"))];
134
- const ends = [...content.matchAll(new RegExp(`<!--\\s*NOWWORK:MEMORY_ENTRY:END\\s+id=${escaped}\\s*-->`, "gu"))];
135
- if (begins.length === 0 && ends.length === 0)
136
- return null;
137
- const section = notePattern(id).exec(content)?.[0];
138
- if (begins.length !== 1 || ends.length !== 1 || section === undefined
139
- || (section.match(/NOWWORK:MEMORY_ENTRY:(?:BEGIN|END)/gu) ?? []).length !== 2) {
140
- throw new Error("local_memory_note_anchor_invalid");
141
- }
142
- return section;
125
+ return new RegExp(`<!--\\s*NOWWORK:MEMORY_ENTRY:BEGIN\\s+id=${escaped}[^>]*-->[\\s\\S]*?<!--\\s*NOWWORK:MEMORY_ENTRY:END\\s+id=${escaped}\\s*-->\\s*`, "u");
143
126
  }
144
127
  function replaceNote(content, id, replacement) {
145
128
  const pattern = notePattern(id);
146
- return uniqueNoteSection(content, id) !== null ? content.replace(pattern, () => replacement) : null;
147
- }
148
- function updateNoteRevision(content, id, revision) {
149
- const section = uniqueNoteSection(content, id);
150
- const beginning = `<!-- NOWWORK:MEMORY_ENTRY:BEGIN id=${id} `;
151
- const ending = `<!-- NOWWORK:MEMORY_ENTRY:END id=${id} -->`;
152
- if (section === null || content.split(beginning).length !== 2 || content.split(ending).length !== 2) {
153
- throw new Error("local_memory_note_anchor_missing");
154
- }
155
- const updated = section.replace(/^(<!--\s*NOWWORK:MEMORY_ENTRY:BEGIN\s+id=[A-Za-z0-9_-]+\s+revision=)[1-9][0-9]*(\s*-->)/u, (_match, prefix, suffix) => `${prefix}${revision}${suffix}`);
156
- if (!updated.startsWith(`${beginning}revision=${revision} -->`))
157
- throw new Error("local_memory_note_anchor_missing");
158
- return content.replace(notePattern(id), () => updated);
129
+ return pattern.test(content) ? content.replace(pattern, replacement) : null;
159
130
  }
160
131
  function validateCandidate(result) {
161
132
  if (result.outcome === "no_change" && (result.changes.length > 0 || result.statusChanges.length > 0)) {
@@ -209,25 +180,6 @@ function stableId(executionId, index, change) {
209
180
  const input = [executionId, String(index), change.operation, change.title, change.summary, change.detailBody].join("\0");
210
181
  return `mem_${createHash("sha256").update(input, "utf8").digest("hex").slice(0, 12)}`;
211
182
  }
212
- function migrationPendingId(change) {
213
- // Keep the digest independent of object insertion order for callers that
214
- // reach applyLocalMemoryResult directly instead of the JSON/Zod boundary.
215
- const canonicalChange = {
216
- operation: change.operation,
217
- memoryId: change.memoryId,
218
- baseRevision: change.baseRevision,
219
- supersedesMemoryId: change.supersedesMemoryId,
220
- title: change.title,
221
- kind: change.kind,
222
- summary: change.summary,
223
- triggers: change.triggers,
224
- priority: change.priority,
225
- detailCategory: change.detailCategory,
226
- detailBody: change.detailBody,
227
- reviewAfter: change.reviewAfter,
228
- };
229
- return `pending-migration-${createHash("sha256").update(JSON.stringify(canonicalChange), "utf8").digest("hex").slice(0, 12)}`;
230
- }
231
183
  function normalize(value) {
232
184
  return value.normalize("NFKC").toLocaleLowerCase().replace(/\s+/gu, " ").trim();
233
185
  }
@@ -247,14 +199,14 @@ function detailCategoryFromFile(file) {
247
199
  return DETAIL_CATEGORIES.has(category) ? category : null;
248
200
  }
249
201
  function readNoteBody(content, id) {
250
- const section = uniqueNoteSection(content, id);
251
- if (section === null)
202
+ const begin = content.indexOf(`<!-- NOWWORK:MEMORY_ENTRY:BEGIN id=${id}`);
203
+ if (begin < 0)
252
204
  return null;
253
- const bodyStart = section.indexOf("\n\n");
254
- const bodyEnd = section.indexOf(`\n<!-- NOWWORK:MEMORY_ENTRY:END id=${id}`, bodyStart + 2);
205
+ const bodyStart = content.indexOf("\n\n", begin);
206
+ const bodyEnd = content.indexOf(`\n<!-- NOWWORK:MEMORY_ENTRY:END id=${id}`, bodyStart + 2);
255
207
  if (bodyStart < 0 || bodyEnd < 0)
256
208
  return null;
257
- return section.slice(bodyStart + 2, bodyEnd).trim();
209
+ return content.slice(bodyStart + 2, bodyEnd).trim();
258
210
  }
259
211
  function archivedChange(entry, detailBody) {
260
212
  const detailCategory = detailCategoryFromFile(entry.file);
@@ -278,34 +230,6 @@ function archivedChange(entry, detailBody) {
278
230
  function emptyResult() {
279
231
  return { createdIds: [], updatedIds: [], statusChangedIds: [], deferredIds: [], pendingIds: [] };
280
232
  }
281
- async function stageMigrationDeferredPending(input) {
282
- if (input.result.outcome !== "proposed")
283
- return emptyResult();
284
- if (input.result.statusChanges.length > 0 || input.result.changes.some((change) => change.operation !== "create")) {
285
- throw new Error("local_memory_migration_pending_create_only");
286
- }
287
- const executionId = input.executionId ?? "unknown";
288
- const pending = input.result.changes.map((change) => ({
289
- schemaVersion: 1,
290
- pendingId: migrationPendingId(change),
291
- createdAt: new Date().toISOString(),
292
- reason: "legacy_requires_migration",
293
- source: { executionId, pruneTraceId: input.pruneTraceId ?? null },
294
- candidate: change,
295
- }));
296
- await assertMemoryAccessDirectory(input.agentDir);
297
- await withMemoryAccess(input.agentDir, async () => {
298
- const memory = await readFile(join(input.agentDir, "MEMORY.md"), "utf8").catch(() => null);
299
- if (memory === null || detectLocalMemoryStorageVersion(memory) !== 1) {
300
- throw new Error("local_memory_migration_pending_storage_invalid");
301
- }
302
- for (const candidate of pending) {
303
- await persistPendingMemoryCandidate({ agentDir: input.agentDir, candidate });
304
- }
305
- }, { pendingOnly: true });
306
- const pendingIds = [...new Set(pending.map((candidate) => candidate.pendingId))];
307
- return { createdIds: [], updatedIds: [], statusChangedIds: [], deferredIds: pendingIds, pendingIds };
308
- }
309
233
  function isWithin(root, candidate) {
310
234
  const path = relative(root, candidate);
311
235
  return path === "" || (path !== ".." && !path.startsWith(`..${sep}`));
@@ -320,29 +244,12 @@ async function assertWritablePaths(agentDir, memoryPath) {
320
244
  if (!memoryStat.isFile() || memoryStat.isSymbolicLink())
321
245
  throw new Error("local_memory_memory_path_invalid");
322
246
  }
323
- async function assertMemoryAccessDirectory(agentDir) {
324
- try {
325
- const stat = await lstat(join(agentDir, ".crew/memory"));
326
- if (!stat.isDirectory() || stat.isSymbolicLink())
327
- throw new Error("local_memory_pending_path_invalid");
328
- }
329
- catch (error) {
330
- if (error.code === "ENOENT")
331
- return;
332
- throw error;
333
- }
334
- }
335
247
  async function readNote(path) {
336
248
  try {
337
249
  const stat = await lstat(path);
338
250
  if (!stat.isFile() || stat.isSymbolicLink())
339
251
  throw new Error("local_memory_note_path_invalid");
340
- if (stat.size > LOCAL_MEMORY_NOTE_BYTE_CAP)
341
- throw new Error("local_memory_note_byte_limit");
342
- const content = await readFile(path, "utf8");
343
- if (Buffer.byteLength(content, "utf8") > LOCAL_MEMORY_NOTE_BYTE_CAP)
344
- throw new Error("local_memory_note_byte_limit");
345
- return content;
252
+ return await readFile(path, "utf8");
346
253
  }
347
254
  catch (error) {
348
255
  if (error.code === "ENOENT")
@@ -362,11 +269,6 @@ async function atomicWrite(path, content) {
362
269
  }
363
270
  }
364
271
  function projectIndex(memory, entries, protectedIds) {
365
- if (detectLocalMemoryStorageVersion(memory) === 2) {
366
- const updated = replaceFullMemoryIndex(memory, entries);
367
- selectLocalMemoryPreview(updated);
368
- return { memory: updated, omittedIds: [], reason: null };
369
- }
370
272
  const empty = replaceGeneratedIndex(memory, []);
371
273
  if (codePointLength(empty) > LOCAL_MEMORY_TOTAL_CODE_POINT_CAP)
372
274
  throw new Error("local_memory_memory_over_budget");
@@ -404,29 +306,12 @@ async function withApplyLease(agentDir, operation) {
404
306
  }
405
307
  export async function applyLocalMemoryResult(input) {
406
308
  validateCandidate(input.result);
407
- if (input.migrationDeferredPending)
408
- return stageMigrationDeferredPending(input);
409
- if (input.result.outcome !== "proposed" && input.requiredStorageVersion !== 2)
309
+ if (input.result.outcome !== "proposed")
410
310
  return emptyResult();
411
311
  const memoryPath = join(input.agentDir, "MEMORY.md");
412
- if (input.result.outcome === "proposed")
413
- await assertWritablePaths(input.agentDir, memoryPath);
414
- await assertMemoryAccessDirectory(input.agentDir);
415
- return withApplyLease(input.agentDir, async () => withMemoryAccess(input.agentDir, async () => {
416
- await assertMemoryWriterAllowed(input.agentDir, input.writerCapabilities ?? []);
417
- const memoryStat = await lstat(memoryPath);
418
- if (memoryStat.size > 16 * 1024 * 1024)
419
- throw new Error("local_memory_storage_byte_limit");
312
+ await assertWritablePaths(input.agentDir, memoryPath);
313
+ return withApplyLease(input.agentDir, async () => {
420
314
  const memory = await readFile(memoryPath, "utf8");
421
- assertLocalMemoryStorageSize(memory);
422
- const fullStorage = detectLocalMemoryStorageVersion(memory) === 2;
423
- if (input.requiredStorageVersion === 2 && !fullStorage) {
424
- throw new Error("local_memory_v2_state_invalid_before_commit");
425
- }
426
- // A V2 no-op is still a commit decision. Return only after the access
427
- // lock and committed-state writer gate have validated the same snapshot.
428
- if (input.result.outcome !== "proposed")
429
- return emptyResult();
430
315
  if (parseUnmarkedMemoryIndex(memory).length > 0)
431
316
  throw new Error("local_memory_legacy_index_requires_migration");
432
317
  let entries = [...parseLocalMemoryIndex(memory)];
@@ -457,26 +342,6 @@ export async function applyLocalMemoryResult(input) {
457
342
  const pendingCandidates = [];
458
343
  const resolvedPendingIds = new Set();
459
344
  const noteUpdates = new Map();
460
- const noteCache = new Map();
461
- let totalReadBytes = Buffer.byteLength(memory, "utf8");
462
- const readStoredNote = async (path) => {
463
- const cached = noteUpdates.get(path) ?? noteCache.get(path);
464
- if (cached !== undefined)
465
- return cached;
466
- const stat = await lstat(path).catch((error) => {
467
- if (error.code === "ENOENT")
468
- return null;
469
- throw error;
470
- });
471
- if (stat !== null && totalReadBytes + stat.size > LOCAL_MEMORY_READ_BYTE_CAP)
472
- throw new Error("local_memory_read_byte_limit");
473
- const content = await readNote(path);
474
- totalReadBytes += Buffer.byteLength(content, "utf8");
475
- if (totalReadBytes > LOCAL_MEMORY_READ_BYTE_CAP)
476
- throw new Error("local_memory_read_byte_limit");
477
- noteCache.set(path, content);
478
- return content;
479
- };
480
345
  let allowActiveEviction = false;
481
346
  const randomId = input.randomId ?? randomUUID;
482
347
  const executionId = input.executionId ?? "unknown";
@@ -518,21 +383,10 @@ export async function applyLocalMemoryResult(input) {
518
383
  if (existingCreate !== undefined && !matchesCreateCandidate(existingCreate, change)) {
519
384
  throw new Error("local_memory_duplicate_candidate");
520
385
  }
521
- if (existingCreate === undefined && entries.some((entry) => (!fullStorage || entry.status === "active" || entry.status === "pinned")
522
- && (normalize(entry.title) === normalize(change.title) || normalize(entry.summary) === normalize(change.summary)))) {
386
+ if (existingCreate === undefined && entries.some((entry) => normalize(entry.title) === normalize(change.title)
387
+ || normalize(entry.summary) === normalize(change.summary))) {
523
388
  throw new Error("local_memory_duplicate_candidate");
524
389
  }
525
- if (existingCreate !== undefined) {
526
- if (existingCreate.revision !== 1 || existingCreate.status !== "active") {
527
- throw new Error("local_memory_create_replay_conflict");
528
- }
529
- const stored = await readStoredNote(resolve(input.agentDir, existingCreate.file));
530
- if (uniqueNoteSection(stored, id)?.trimEnd() !== noteSection(id, 1, change.title, change.detailBody).trimEnd()) {
531
- throw new Error("local_memory_create_replay_conflict");
532
- }
533
- // A proven duplicate is a no-op; it must never rewrite a newer body or status.
534
- continue;
535
- }
536
390
  }
537
391
  const revision = existingCreate?.revision ?? (current === undefined ? 1 : current.revision + 1);
538
392
  const entry = existingCreate ?? entryFromChange(change, id, revision, current?.status ?? "active");
@@ -546,7 +400,7 @@ export async function applyLocalMemoryResult(input) {
546
400
  : item),
547
401
  entry,
548
402
  ];
549
- const allowEvictionForChange = !fullStorage && change.operation === "create" && change.priority === "high";
403
+ const allowEvictionForChange = change.operation === "create" && change.priority === "high";
550
404
  const protectedIds = new Set(allowEvictionForChange ? pinnedIds(entries) : activeOrPinnedIds(entries));
551
405
  if (allowEvictionForChange)
552
406
  allowActiveEviction = true;
@@ -563,7 +417,7 @@ export async function applyLocalMemoryResult(input) {
563
417
  const notePath = resolve(input.agentDir, entry.file);
564
418
  if (!isWithin(resolve(input.agentDir, "notes"), notePath))
565
419
  throw new Error("local_memory_note_path_invalid");
566
- const previous = await readStoredNote(notePath);
420
+ const previous = noteUpdates.get(notePath) ?? await readNote(notePath);
567
421
  const replacement = noteSection(id, revision, change.title, change.detailBody);
568
422
  const existingSection = replaceNote(previous, id, replacement);
569
423
  const next = existingSection ?? (current === undefined
@@ -581,7 +435,7 @@ export async function applyLocalMemoryResult(input) {
581
435
  // Pending create candidates are durable overflow, not dead ends. Promote only
582
436
  // when the current catalog has room; supersede/update candidates stay pending
583
437
  // until a fresh revision-aware decision can safely replay them.
584
- const pendingCandidatesOnDisk = fullStorage ? [] : await readPendingMemoryCandidates({ agentDir: input.agentDir }).catch((error) => {
438
+ const pendingCandidatesOnDisk = await readPendingMemoryCandidates({ agentDir: input.agentDir }).catch((error) => {
585
439
  if (!(error instanceof Error) || error.message !== "memory_pending_directory_over_budget")
586
440
  throw error;
587
441
  // 自动提升是可选工作,目录查询预算不应阻断本轮已验证的更新或腾位操作。
@@ -592,10 +446,6 @@ export async function applyLocalMemoryResult(input) {
592
446
  return [];
593
447
  });
594
448
  for (const pending of pendingCandidatesOnDisk) {
595
- // Migration-deferred candidates require reviewed import; only capacity
596
- // overflow is eligible for the existing automatic promotion path.
597
- if (pending.reason !== "index_over_budget")
598
- continue;
599
449
  const change = pending.candidate;
600
450
  if (change.operation !== "create")
601
451
  continue;
@@ -610,7 +460,7 @@ export async function applyLocalMemoryResult(input) {
610
460
  || normalize(entry.summary) === normalize(change.summary)));
611
461
  if (duplicate !== undefined) {
612
462
  const duplicatePath = resolve(input.agentDir, duplicate.file);
613
- const duplicateBody = readNoteBody(await readStoredNote(duplicatePath), duplicate.id);
463
+ const duplicateBody = readNoteBody(noteUpdates.get(duplicatePath) ?? await readNote(duplicatePath), duplicate.id);
614
464
  if (duplicateBody === change.detailBody
615
465
  || duplicateBody === unwrappedNoteBody(change.title, change.detailBody).trim()) {
616
466
  resolvedPendingIds.add(pending.pendingId);
@@ -628,7 +478,7 @@ export async function applyLocalMemoryResult(input) {
628
478
  const notePath = resolve(input.agentDir, entry.file);
629
479
  if (!isWithin(resolve(input.agentDir, "notes"), notePath))
630
480
  throw new Error("local_memory_note_path_invalid");
631
- const previous = await readStoredNote(notePath);
481
+ const previous = noteUpdates.get(notePath) ?? await readNote(notePath);
632
482
  const replacement = noteSection(id, 1, change.title, change.detailBody);
633
483
  const existingSection = replaceNote(previous, id, replacement);
634
484
  const next = existingSection ?? `${previous.trimEnd()}${previous.trimEnd() ? "\n\n" : ""}${replacement}`;
@@ -637,17 +487,6 @@ export async function applyLocalMemoryResult(input) {
637
487
  createdIds.push(id);
638
488
  resolvedPendingIds.add(pending.pendingId);
639
489
  }
640
- if (fullStorage) {
641
- const changedMetadataIds = new Set([
642
- ...statusChangedIds,
643
- ...input.result.changes.flatMap(change => change.supersedesMemoryId === null ? [] : [change.supersedesMemoryId]),
644
- ]);
645
- for (const id of changedMetadataIds) {
646
- const entry = entries.find(item => item.id === id);
647
- const notePath = resolve(input.agentDir, entry.file);
648
- noteUpdates.set(notePath, updateNoteRevision(await readStoredNote(notePath), id, entry.revision));
649
- }
650
- }
651
490
  let finalProjection = projectIndex(memory, entries, allowActiveEviction ? pinnedIds(entries) : activeOrPinnedIds(entries));
652
491
  if (finalProjection.reason !== null
653
492
  && (!allowActiveEviction || finalProjection.reason === "index_over_budget_pinned")) {
@@ -659,7 +498,7 @@ export async function applyLocalMemoryResult(input) {
659
498
  .filter((entry) => entry?.status === "active");
660
499
  for (const entry of omittedActiveEntries) {
661
500
  const notePath = resolve(input.agentDir, entry.file);
662
- const content = await readStoredNote(notePath);
501
+ const content = noteUpdates.get(notePath) ?? await readNote(notePath);
663
502
  const detailBody = readNoteBody(content, entry.id);
664
503
  const change = detailBody === null ? null : archivedChange(entry, detailBody);
665
504
  if (change === null)
@@ -680,13 +519,6 @@ export async function applyLocalMemoryResult(input) {
680
519
  if (finalProjection.reason !== null)
681
520
  throw new Error(finalProjection.reason);
682
521
  }
683
- // Complete every resource check before the first persistent write.
684
- assertLocalMemoryStorageSize(finalProjection.memory);
685
- for (const content of noteUpdates.values()) {
686
- if (Buffer.byteLength(content, "utf8") > LOCAL_MEMORY_NOTE_BYTE_CAP)
687
- throw new Error("local_memory_note_byte_limit");
688
- }
689
- await assertMemoryInventoryHealth(input.agentDir);
690
522
  for (const candidate of pendingCandidates)
691
523
  await persistPendingMemoryCandidate({ agentDir: input.agentDir, candidate });
692
524
  for (const [path, content] of noteUpdates)
@@ -703,5 +535,5 @@ export async function applyLocalMemoryResult(input) {
703
535
  deferredIds,
704
536
  pendingIds: deferredIds,
705
537
  };
706
- }));
538
+ });
707
539
  }