@nowcrew/daemon 0.6.97-beta.1 → 0.6.97-beta.2

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 (55) hide show
  1. package/dist/cloud-report-upload.js +1 -10
  2. package/dist/execution-journal.js +3 -1
  3. package/dist/execution-protocol.js +1 -1
  4. package/dist/execution-runner.js +4 -1
  5. package/dist/local-execution-prompt.js +8 -3
  6. package/dist/local-executor.js +63 -39
  7. package/dist/local-memory/commit-telemetry.js +30 -0
  8. package/dist/local-memory/execution-workspace.js +62 -0
  9. package/dist/local-memory/full-index-search.js +47 -0
  10. package/dist/local-memory/index-budget.js +9 -3
  11. package/dist/local-memory/initialization-identity.js +36 -0
  12. package/dist/local-memory/inventory-cli.js +49 -8
  13. package/dist/local-memory/inventory-source.js +15 -8
  14. package/dist/local-memory/inventory-v2.js +361 -0
  15. package/dist/local-memory/legacy-adapter.js +233 -0
  16. package/dist/local-memory/migration-access.js +242 -0
  17. package/dist/local-memory/migration-cli.js +54 -0
  18. package/dist/local-memory/migration-io.js +199 -0
  19. package/dist/local-memory/migration-pending.js +9 -0
  20. package/dist/local-memory/migration-plan.js +317 -0
  21. package/dist/local-memory/migration-schema.js +55 -0
  22. package/dist/local-memory/migration-state-invariants.js +65 -0
  23. package/dist/local-memory/migration-store.js +414 -0
  24. package/dist/local-memory/migration-tool-entry.js +21 -0
  25. package/dist/local-memory/pending-query.js +5 -2
  26. package/dist/local-memory/pending-schema.js +1 -1
  27. package/dist/local-memory/pending.js +16 -2
  28. package/dist/local-memory/prompt.js +21 -5
  29. package/dist/local-memory/read-cli.js +98 -76
  30. package/dist/local-memory/rollout.js +79 -11
  31. package/dist/local-memory/runtime.js +181 -13
  32. package/dist/local-memory/storage-format.js +152 -0
  33. package/dist/local-memory/write.js +189 -21
  34. package/dist/local-memory/writer-mode.js +13 -0
  35. package/dist/local-memory-diagnostics.js +94 -13
  36. package/dist/local-memory-telemetry.js +19 -2
  37. package/dist/machine-info.js +3 -0
  38. package/dist/marketplace-protocol.js +1 -1
  39. package/dist/marketplace-remote.js +1 -1
  40. package/dist/marketplace-runner.js +1 -1
  41. package/dist/memory-migration-controller.js +86 -0
  42. package/dist/memory-migration-inspect.js +108 -0
  43. package/dist/memory-migration-journal.js +14 -0
  44. package/dist/memory-migration-protocol.js +14 -0
  45. package/dist/memory-migration-runtime.js +305 -0
  46. package/dist/memory-prune-diagnostics.js +60 -4
  47. package/dist/message-file-preview.js +11 -1
  48. package/dist/prompt.js +6 -2
  49. package/dist/runner.js +1 -1
  50. package/dist/runtimes/claude-thread-runner.js +25 -4
  51. package/dist/serve.js +38 -3
  52. package/dist/token.js +1 -0
  53. package/dist/workspace-control-handler.js +16 -3
  54. package/dist/workspace.js +13 -2
  55. package/package.json +1 -1
@@ -0,0 +1,152 @@
1
+ import { LOCAL_MEMORY_INDEX_CODE_POINT_CAP, LOCAL_MEMORY_INDEX_ENTRY_CAP, LOCAL_MEMORY_TOTAL_CODE_POINT_CAP, renderGeneratedIndex, renderLocalMemoryIndexCard, selectLocalMemoryIndexEntries, } from "./index-budget.js";
2
+ export const LOCAL_MEMORY_STORAGE_BYTE_CAP = 16 * 1024 * 1024;
3
+ export const LOCAL_MEMORY_STORAGE_ENTRY_CAP = 10_000;
4
+ export const LOCAL_MEMORY_NOTE_BYTE_CAP = 32 * 1024 * 1024;
5
+ export const LOCAL_MEMORY_READ_BYTE_CAP = 64 * 1024 * 1024;
6
+ export const LOCAL_MEMORY_GENERATED_INDEX_PATTERN = /<!--\s*NOWWORK:MEMORY_INDEX:BEGIN\s+schema=[12]\s*-->[\s\S]*?<!--\s*NOWWORK:MEMORY_INDEX:END\s*-->/u;
7
+ const length = (text) => {
8
+ let count = 0;
9
+ for (const _character of text)
10
+ count += 1;
11
+ return count;
12
+ };
13
+ const TYPES = new Set(["procedural", "semantic", "episodic", "preference"]);
14
+ const STATUSES = new Set(["candidate", "active", "pinned", "stale", "superseded", "archived"]);
15
+ const PRIORITIES = new Set(["high", "medium", "low"]);
16
+ const FIELDS = new Set(["id", "file", "type", "status", "summary", "triggers", "priority", "revision", "anchor", "updated", "review_after"]);
17
+ /** A malformed/unknown marker is never treated as an unmarked legacy file. */
18
+ export function detectLocalMemoryStorageVersion(memory) {
19
+ const markerCount = (memory.match(/NOWWORK:MEMORY_INDEX:/gu) ?? []).length;
20
+ if (markerCount === 0)
21
+ return 0;
22
+ const block = LOCAL_MEMORY_GENERATED_INDEX_PATTERN.exec(memory);
23
+ if (markerCount !== 2 || block?.index === undefined)
24
+ throw new Error("local_memory_storage_version_invalid");
25
+ const headings = [...memory.matchAll(/^##[\t ]+Knowledge Index[\t ]*\r?$/gmu)];
26
+ const heading = headings[0];
27
+ if (headings.length !== 1 || heading?.index === undefined)
28
+ throw new Error("local_memory_storage_index_invalid");
29
+ const sectionStart = heading.index + heading[0].length;
30
+ const nextSection = /^##[\t ]+(?!#)/mu.exec(memory.slice(sectionStart));
31
+ const sectionEnd = nextSection?.index === undefined ? memory.length : sectionStart + nextSection.index;
32
+ if (block.index < sectionStart || block.index + block[0].length > sectionEnd) {
33
+ throw new Error("local_memory_storage_index_invalid");
34
+ }
35
+ return /schema=2\s*-->/u.test(block[0].slice(0, block[0].indexOf("\n") + 1) || block[0]) ? 2 : 1;
36
+ }
37
+ function validateEntry(entry) {
38
+ const singleLine = (value) => !/[\r\n\u0000-\u001f\u007f]/u.test(value)
39
+ && !/<!--/u.test(value);
40
+ if (!entry.title.trim() || !entry.summary.trim()
41
+ || ![entry.title, entry.summary, entry.updated, ...entry.triggers].every(singleLine)
42
+ || !/^[A-Za-z0-9_-]+$/u.test(entry.id) || entry.anchor !== entry.id
43
+ || !/^notes\/(?:preferences|project-rules|testing|debugging|git|incidents|lessons)\.md$/u.test(entry.file)
44
+ || !TYPES.has(entry.type) || !STATUSES.has(entry.status) || !PRIORITIES.has(entry.priority)
45
+ || !Number.isSafeInteger(entry.revision) || entry.revision < 1
46
+ || !entry.updated.trim()
47
+ || (entry.reviewAfter !== null && !singleLine(entry.reviewAfter))) {
48
+ throw new Error("local_memory_storage_entry_invalid");
49
+ }
50
+ }
51
+ function validateEntries(entries) {
52
+ if (entries.length > LOCAL_MEMORY_STORAGE_ENTRY_CAP)
53
+ throw new Error("local_memory_storage_entry_limit");
54
+ const ids = new Set();
55
+ for (const entry of entries) {
56
+ validateEntry(entry);
57
+ if (ids.has(entry.id))
58
+ throw new Error("local_memory_storage_duplicate_id");
59
+ ids.add(entry.id);
60
+ }
61
+ }
62
+ export function assertLocalMemoryStorageSize(memory) {
63
+ if (Buffer.byteLength(memory, "utf8") > LOCAL_MEMORY_STORAGE_BYTE_CAP)
64
+ throw new Error("local_memory_storage_byte_limit");
65
+ }
66
+ /** schema=2 stores every lifecycle state, unlike the schema=1 prompt projection. */
67
+ export function parseFullMemoryIndex(memory) {
68
+ assertLocalMemoryStorageSize(memory);
69
+ if (detectLocalMemoryStorageVersion(memory) !== 2)
70
+ throw new Error("local_memory_storage_version_invalid");
71
+ const block = LOCAL_MEMORY_GENERATED_INDEX_PATTERN.exec(memory)[0];
72
+ const body = block.replace(/^<!--[^>]*-->\s*/u, "").replace(/<!--[^>]*-->$/u, "");
73
+ const headings = [...body.matchAll(/^###[\t ]+(.+?)[\t ]*\r?$/gmu)];
74
+ if (headings.length > LOCAL_MEMORY_STORAGE_ENTRY_CAP)
75
+ throw new Error("local_memory_storage_entry_limit");
76
+ if (body.slice(0, headings[0]?.index ?? body.length).trim())
77
+ throw new Error("local_memory_storage_entry_invalid");
78
+ const entries = headings.map((heading, index) => {
79
+ const fields = {};
80
+ const tail = body.slice(heading.index + heading[0].length, headings[index + 1]?.index ?? body.length);
81
+ for (const line of tail.split(/\r?\n/u)) {
82
+ if (!line.trim())
83
+ continue;
84
+ const field = /^-\s+([a-z][a-z_]*)\s*:\s*(.*?)\s*$/u.exec(line);
85
+ if (!field || !FIELDS.has(field[1]) || fields[field[1]] !== undefined)
86
+ throw new Error("local_memory_storage_entry_invalid");
87
+ fields[field[1]] = field[2];
88
+ }
89
+ if ([...FIELDS].filter(field => field !== "review_after").some(field => fields[field] === undefined)) {
90
+ throw new Error("local_memory_storage_entry_invalid");
91
+ }
92
+ if (!/^[1-9][0-9]*$/u.test(fields.revision))
93
+ throw new Error("local_memory_storage_entry_invalid");
94
+ return {
95
+ id: fields.id, title: heading[1], file: fields.file, type: fields.type,
96
+ status: fields.status, summary: fields.summary,
97
+ triggers: fields.triggers.split(/[,,、]/u).map(value => value.trim()).filter(Boolean),
98
+ priority: fields.priority, revision: Number(fields.revision),
99
+ anchor: fields.anchor, updated: fields.updated, reviewAfter: fields.review_after ?? null,
100
+ };
101
+ });
102
+ validateEntries(entries);
103
+ const section = memory.slice(memory.indexOf("## Knowledge Index") + "## Knowledge Index".length);
104
+ const remaining = section.replace(LOCAL_MEMORY_GENERATED_INDEX_PATTERN, "").split(/^##[\t ]+(?!#)/mu)[0] ?? "";
105
+ if (/^###[\t ]+/mu.test(remaining.replace(/<!--[\s\S]*?-->/gu, "")))
106
+ throw new Error("local_memory_legacy_index_requires_migration");
107
+ return entries;
108
+ }
109
+ export function renderFullMemoryIndex(entries) {
110
+ validateEntries(entries);
111
+ const block = `<!-- NOWWORK:MEMORY_INDEX:BEGIN schema=2 -->\n${entries.map(renderLocalMemoryIndexCard).join("\n")}<!-- NOWWORK:MEMORY_INDEX:END -->`;
112
+ assertLocalMemoryStorageSize(block);
113
+ return block;
114
+ }
115
+ export function replaceFullMemoryIndex(memory, entries) {
116
+ if (detectLocalMemoryStorageVersion(memory) !== 2)
117
+ throw new Error("local_memory_storage_version_invalid");
118
+ const updated = memory.replace(LOCAL_MEMORY_GENERATED_INDEX_PATTERN, () => renderFullMemoryIndex(entries));
119
+ assertLocalMemoryStorageSize(updated);
120
+ return updated;
121
+ }
122
+ export function selectLocalMemoryPreview(memory, query = "", options = {}) {
123
+ const entries = parseFullMemoryIndex(memory);
124
+ const totalActiveEntries = entries.filter(entry => entry.status === "active" || entry.status === "pinned").length;
125
+ const emptyIndex = renderGeneratedIndex([]);
126
+ // Reserve the largest count label before selecting; all discovery text counts toward the same cap.
127
+ const hint = (shown) => `\nMemory preview: ${shown}/${totalActiveEntries} active entries. Use crew-memory search --query <text> and crew-memory read --file <file> for more.\n`;
128
+ const base = memory.replace(LOCAL_MEMORY_GENERATED_INDEX_PATTERN, () => `${emptyIndex}${hint(totalActiveEntries)}`);
129
+ const remaining = Math.min(LOCAL_MEMORY_TOTAL_CODE_POINT_CAP, options.maxCodePoints ?? Infinity) - length(base);
130
+ const remainingBytes = (options.maxBytes ?? Infinity) - Buffer.byteLength(base, "utf8");
131
+ if (remaining < 0 || remainingBytes < 0 || Number.isNaN(remaining) || Number.isNaN(remainingBytes)) {
132
+ throw new Error("local_memory_identity_budget_exceeded");
133
+ }
134
+ const terms = query.normalize("NFKC").toLowerCase().split(/\s+/u).filter(Boolean);
135
+ const selection = selectLocalMemoryIndexEntries(entries, {
136
+ entryCap: LOCAL_MEMORY_INDEX_ENTRY_CAP,
137
+ indexCodePointCap: Math.min(LOCAL_MEMORY_INDEX_CODE_POINT_CAP - length(emptyIndex), remaining),
138
+ indexByteCap: remainingBytes,
139
+ }, {
140
+ protectedIds: new Set(entries.filter(entry => entry.status === "pinned").map(entry => entry.id)),
141
+ relevance: entry => {
142
+ const searchable = `${entry.title} ${entry.summary} ${entry.triggers.join(" ")}`.normalize("NFKC").toLowerCase();
143
+ return terms.filter(term => searchable.includes(term)).length;
144
+ },
145
+ });
146
+ if (selection.reason === "index_over_budget_pinned")
147
+ throw new Error("local_memory_pinned_budget_exceeded");
148
+ return {
149
+ ...selection, totalActiveEntries,
150
+ memory: memory.replace(LOCAL_MEMORY_GENERATED_INDEX_PATTERN, () => `${renderGeneratedIndex(selection.entries)}${hint(selection.entries.length)}`),
151
+ };
152
+ }
@@ -6,6 +6,8 @@ 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";
9
11
  const StatusChangeSchema = z.object({
10
12
  memoryId: LocalMemorySingleLineText,
11
13
  expectedRevision: z.number().int().positive(),
@@ -90,12 +92,14 @@ function parseCards(section) {
90
92
  });
91
93
  }
92
94
  export function parseLocalMemoryIndex(memory) {
95
+ if (detectLocalMemoryStorageVersion(memory) === 2)
96
+ return parseFullMemoryIndex(memory);
93
97
  const section = indexSection(memory);
94
98
  const generated = GENERATED_INDEX_PATTERN.exec(section)?.[0];
95
99
  return parseCards(generated ?? section);
96
100
  }
97
101
  function parseUnmarkedMemoryIndex(memory) {
98
- const section = indexSection(memory).replace(GENERATED_INDEX_PATTERN, "");
102
+ const section = indexSection(memory).replace(LOCAL_MEMORY_GENERATED_INDEX_PATTERN, "");
99
103
  return parseCards(section);
100
104
  }
101
105
  function replaceGeneratedIndex(memory, entries) {
@@ -122,11 +126,36 @@ function noteSection(id, revision, title, body) {
122
126
  }
123
127
  function notePattern(id) {
124
128
  const escaped = id.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
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");
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;
126
143
  }
127
144
  function replaceNote(content, id, replacement) {
128
145
  const pattern = notePattern(id);
129
- return pattern.test(content) ? content.replace(pattern, replacement) : null;
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);
130
159
  }
131
160
  function validateCandidate(result) {
132
161
  if (result.outcome === "no_change" && (result.changes.length > 0 || result.statusChanges.length > 0)) {
@@ -180,6 +209,25 @@ function stableId(executionId, index, change) {
180
209
  const input = [executionId, String(index), change.operation, change.title, change.summary, change.detailBody].join("\0");
181
210
  return `mem_${createHash("sha256").update(input, "utf8").digest("hex").slice(0, 12)}`;
182
211
  }
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
+ }
183
231
  function normalize(value) {
184
232
  return value.normalize("NFKC").toLocaleLowerCase().replace(/\s+/gu, " ").trim();
185
233
  }
@@ -199,14 +247,14 @@ function detailCategoryFromFile(file) {
199
247
  return DETAIL_CATEGORIES.has(category) ? category : null;
200
248
  }
201
249
  function readNoteBody(content, id) {
202
- const begin = content.indexOf(`<!-- NOWWORK:MEMORY_ENTRY:BEGIN id=${id}`);
203
- if (begin < 0)
250
+ const section = uniqueNoteSection(content, id);
251
+ if (section === null)
204
252
  return null;
205
- const bodyStart = content.indexOf("\n\n", begin);
206
- const bodyEnd = content.indexOf(`\n<!-- NOWWORK:MEMORY_ENTRY:END id=${id}`, bodyStart + 2);
253
+ const bodyStart = section.indexOf("\n\n");
254
+ const bodyEnd = section.indexOf(`\n<!-- NOWWORK:MEMORY_ENTRY:END id=${id}`, bodyStart + 2);
207
255
  if (bodyStart < 0 || bodyEnd < 0)
208
256
  return null;
209
- return content.slice(bodyStart + 2, bodyEnd).trim();
257
+ return section.slice(bodyStart + 2, bodyEnd).trim();
210
258
  }
211
259
  function archivedChange(entry, detailBody) {
212
260
  const detailCategory = detailCategoryFromFile(entry.file);
@@ -230,6 +278,34 @@ function archivedChange(entry, detailBody) {
230
278
  function emptyResult() {
231
279
  return { createdIds: [], updatedIds: [], statusChangedIds: [], deferredIds: [], pendingIds: [] };
232
280
  }
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
+ }
233
309
  function isWithin(root, candidate) {
234
310
  const path = relative(root, candidate);
235
311
  return path === "" || (path !== ".." && !path.startsWith(`..${sep}`));
@@ -244,12 +320,29 @@ async function assertWritablePaths(agentDir, memoryPath) {
244
320
  if (!memoryStat.isFile() || memoryStat.isSymbolicLink())
245
321
  throw new Error("local_memory_memory_path_invalid");
246
322
  }
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
+ }
247
335
  async function readNote(path) {
248
336
  try {
249
337
  const stat = await lstat(path);
250
338
  if (!stat.isFile() || stat.isSymbolicLink())
251
339
  throw new Error("local_memory_note_path_invalid");
252
- return await readFile(path, "utf8");
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;
253
346
  }
254
347
  catch (error) {
255
348
  if (error.code === "ENOENT")
@@ -269,6 +362,11 @@ async function atomicWrite(path, content) {
269
362
  }
270
363
  }
271
364
  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
+ }
272
370
  const empty = replaceGeneratedIndex(memory, []);
273
371
  if (codePointLength(empty) > LOCAL_MEMORY_TOTAL_CODE_POINT_CAP)
274
372
  throw new Error("local_memory_memory_over_budget");
@@ -306,12 +404,29 @@ async function withApplyLease(agentDir, operation) {
306
404
  }
307
405
  export async function applyLocalMemoryResult(input) {
308
406
  validateCandidate(input.result);
309
- if (input.result.outcome !== "proposed")
407
+ if (input.migrationDeferredPending)
408
+ return stageMigrationDeferredPending(input);
409
+ if (input.result.outcome !== "proposed" && input.requiredStorageVersion !== 2)
310
410
  return emptyResult();
311
411
  const memoryPath = join(input.agentDir, "MEMORY.md");
312
- await assertWritablePaths(input.agentDir, memoryPath);
313
- return withApplyLease(input.agentDir, async () => {
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");
314
420
  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();
315
430
  if (parseUnmarkedMemoryIndex(memory).length > 0)
316
431
  throw new Error("local_memory_legacy_index_requires_migration");
317
432
  let entries = [...parseLocalMemoryIndex(memory)];
@@ -342,6 +457,26 @@ export async function applyLocalMemoryResult(input) {
342
457
  const pendingCandidates = [];
343
458
  const resolvedPendingIds = new Set();
344
459
  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
+ };
345
480
  let allowActiveEviction = false;
346
481
  const randomId = input.randomId ?? randomUUID;
347
482
  const executionId = input.executionId ?? "unknown";
@@ -383,10 +518,21 @@ export async function applyLocalMemoryResult(input) {
383
518
  if (existingCreate !== undefined && !matchesCreateCandidate(existingCreate, change)) {
384
519
  throw new Error("local_memory_duplicate_candidate");
385
520
  }
386
- if (existingCreate === undefined && entries.some((entry) => normalize(entry.title) === normalize(change.title)
387
- || normalize(entry.summary) === normalize(change.summary))) {
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)))) {
388
523
  throw new Error("local_memory_duplicate_candidate");
389
524
  }
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
+ }
390
536
  }
391
537
  const revision = existingCreate?.revision ?? (current === undefined ? 1 : current.revision + 1);
392
538
  const entry = existingCreate ?? entryFromChange(change, id, revision, current?.status ?? "active");
@@ -400,7 +546,7 @@ export async function applyLocalMemoryResult(input) {
400
546
  : item),
401
547
  entry,
402
548
  ];
403
- const allowEvictionForChange = change.operation === "create" && change.priority === "high";
549
+ const allowEvictionForChange = !fullStorage && change.operation === "create" && change.priority === "high";
404
550
  const protectedIds = new Set(allowEvictionForChange ? pinnedIds(entries) : activeOrPinnedIds(entries));
405
551
  if (allowEvictionForChange)
406
552
  allowActiveEviction = true;
@@ -417,7 +563,7 @@ export async function applyLocalMemoryResult(input) {
417
563
  const notePath = resolve(input.agentDir, entry.file);
418
564
  if (!isWithin(resolve(input.agentDir, "notes"), notePath))
419
565
  throw new Error("local_memory_note_path_invalid");
420
- const previous = noteUpdates.get(notePath) ?? await readNote(notePath);
566
+ const previous = await readStoredNote(notePath);
421
567
  const replacement = noteSection(id, revision, change.title, change.detailBody);
422
568
  const existingSection = replaceNote(previous, id, replacement);
423
569
  const next = existingSection ?? (current === undefined
@@ -435,7 +581,7 @@ export async function applyLocalMemoryResult(input) {
435
581
  // Pending create candidates are durable overflow, not dead ends. Promote only
436
582
  // when the current catalog has room; supersede/update candidates stay pending
437
583
  // until a fresh revision-aware decision can safely replay them.
438
- const pendingCandidatesOnDisk = await readPendingMemoryCandidates({ agentDir: input.agentDir }).catch((error) => {
584
+ const pendingCandidatesOnDisk = fullStorage ? [] : await readPendingMemoryCandidates({ agentDir: input.agentDir }).catch((error) => {
439
585
  if (!(error instanceof Error) || error.message !== "memory_pending_directory_over_budget")
440
586
  throw error;
441
587
  // 自动提升是可选工作,目录查询预算不应阻断本轮已验证的更新或腾位操作。
@@ -446,6 +592,10 @@ export async function applyLocalMemoryResult(input) {
446
592
  return [];
447
593
  });
448
594
  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;
449
599
  const change = pending.candidate;
450
600
  if (change.operation !== "create")
451
601
  continue;
@@ -460,7 +610,7 @@ export async function applyLocalMemoryResult(input) {
460
610
  || normalize(entry.summary) === normalize(change.summary)));
461
611
  if (duplicate !== undefined) {
462
612
  const duplicatePath = resolve(input.agentDir, duplicate.file);
463
- const duplicateBody = readNoteBody(noteUpdates.get(duplicatePath) ?? await readNote(duplicatePath), duplicate.id);
613
+ const duplicateBody = readNoteBody(await readStoredNote(duplicatePath), duplicate.id);
464
614
  if (duplicateBody === change.detailBody
465
615
  || duplicateBody === unwrappedNoteBody(change.title, change.detailBody).trim()) {
466
616
  resolvedPendingIds.add(pending.pendingId);
@@ -478,7 +628,7 @@ export async function applyLocalMemoryResult(input) {
478
628
  const notePath = resolve(input.agentDir, entry.file);
479
629
  if (!isWithin(resolve(input.agentDir, "notes"), notePath))
480
630
  throw new Error("local_memory_note_path_invalid");
481
- const previous = noteUpdates.get(notePath) ?? await readNote(notePath);
631
+ const previous = await readStoredNote(notePath);
482
632
  const replacement = noteSection(id, 1, change.title, change.detailBody);
483
633
  const existingSection = replaceNote(previous, id, replacement);
484
634
  const next = existingSection ?? `${previous.trimEnd()}${previous.trimEnd() ? "\n\n" : ""}${replacement}`;
@@ -487,6 +637,17 @@ export async function applyLocalMemoryResult(input) {
487
637
  createdIds.push(id);
488
638
  resolvedPendingIds.add(pending.pendingId);
489
639
  }
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
+ }
490
651
  let finalProjection = projectIndex(memory, entries, allowActiveEviction ? pinnedIds(entries) : activeOrPinnedIds(entries));
491
652
  if (finalProjection.reason !== null
492
653
  && (!allowActiveEviction || finalProjection.reason === "index_over_budget_pinned")) {
@@ -498,7 +659,7 @@ export async function applyLocalMemoryResult(input) {
498
659
  .filter((entry) => entry?.status === "active");
499
660
  for (const entry of omittedActiveEntries) {
500
661
  const notePath = resolve(input.agentDir, entry.file);
501
- const content = noteUpdates.get(notePath) ?? await readNote(notePath);
662
+ const content = await readStoredNote(notePath);
502
663
  const detailBody = readNoteBody(content, entry.id);
503
664
  const change = detailBody === null ? null : archivedChange(entry, detailBody);
504
665
  if (change === null)
@@ -519,6 +680,13 @@ export async function applyLocalMemoryResult(input) {
519
680
  if (finalProjection.reason !== null)
520
681
  throw new Error(finalProjection.reason);
521
682
  }
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);
522
690
  for (const candidate of pendingCandidates)
523
691
  await persistPendingMemoryCandidate({ agentDir: input.agentDir, candidate });
524
692
  for (const [path, content] of noteUpdates)
@@ -535,5 +703,5 @@ export async function applyLocalMemoryResult(input) {
535
703
  deferredIds,
536
704
  pendingIds: deferredIds,
537
705
  };
538
- });
706
+ }));
539
707
  }
@@ -0,0 +1,13 @@
1
+ import { detectLocalMemoryStorageVersion } from "./storage-format.js";
2
+ /** Resolve format and rollout into one writer decision before a prune result is requested. */
3
+ export function resolveLocalMemoryWriterMode(workspace, rollout) {
4
+ if (rollout.legacyFallback)
5
+ return "legacy";
6
+ if (!rollout.enabled)
7
+ return rollout.deferred ? "blocked" : "legacy";
8
+ // Production workspaces always report this fact. Historical partial unit
9
+ // callers retain their already-resolved rollout witness.
10
+ if (workspace.localMemoryV2Initialized === undefined)
11
+ return "v2";
12
+ return detectLocalMemoryStorageVersion(workspace.memory) === 2 ? "v2" : "blocked";
13
+ }