@nowcrew/daemon 0.6.92 → 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 (42) 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/machine-info.js +3 -0
  29. package/dist/memory-migration-controller.js +84 -0
  30. package/dist/memory-migration-inspect.js +75 -0
  31. package/dist/memory-migration-journal.js +14 -0
  32. package/dist/memory-migration-protocol.js +14 -0
  33. package/dist/memory-migration-runtime.js +305 -0
  34. package/dist/memory-prune-diagnostics.js +60 -4
  35. package/dist/message-file-preview.js +11 -1
  36. package/dist/prompt.js +5 -1
  37. package/dist/runner.js +1 -1
  38. package/dist/serve.js +36 -3
  39. package/dist/token.js +1 -0
  40. package/dist/workspace-control-handler.js +16 -3
  41. package/dist/workspace.js +10 -2
  42. 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 { 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)) {
@@ -199,14 +228,14 @@ function detailCategoryFromFile(file) {
199
228
  return DETAIL_CATEGORIES.has(category) ? category : null;
200
229
  }
201
230
  function readNoteBody(content, id) {
202
- const begin = content.indexOf(`<!-- NOWWORK:MEMORY_ENTRY:BEGIN id=${id}`);
203
- if (begin < 0)
231
+ const section = uniqueNoteSection(content, id);
232
+ if (section === null)
204
233
  return null;
205
- const bodyStart = content.indexOf("\n\n", begin);
206
- const bodyEnd = content.indexOf(`\n<!-- NOWWORK:MEMORY_ENTRY:END id=${id}`, bodyStart + 2);
234
+ const bodyStart = section.indexOf("\n\n");
235
+ const bodyEnd = section.indexOf(`\n<!-- NOWWORK:MEMORY_ENTRY:END id=${id}`, bodyStart + 2);
207
236
  if (bodyStart < 0 || bodyEnd < 0)
208
237
  return null;
209
- return content.slice(bodyStart + 2, bodyEnd).trim();
238
+ return section.slice(bodyStart + 2, bodyEnd).trim();
210
239
  }
211
240
  function archivedChange(entry, detailBody) {
212
241
  const detailCategory = detailCategoryFromFile(entry.file);
@@ -244,12 +273,29 @@ async function assertWritablePaths(agentDir, memoryPath) {
244
273
  if (!memoryStat.isFile() || memoryStat.isSymbolicLink())
245
274
  throw new Error("local_memory_memory_path_invalid");
246
275
  }
276
+ async function assertMemoryAccessDirectory(agentDir) {
277
+ try {
278
+ const stat = await lstat(join(agentDir, ".crew/memory"));
279
+ if (!stat.isDirectory() || stat.isSymbolicLink())
280
+ throw new Error("local_memory_pending_path_invalid");
281
+ }
282
+ catch (error) {
283
+ if (error.code === "ENOENT")
284
+ return;
285
+ throw error;
286
+ }
287
+ }
247
288
  async function readNote(path) {
248
289
  try {
249
290
  const stat = await lstat(path);
250
291
  if (!stat.isFile() || stat.isSymbolicLink())
251
292
  throw new Error("local_memory_note_path_invalid");
252
- return await readFile(path, "utf8");
293
+ if (stat.size > LOCAL_MEMORY_NOTE_BYTE_CAP)
294
+ throw new Error("local_memory_note_byte_limit");
295
+ const content = await readFile(path, "utf8");
296
+ if (Buffer.byteLength(content, "utf8") > LOCAL_MEMORY_NOTE_BYTE_CAP)
297
+ throw new Error("local_memory_note_byte_limit");
298
+ return content;
253
299
  }
254
300
  catch (error) {
255
301
  if (error.code === "ENOENT")
@@ -269,6 +315,11 @@ async function atomicWrite(path, content) {
269
315
  }
270
316
  }
271
317
  function projectIndex(memory, entries, protectedIds) {
318
+ if (detectLocalMemoryStorageVersion(memory) === 2) {
319
+ const updated = replaceFullMemoryIndex(memory, entries);
320
+ selectLocalMemoryPreview(updated);
321
+ return { memory: updated, omittedIds: [], reason: null };
322
+ }
272
323
  const empty = replaceGeneratedIndex(memory, []);
273
324
  if (codePointLength(empty) > LOCAL_MEMORY_TOTAL_CODE_POINT_CAP)
274
325
  throw new Error("local_memory_memory_over_budget");
@@ -310,8 +361,15 @@ export async function applyLocalMemoryResult(input) {
310
361
  return emptyResult();
311
362
  const memoryPath = join(input.agentDir, "MEMORY.md");
312
363
  await assertWritablePaths(input.agentDir, memoryPath);
313
- return withApplyLease(input.agentDir, async () => {
364
+ await assertMemoryAccessDirectory(input.agentDir);
365
+ return withApplyLease(input.agentDir, async () => withMemoryAccess(input.agentDir, async () => {
366
+ await assertMemoryWriterAllowed(input.agentDir, input.writerCapabilities ?? []);
367
+ const memoryStat = await lstat(memoryPath);
368
+ if (memoryStat.size > 16 * 1024 * 1024)
369
+ throw new Error("local_memory_storage_byte_limit");
314
370
  const memory = await readFile(memoryPath, "utf8");
371
+ assertLocalMemoryStorageSize(memory);
372
+ const fullStorage = detectLocalMemoryStorageVersion(memory) === 2;
315
373
  if (parseUnmarkedMemoryIndex(memory).length > 0)
316
374
  throw new Error("local_memory_legacy_index_requires_migration");
317
375
  let entries = [...parseLocalMemoryIndex(memory)];
@@ -342,6 +400,26 @@ export async function applyLocalMemoryResult(input) {
342
400
  const pendingCandidates = [];
343
401
  const resolvedPendingIds = new Set();
344
402
  const noteUpdates = new Map();
403
+ const noteCache = new Map();
404
+ let totalReadBytes = Buffer.byteLength(memory, "utf8");
405
+ const readStoredNote = async (path) => {
406
+ const cached = noteUpdates.get(path) ?? noteCache.get(path);
407
+ if (cached !== undefined)
408
+ return cached;
409
+ const stat = await lstat(path).catch((error) => {
410
+ if (error.code === "ENOENT")
411
+ return null;
412
+ throw error;
413
+ });
414
+ if (stat !== null && totalReadBytes + stat.size > LOCAL_MEMORY_READ_BYTE_CAP)
415
+ throw new Error("local_memory_read_byte_limit");
416
+ const content = await readNote(path);
417
+ totalReadBytes += Buffer.byteLength(content, "utf8");
418
+ if (totalReadBytes > LOCAL_MEMORY_READ_BYTE_CAP)
419
+ throw new Error("local_memory_read_byte_limit");
420
+ noteCache.set(path, content);
421
+ return content;
422
+ };
345
423
  let allowActiveEviction = false;
346
424
  const randomId = input.randomId ?? randomUUID;
347
425
  const executionId = input.executionId ?? "unknown";
@@ -383,10 +461,21 @@ export async function applyLocalMemoryResult(input) {
383
461
  if (existingCreate !== undefined && !matchesCreateCandidate(existingCreate, change)) {
384
462
  throw new Error("local_memory_duplicate_candidate");
385
463
  }
386
- if (existingCreate === undefined && entries.some((entry) => normalize(entry.title) === normalize(change.title)
387
- || normalize(entry.summary) === normalize(change.summary))) {
464
+ if (existingCreate === undefined && entries.some((entry) => (!fullStorage || entry.status === "active" || entry.status === "pinned")
465
+ && (normalize(entry.title) === normalize(change.title) || normalize(entry.summary) === normalize(change.summary)))) {
388
466
  throw new Error("local_memory_duplicate_candidate");
389
467
  }
468
+ if (existingCreate !== undefined) {
469
+ if (existingCreate.revision !== 1 || existingCreate.status !== "active") {
470
+ throw new Error("local_memory_create_replay_conflict");
471
+ }
472
+ const stored = await readStoredNote(resolve(input.agentDir, existingCreate.file));
473
+ if (uniqueNoteSection(stored, id)?.trimEnd() !== noteSection(id, 1, change.title, change.detailBody).trimEnd()) {
474
+ throw new Error("local_memory_create_replay_conflict");
475
+ }
476
+ // A proven duplicate is a no-op; it must never rewrite a newer body or status.
477
+ continue;
478
+ }
390
479
  }
391
480
  const revision = existingCreate?.revision ?? (current === undefined ? 1 : current.revision + 1);
392
481
  const entry = existingCreate ?? entryFromChange(change, id, revision, current?.status ?? "active");
@@ -400,7 +489,7 @@ export async function applyLocalMemoryResult(input) {
400
489
  : item),
401
490
  entry,
402
491
  ];
403
- const allowEvictionForChange = change.operation === "create" && change.priority === "high";
492
+ const allowEvictionForChange = !fullStorage && change.operation === "create" && change.priority === "high";
404
493
  const protectedIds = new Set(allowEvictionForChange ? pinnedIds(entries) : activeOrPinnedIds(entries));
405
494
  if (allowEvictionForChange)
406
495
  allowActiveEviction = true;
@@ -417,7 +506,7 @@ export async function applyLocalMemoryResult(input) {
417
506
  const notePath = resolve(input.agentDir, entry.file);
418
507
  if (!isWithin(resolve(input.agentDir, "notes"), notePath))
419
508
  throw new Error("local_memory_note_path_invalid");
420
- const previous = noteUpdates.get(notePath) ?? await readNote(notePath);
509
+ const previous = await readStoredNote(notePath);
421
510
  const replacement = noteSection(id, revision, change.title, change.detailBody);
422
511
  const existingSection = replaceNote(previous, id, replacement);
423
512
  const next = existingSection ?? (current === undefined
@@ -435,7 +524,7 @@ export async function applyLocalMemoryResult(input) {
435
524
  // Pending create candidates are durable overflow, not dead ends. Promote only
436
525
  // when the current catalog has room; supersede/update candidates stay pending
437
526
  // until a fresh revision-aware decision can safely replay them.
438
- const pendingCandidatesOnDisk = await readPendingMemoryCandidates({ agentDir: input.agentDir }).catch((error) => {
527
+ const pendingCandidatesOnDisk = fullStorage ? [] : await readPendingMemoryCandidates({ agentDir: input.agentDir }).catch((error) => {
439
528
  if (!(error instanceof Error) || error.message !== "memory_pending_directory_over_budget")
440
529
  throw error;
441
530
  // 自动提升是可选工作,目录查询预算不应阻断本轮已验证的更新或腾位操作。
@@ -460,7 +549,7 @@ export async function applyLocalMemoryResult(input) {
460
549
  || normalize(entry.summary) === normalize(change.summary)));
461
550
  if (duplicate !== undefined) {
462
551
  const duplicatePath = resolve(input.agentDir, duplicate.file);
463
- const duplicateBody = readNoteBody(noteUpdates.get(duplicatePath) ?? await readNote(duplicatePath), duplicate.id);
552
+ const duplicateBody = readNoteBody(await readStoredNote(duplicatePath), duplicate.id);
464
553
  if (duplicateBody === change.detailBody
465
554
  || duplicateBody === unwrappedNoteBody(change.title, change.detailBody).trim()) {
466
555
  resolvedPendingIds.add(pending.pendingId);
@@ -478,7 +567,7 @@ export async function applyLocalMemoryResult(input) {
478
567
  const notePath = resolve(input.agentDir, entry.file);
479
568
  if (!isWithin(resolve(input.agentDir, "notes"), notePath))
480
569
  throw new Error("local_memory_note_path_invalid");
481
- const previous = noteUpdates.get(notePath) ?? await readNote(notePath);
570
+ const previous = await readStoredNote(notePath);
482
571
  const replacement = noteSection(id, 1, change.title, change.detailBody);
483
572
  const existingSection = replaceNote(previous, id, replacement);
484
573
  const next = existingSection ?? `${previous.trimEnd()}${previous.trimEnd() ? "\n\n" : ""}${replacement}`;
@@ -487,6 +576,17 @@ export async function applyLocalMemoryResult(input) {
487
576
  createdIds.push(id);
488
577
  resolvedPendingIds.add(pending.pendingId);
489
578
  }
579
+ if (fullStorage) {
580
+ const changedMetadataIds = new Set([
581
+ ...statusChangedIds,
582
+ ...input.result.changes.flatMap(change => change.supersedesMemoryId === null ? [] : [change.supersedesMemoryId]),
583
+ ]);
584
+ for (const id of changedMetadataIds) {
585
+ const entry = entries.find(item => item.id === id);
586
+ const notePath = resolve(input.agentDir, entry.file);
587
+ noteUpdates.set(notePath, updateNoteRevision(await readStoredNote(notePath), id, entry.revision));
588
+ }
589
+ }
490
590
  let finalProjection = projectIndex(memory, entries, allowActiveEviction ? pinnedIds(entries) : activeOrPinnedIds(entries));
491
591
  if (finalProjection.reason !== null
492
592
  && (!allowActiveEviction || finalProjection.reason === "index_over_budget_pinned")) {
@@ -498,7 +598,7 @@ export async function applyLocalMemoryResult(input) {
498
598
  .filter((entry) => entry?.status === "active");
499
599
  for (const entry of omittedActiveEntries) {
500
600
  const notePath = resolve(input.agentDir, entry.file);
501
- const content = noteUpdates.get(notePath) ?? await readNote(notePath);
601
+ const content = await readStoredNote(notePath);
502
602
  const detailBody = readNoteBody(content, entry.id);
503
603
  const change = detailBody === null ? null : archivedChange(entry, detailBody);
504
604
  if (change === null)
@@ -519,6 +619,12 @@ export async function applyLocalMemoryResult(input) {
519
619
  if (finalProjection.reason !== null)
520
620
  throw new Error(finalProjection.reason);
521
621
  }
622
+ // Complete every resource check before the first persistent write.
623
+ assertLocalMemoryStorageSize(finalProjection.memory);
624
+ for (const content of noteUpdates.values()) {
625
+ if (Buffer.byteLength(content, "utf8") > LOCAL_MEMORY_NOTE_BYTE_CAP)
626
+ throw new Error("local_memory_note_byte_limit");
627
+ }
522
628
  for (const candidate of pendingCandidates)
523
629
  await persistPendingMemoryCandidate({ agentDir: input.agentDir, candidate });
524
630
  for (const [path, content] of noteUpdates)
@@ -535,5 +641,5 @@ export async function applyLocalMemoryResult(input) {
535
641
  deferredIds,
536
642
  pendingIds: deferredIds,
537
643
  };
538
- });
644
+ }));
539
645
  }
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { readFile, stat } from "node:fs/promises";
2
+ import { lstat, readFile, readdir, stat } from "node:fs/promises";
3
3
  import { isAbsolute, join, normalize, relative, resolve } from "node:path";
4
4
  import { parseArgs } from "node:util";
5
5
  import { capMemoryForInject, MEMORY_INJECT_CAP } from "./prompt.js";
@@ -39,13 +39,52 @@ async function inspectFile(path, signal) {
39
39
  return { ...base, error_code: errorCode(error) };
40
40
  }
41
41
  }
42
+ async function inspectPendingDirectory(path, signal) {
43
+ let metadata;
44
+ try {
45
+ metadata = await lstat(path);
46
+ }
47
+ catch (error) {
48
+ const code = errorCode(error);
49
+ return code === "ENOENT" ? { exists: false, entry_count: 0 } : { exists: false, error_code: code };
50
+ }
51
+ const base = { exists: true, mtime_ms: metadata.mtimeMs };
52
+ if (metadata.isSymbolicLink() || !metadata.isDirectory())
53
+ return { ...base, hash_skipped_reason: "not_regular_file" };
54
+ try {
55
+ const names = (await readdir(path, { withFileTypes: true }))
56
+ .filter(entry => entry.name.endsWith(".json"))
57
+ .sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
58
+ if (names.length > 128)
59
+ return { ...base, entry_count: names.length, error_code: "entry_limit" };
60
+ const hash = createHash("sha256");
61
+ let size = 0;
62
+ for (const entry of names) {
63
+ if (signal.aborted)
64
+ throw signal.reason;
65
+ if (!entry.isFile() || entry.isSymbolicLink())
66
+ return { ...base, entry_count: names.length, error_code: "invalid_entry" };
67
+ const fact = await inspectFile(join(path, entry.name), signal);
68
+ if (!fact.exists || fact.sha256 === undefined || fact.error_code !== undefined
69
+ || fact.hash_skipped_reason !== undefined)
70
+ return { ...base, entry_count: names.length, error_code: "incomplete_entry" };
71
+ size += fact.size_bytes ?? 0;
72
+ hash.update(entry.name).update("\0").update(fact.sha256).update("\0");
73
+ }
74
+ return { ...base, size_bytes: size, entry_count: names.length, sha256: hash.digest("hex") };
75
+ }
76
+ catch (error) {
77
+ return { ...base, error_code: errorCode(error) };
78
+ }
79
+ }
42
80
  export async function inspectLocalMemoryFilesWithinDeadline(homeDir, timeoutMs) {
43
81
  const controller = new AbortController();
44
82
  let timer;
45
83
  const inspection = Promise.all([
46
84
  inspectFile(join(homeDir, "MEMORY.md"), controller.signal),
47
85
  inspectFile(join(homeDir, "notes", "lessons.md"), controller.signal),
48
- ]).then(([memory, lessons]) => ({ memory, lessons }));
86
+ inspectPendingDirectory(join(homeDir, ".crew", "memory", "pending"), controller.signal),
87
+ ]).then(([memory, lessons, pending]) => ({ memory, lessons, pending }));
49
88
  const timeout = new Promise((_resolve, reject) => {
50
89
  timer = setTimeout(() => {
51
90
  controller.abort();
@@ -38,6 +38,7 @@ export function logLocalMemoryDiagnosticsFailure(identity, phase, error) {
38
38
  function snapshotFields(snapshot) {
39
39
  if (snapshot === null)
40
40
  return {};
41
+ const pending = snapshot.pending ?? { exists: false, entry_count: 0 };
41
42
  return {
42
43
  memory_exists: snapshot.memory.exists,
43
44
  memory_size_bytes: snapshot.memory.size_bytes,
@@ -54,6 +55,12 @@ function snapshotFields(snapshot) {
54
55
  lessons_hash_skipped_reason: snapshot.lessons.hash_skipped_reason,
55
56
  lessons_error_code: snapshot.lessons.error_code,
56
57
  lessons_markdown_heading_count: snapshot.lessons.markdown_heading_count,
58
+ pending_exists: pending.exists,
59
+ pending_size_bytes: pending.size_bytes,
60
+ pending_mtime_ms: pending.mtime_ms,
61
+ pending_sha256: pending.sha256,
62
+ pending_error_code: pending.error_code,
63
+ pending_entry_count: pending.entry_count,
57
64
  };
58
65
  }
59
66
  function promptDelivery(runtime, stableProtocolRuntime) {
@@ -183,10 +190,13 @@ export function createLocalMemoryTelemetry(options) {
183
190
  const lessonsChanged = beforeSnapshot !== null && afterSnapshot !== null
184
191
  ? localMemoryFileChanged(beforeSnapshot.lessons, afterSnapshot.lessons)
185
192
  : undefined;
193
+ const pendingChanged = beforeSnapshot !== null && afterSnapshot !== null
194
+ ? localMemoryFileChanged(beforeSnapshot.pending ?? { exists: false }, afterSnapshot.pending ?? { exists: false })
195
+ : undefined;
186
196
  const accessSummary = accessObserver.summary();
187
- const sharedFilesChanged = memoryChanged === undefined || lessonsChanged === undefined
197
+ const sharedFilesChanged = memoryChanged === undefined || lessonsChanged === undefined || pendingChanged === undefined
188
198
  ? undefined
189
- : memoryChanged || lessonsChanged;
199
+ : memoryChanged || lessonsChanged || pendingChanged;
190
200
  safeLog("local_memory.execution_observed", "本地记忆执行观察已完成", {
191
201
  execution_id: options.executionId,
192
202
  agent_handle: options.agentHandle,
@@ -200,6 +210,7 @@ export function createLocalMemoryTelemetry(options) {
200
210
  executor_outcome: executorCompleted ? "succeeded" : "failed",
201
211
  memory_changed: memoryChanged,
202
212
  lessons_changed: lessonsChanged,
213
+ pending_changed: pendingChanged,
203
214
  shared_files_changed_during_execution: sharedFilesChanged,
204
215
  unexpected_shared_write: options.isMemoryPrune
205
216
  ? undefined
@@ -224,6 +235,12 @@ export function createLocalMemoryTelemetry(options) {
224
235
  lessons_after_hash_skipped_reason: afterSnapshot.lessons.hash_skipped_reason,
225
236
  lessons_after_error_code: afterSnapshot.lessons.error_code,
226
237
  lessons_after_markdown_heading_count: afterSnapshot.lessons.markdown_heading_count,
238
+ pending_after_exists: afterSnapshot.pending?.exists ?? false,
239
+ pending_after_size_bytes: afterSnapshot.pending?.size_bytes,
240
+ pending_after_mtime_ms: afterSnapshot.pending?.mtime_ms,
241
+ pending_after_sha256: afterSnapshot.pending?.sha256,
242
+ pending_after_error_code: afterSnapshot.pending?.error_code,
243
+ pending_after_entry_count: afterSnapshot.pending?.entry_count ?? 0,
227
244
  }),
228
245
  });
229
246
  },
@@ -25,6 +25,7 @@ import { RUNTIME_HEALTH_PROBE_CAPABILITY } from "./runtime-health.js";
25
25
  import { probeDeepSeekHarnessAcp, deepSeekHarnessLauncher } from "./runtimes/deepseek-harness.js";
26
26
  import { daemonHome } from "./computer-profile.js";
27
27
  import { daemonReleaseProvenance } from "./release-provenance.js";
28
+ import { MEMORY_MIGRATION_CAPABILITY, MEMORY_STORAGE_V2_CAPABILITY, MEMORY_STATE_GUARD_CAPABILITY } from "./memory-migration-protocol.js";
28
29
  export { executableRuntimes } from "./runtime-capabilities.js";
29
30
  const execFileP = promisify(execFile);
30
31
  export const PROJECT_WORKSPACES_CAPABILITY = "project_workspaces_v1";
@@ -39,6 +40,7 @@ export const NATIVE_PROJECT_WORKSPACE_READINESS = Object.freeze({
39
40
  });
40
41
  export const DAEMON_CAPABILITIES = Object.freeze([
41
42
  MARKETPLACE_CAPABILITY,
43
+ MEMORY_MIGRATION_CAPABILITY, MEMORY_STORAGE_V2_CAPABILITY, MEMORY_STATE_GUARD_CAPABILITY,
42
44
  "scheduled_job_v1",
43
45
  "reply_origin_v1",
44
46
  "origin_decision_v1",
@@ -73,6 +75,7 @@ export const daemonCapabilities = (runtimePlatform = process.platform, readiness
73
75
  return Object.freeze(DAEMON_CAPABILITIES.filter((capability) => ((capability !== MARKETPLACE_CAPABILITY || nativeWorkspacePlatform)
74
76
  && (capability !== PROJECT_WORKSPACES_CAPABILITY || nativeWorkspaceReady)
75
77
  && (capability !== "message_file_preview_v1" || nativeWorkspacePlatform)
78
+ && (nativeWorkspacePlatform || ![MEMORY_MIGRATION_CAPABILITY, MEMORY_STORAGE_V2_CAPABILITY, MEMORY_STATE_GUARD_CAPABILITY].includes(capability))
76
79
  && (nativeWorkspacePlatform
77
80
  || (capability !== "project_skills_v1"
78
81
  && capability !== "project_skill_projection_v2"