@nowcrew/daemon 0.6.100 → 0.6.101-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 (59) hide show
  1. package/dist/cloud-report-upload.js +1 -10
  2. package/dist/computer-main.js +0 -0
  3. package/dist/execution-journal.js +3 -1
  4. package/dist/execution-protocol.js +1 -1
  5. package/dist/execution-runner.js +4 -1
  6. package/dist/local-execution-prompt.js +8 -3
  7. package/dist/local-executor.js +78 -55
  8. package/dist/local-memory/commit-telemetry.js +30 -0
  9. package/dist/local-memory/execution-workspace.js +62 -0
  10. package/dist/local-memory/full-index-search.js +47 -0
  11. package/dist/local-memory/index-budget.js +9 -3
  12. package/dist/local-memory/initialization-identity.js +36 -0
  13. package/dist/local-memory/inventory-cli.js +52 -8
  14. package/dist/local-memory/inventory-source.js +15 -8
  15. package/dist/local-memory/inventory-v2.js +362 -0
  16. package/dist/local-memory/legacy-adapter.js +233 -0
  17. package/dist/local-memory/migration-access.js +242 -0
  18. package/dist/local-memory/migration-cli.js +54 -0
  19. package/dist/local-memory/migration-io.js +199 -0
  20. package/dist/local-memory/migration-pending.js +9 -0
  21. package/dist/local-memory/migration-plan.js +432 -0
  22. package/dist/local-memory/migration-schema.js +55 -0
  23. package/dist/local-memory/migration-state-invariants.js +65 -0
  24. package/dist/local-memory/migration-store.js +414 -0
  25. package/dist/local-memory/migration-tool-entry.js +21 -0
  26. package/dist/local-memory/pending-query.js +5 -2
  27. package/dist/local-memory/pending-schema.js +1 -1
  28. package/dist/local-memory/pending.js +16 -2
  29. package/dist/local-memory/prompt.js +21 -5
  30. package/dist/local-memory/read-cli.js +98 -76
  31. package/dist/local-memory/release-artifact-guard.js +55 -0
  32. package/dist/local-memory/rollout.js +79 -11
  33. package/dist/local-memory/runtime.js +191 -17
  34. package/dist/local-memory/source-path.js +13 -0
  35. package/dist/local-memory/storage-format.js +249 -0
  36. package/dist/local-memory/write.js +190 -21
  37. package/dist/local-memory/writer-mode.js +13 -0
  38. package/dist/local-memory-diagnostics.js +96 -15
  39. package/dist/local-memory-telemetry.js +20 -3
  40. package/dist/machine-info.js +3 -0
  41. package/dist/main.js +0 -0
  42. package/dist/marketplace-protocol.js +1 -1
  43. package/dist/marketplace-remote.js +1 -1
  44. package/dist/marketplace-runner.js +1 -1
  45. package/dist/memory-migration-controller.js +89 -0
  46. package/dist/memory-migration-inspect.js +116 -0
  47. package/dist/memory-migration-journal.js +14 -0
  48. package/dist/memory-migration-protocol.js +14 -0
  49. package/dist/memory-migration-runtime.js +305 -0
  50. package/dist/memory-prune-diagnostics.js +74 -4
  51. package/dist/message-file-preview.js +11 -1
  52. package/dist/prompt.js +9 -17
  53. package/dist/runner.js +1 -1
  54. package/dist/runtimes/claude-thread-runner.js +25 -4
  55. package/dist/serve.js +38 -3
  56. package/dist/token.js +1 -0
  57. package/dist/workspace-control-handler.js +16 -3
  58. package/dist/workspace.js +13 -2
  59. package/package.json +9 -10
@@ -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(),
@@ -20,6 +22,7 @@ const ResultSchema = z.object({
20
22
  outcome: z.enum(["no_change", "proposed", "conflict", "failed"]),
21
23
  changes: z.array(LocalMemoryChangeSchema).max(50),
22
24
  statusChanges: z.array(StatusChangeSchema).max(50),
25
+ workLogDisposition: z.enum(["clear", "preserve"]).optional(),
23
26
  }).strict();
24
27
  const GENERATED_INDEX_PATTERN = /<!--\s*NOWWORK:MEMORY_INDEX:BEGIN\s+schema=1\s*-->[\s\S]*?<!--\s*NOWWORK:MEMORY_INDEX:END\s*-->/u;
25
28
  const INDEX_HEADING = /^##\s+Knowledge Index\s*$/mu;
@@ -90,12 +93,14 @@ function parseCards(section) {
90
93
  });
91
94
  }
92
95
  export function parseLocalMemoryIndex(memory) {
96
+ if (detectLocalMemoryStorageVersion(memory) === 2)
97
+ return parseFullMemoryIndex(memory);
93
98
  const section = indexSection(memory);
94
99
  const generated = GENERATED_INDEX_PATTERN.exec(section)?.[0];
95
100
  return parseCards(generated ?? section);
96
101
  }
97
102
  function parseUnmarkedMemoryIndex(memory) {
98
- const section = indexSection(memory).replace(GENERATED_INDEX_PATTERN, "");
103
+ const section = indexSection(memory).replace(LOCAL_MEMORY_GENERATED_INDEX_PATTERN, "");
99
104
  return parseCards(section);
100
105
  }
101
106
  function replaceGeneratedIndex(memory, entries) {
@@ -122,11 +127,36 @@ function noteSection(id, revision, title, body) {
122
127
  }
123
128
  function notePattern(id) {
124
129
  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");
130
+ 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");
131
+ }
132
+ function uniqueNoteSection(content, id) {
133
+ const escaped = id.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
134
+ const begins = [...content.matchAll(new RegExp(`<!--\\s*NOWWORK:MEMORY_ENTRY:BEGIN\\s+id=${escaped}(?=\\s|-->)`, "gu"))];
135
+ const ends = [...content.matchAll(new RegExp(`<!--\\s*NOWWORK:MEMORY_ENTRY:END\\s+id=${escaped}\\s*-->`, "gu"))];
136
+ if (begins.length === 0 && ends.length === 0)
137
+ return null;
138
+ const section = notePattern(id).exec(content)?.[0];
139
+ if (begins.length !== 1 || ends.length !== 1 || section === undefined
140
+ || (section.match(/NOWWORK:MEMORY_ENTRY:(?:BEGIN|END)/gu) ?? []).length !== 2) {
141
+ throw new Error("local_memory_note_anchor_invalid");
142
+ }
143
+ return section;
126
144
  }
127
145
  function replaceNote(content, id, replacement) {
128
146
  const pattern = notePattern(id);
129
- return pattern.test(content) ? content.replace(pattern, replacement) : null;
147
+ return uniqueNoteSection(content, id) !== null ? content.replace(pattern, () => replacement) : null;
148
+ }
149
+ function updateNoteRevision(content, id, revision) {
150
+ const section = uniqueNoteSection(content, id);
151
+ const beginning = `<!-- NOWWORK:MEMORY_ENTRY:BEGIN id=${id} `;
152
+ const ending = `<!-- NOWWORK:MEMORY_ENTRY:END id=${id} -->`;
153
+ if (section === null || content.split(beginning).length !== 2 || content.split(ending).length !== 2) {
154
+ throw new Error("local_memory_note_anchor_missing");
155
+ }
156
+ 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}`);
157
+ if (!updated.startsWith(`${beginning}revision=${revision} -->`))
158
+ throw new Error("local_memory_note_anchor_missing");
159
+ return content.replace(notePattern(id), () => updated);
130
160
  }
131
161
  function validateCandidate(result) {
132
162
  if (result.outcome === "no_change" && (result.changes.length > 0 || result.statusChanges.length > 0)) {
@@ -180,6 +210,25 @@ function stableId(executionId, index, change) {
180
210
  const input = [executionId, String(index), change.operation, change.title, change.summary, change.detailBody].join("\0");
181
211
  return `mem_${createHash("sha256").update(input, "utf8").digest("hex").slice(0, 12)}`;
182
212
  }
213
+ function migrationPendingId(change) {
214
+ // Keep the digest independent of object insertion order for callers that
215
+ // reach applyLocalMemoryResult directly instead of the JSON/Zod boundary.
216
+ const canonicalChange = {
217
+ operation: change.operation,
218
+ memoryId: change.memoryId,
219
+ baseRevision: change.baseRevision,
220
+ supersedesMemoryId: change.supersedesMemoryId,
221
+ title: change.title,
222
+ kind: change.kind,
223
+ summary: change.summary,
224
+ triggers: change.triggers,
225
+ priority: change.priority,
226
+ detailCategory: change.detailCategory,
227
+ detailBody: change.detailBody,
228
+ reviewAfter: change.reviewAfter,
229
+ };
230
+ return `pending-migration-${createHash("sha256").update(JSON.stringify(canonicalChange), "utf8").digest("hex").slice(0, 12)}`;
231
+ }
183
232
  function normalize(value) {
184
233
  return value.normalize("NFKC").toLocaleLowerCase().replace(/\s+/gu, " ").trim();
185
234
  }
@@ -199,14 +248,14 @@ function detailCategoryFromFile(file) {
199
248
  return DETAIL_CATEGORIES.has(category) ? category : null;
200
249
  }
201
250
  function readNoteBody(content, id) {
202
- const begin = content.indexOf(`<!-- NOWWORK:MEMORY_ENTRY:BEGIN id=${id}`);
203
- if (begin < 0)
251
+ const section = uniqueNoteSection(content, id);
252
+ if (section === null)
204
253
  return null;
205
- const bodyStart = content.indexOf("\n\n", begin);
206
- const bodyEnd = content.indexOf(`\n<!-- NOWWORK:MEMORY_ENTRY:END id=${id}`, bodyStart + 2);
254
+ const bodyStart = section.indexOf("\n\n");
255
+ const bodyEnd = section.indexOf(`\n<!-- NOWWORK:MEMORY_ENTRY:END id=${id}`, bodyStart + 2);
207
256
  if (bodyStart < 0 || bodyEnd < 0)
208
257
  return null;
209
- return content.slice(bodyStart + 2, bodyEnd).trim();
258
+ return section.slice(bodyStart + 2, bodyEnd).trim();
210
259
  }
211
260
  function archivedChange(entry, detailBody) {
212
261
  const detailCategory = detailCategoryFromFile(entry.file);
@@ -230,6 +279,34 @@ function archivedChange(entry, detailBody) {
230
279
  function emptyResult() {
231
280
  return { createdIds: [], updatedIds: [], statusChangedIds: [], deferredIds: [], pendingIds: [] };
232
281
  }
282
+ async function stageMigrationDeferredPending(input) {
283
+ if (input.result.outcome !== "proposed")
284
+ return emptyResult();
285
+ if (input.result.statusChanges.length > 0 || input.result.changes.some((change) => change.operation !== "create")) {
286
+ throw new Error("local_memory_migration_pending_create_only");
287
+ }
288
+ const executionId = input.executionId ?? "unknown";
289
+ const pending = input.result.changes.map((change) => ({
290
+ schemaVersion: 1,
291
+ pendingId: migrationPendingId(change),
292
+ createdAt: new Date().toISOString(),
293
+ reason: "legacy_requires_migration",
294
+ source: { executionId, pruneTraceId: input.pruneTraceId ?? null },
295
+ candidate: change,
296
+ }));
297
+ await assertMemoryAccessDirectory(input.agentDir);
298
+ await withMemoryAccess(input.agentDir, async () => {
299
+ const memory = await readFile(join(input.agentDir, "MEMORY.md"), "utf8").catch(() => null);
300
+ if (memory === null || detectLocalMemoryStorageVersion(memory) !== 1) {
301
+ throw new Error("local_memory_migration_pending_storage_invalid");
302
+ }
303
+ for (const candidate of pending) {
304
+ await persistPendingMemoryCandidate({ agentDir: input.agentDir, candidate });
305
+ }
306
+ }, { pendingOnly: true });
307
+ const pendingIds = [...new Set(pending.map((candidate) => candidate.pendingId))];
308
+ return { createdIds: [], updatedIds: [], statusChangedIds: [], deferredIds: pendingIds, pendingIds };
309
+ }
233
310
  function isWithin(root, candidate) {
234
311
  const path = relative(root, candidate);
235
312
  return path === "" || (path !== ".." && !path.startsWith(`..${sep}`));
@@ -244,12 +321,29 @@ async function assertWritablePaths(agentDir, memoryPath) {
244
321
  if (!memoryStat.isFile() || memoryStat.isSymbolicLink())
245
322
  throw new Error("local_memory_memory_path_invalid");
246
323
  }
324
+ async function assertMemoryAccessDirectory(agentDir) {
325
+ try {
326
+ const stat = await lstat(join(agentDir, ".crew/memory"));
327
+ if (!stat.isDirectory() || stat.isSymbolicLink())
328
+ throw new Error("local_memory_pending_path_invalid");
329
+ }
330
+ catch (error) {
331
+ if (error.code === "ENOENT")
332
+ return;
333
+ throw error;
334
+ }
335
+ }
247
336
  async function readNote(path) {
248
337
  try {
249
338
  const stat = await lstat(path);
250
339
  if (!stat.isFile() || stat.isSymbolicLink())
251
340
  throw new Error("local_memory_note_path_invalid");
252
- return await readFile(path, "utf8");
341
+ if (stat.size > LOCAL_MEMORY_NOTE_BYTE_CAP)
342
+ throw new Error("local_memory_note_byte_limit");
343
+ const content = await readFile(path, "utf8");
344
+ if (Buffer.byteLength(content, "utf8") > LOCAL_MEMORY_NOTE_BYTE_CAP)
345
+ throw new Error("local_memory_note_byte_limit");
346
+ return content;
253
347
  }
254
348
  catch (error) {
255
349
  if (error.code === "ENOENT")
@@ -269,6 +363,11 @@ async function atomicWrite(path, content) {
269
363
  }
270
364
  }
271
365
  function projectIndex(memory, entries, protectedIds) {
366
+ if (detectLocalMemoryStorageVersion(memory) === 2) {
367
+ const updated = replaceFullMemoryIndex(memory, entries);
368
+ selectLocalMemoryPreview(updated);
369
+ return { memory: updated, omittedIds: [], reason: null };
370
+ }
272
371
  const empty = replaceGeneratedIndex(memory, []);
273
372
  if (codePointLength(empty) > LOCAL_MEMORY_TOTAL_CODE_POINT_CAP)
274
373
  throw new Error("local_memory_memory_over_budget");
@@ -306,12 +405,29 @@ async function withApplyLease(agentDir, operation) {
306
405
  }
307
406
  export async function applyLocalMemoryResult(input) {
308
407
  validateCandidate(input.result);
309
- if (input.result.outcome !== "proposed")
408
+ if (input.migrationDeferredPending)
409
+ return stageMigrationDeferredPending(input);
410
+ if (input.result.outcome !== "proposed" && input.requiredStorageVersion !== 2)
310
411
  return emptyResult();
311
412
  const memoryPath = join(input.agentDir, "MEMORY.md");
312
- await assertWritablePaths(input.agentDir, memoryPath);
313
- return withApplyLease(input.agentDir, async () => {
413
+ if (input.result.outcome === "proposed")
414
+ await assertWritablePaths(input.agentDir, memoryPath);
415
+ await assertMemoryAccessDirectory(input.agentDir);
416
+ return withApplyLease(input.agentDir, async () => withMemoryAccess(input.agentDir, async () => {
417
+ await assertMemoryWriterAllowed(input.agentDir, input.writerCapabilities ?? []);
418
+ const memoryStat = await lstat(memoryPath);
419
+ if (memoryStat.size > 16 * 1024 * 1024)
420
+ throw new Error("local_memory_storage_byte_limit");
314
421
  const memory = await readFile(memoryPath, "utf8");
422
+ assertLocalMemoryStorageSize(memory);
423
+ const fullStorage = detectLocalMemoryStorageVersion(memory) === 2;
424
+ if (input.requiredStorageVersion === 2 && !fullStorage) {
425
+ throw new Error("local_memory_v2_state_invalid_before_commit");
426
+ }
427
+ // A V2 no-op is still a commit decision. Return only after the access
428
+ // lock and committed-state writer gate have validated the same snapshot.
429
+ if (input.result.outcome !== "proposed")
430
+ return emptyResult();
315
431
  if (parseUnmarkedMemoryIndex(memory).length > 0)
316
432
  throw new Error("local_memory_legacy_index_requires_migration");
317
433
  let entries = [...parseLocalMemoryIndex(memory)];
@@ -342,6 +458,26 @@ export async function applyLocalMemoryResult(input) {
342
458
  const pendingCandidates = [];
343
459
  const resolvedPendingIds = new Set();
344
460
  const noteUpdates = new Map();
461
+ const noteCache = new Map();
462
+ let totalReadBytes = Buffer.byteLength(memory, "utf8");
463
+ const readStoredNote = async (path) => {
464
+ const cached = noteUpdates.get(path) ?? noteCache.get(path);
465
+ if (cached !== undefined)
466
+ return cached;
467
+ const stat = await lstat(path).catch((error) => {
468
+ if (error.code === "ENOENT")
469
+ return null;
470
+ throw error;
471
+ });
472
+ if (stat !== null && totalReadBytes + stat.size > LOCAL_MEMORY_READ_BYTE_CAP)
473
+ throw new Error("local_memory_read_byte_limit");
474
+ const content = await readNote(path);
475
+ totalReadBytes += Buffer.byteLength(content, "utf8");
476
+ if (totalReadBytes > LOCAL_MEMORY_READ_BYTE_CAP)
477
+ throw new Error("local_memory_read_byte_limit");
478
+ noteCache.set(path, content);
479
+ return content;
480
+ };
345
481
  let allowActiveEviction = false;
346
482
  const randomId = input.randomId ?? randomUUID;
347
483
  const executionId = input.executionId ?? "unknown";
@@ -383,10 +519,21 @@ export async function applyLocalMemoryResult(input) {
383
519
  if (existingCreate !== undefined && !matchesCreateCandidate(existingCreate, change)) {
384
520
  throw new Error("local_memory_duplicate_candidate");
385
521
  }
386
- if (existingCreate === undefined && entries.some((entry) => normalize(entry.title) === normalize(change.title)
387
- || normalize(entry.summary) === normalize(change.summary))) {
522
+ if (existingCreate === undefined && entries.some((entry) => (!fullStorage || entry.status === "active" || entry.status === "pinned")
523
+ && (normalize(entry.title) === normalize(change.title) || normalize(entry.summary) === normalize(change.summary)))) {
388
524
  throw new Error("local_memory_duplicate_candidate");
389
525
  }
526
+ if (existingCreate !== undefined) {
527
+ if (existingCreate.revision !== 1 || existingCreate.status !== "active") {
528
+ throw new Error("local_memory_create_replay_conflict");
529
+ }
530
+ const stored = await readStoredNote(resolve(input.agentDir, existingCreate.file));
531
+ if (uniqueNoteSection(stored, id)?.trimEnd() !== noteSection(id, 1, change.title, change.detailBody).trimEnd()) {
532
+ throw new Error("local_memory_create_replay_conflict");
533
+ }
534
+ // A proven duplicate is a no-op; it must never rewrite a newer body or status.
535
+ continue;
536
+ }
390
537
  }
391
538
  const revision = existingCreate?.revision ?? (current === undefined ? 1 : current.revision + 1);
392
539
  const entry = existingCreate ?? entryFromChange(change, id, revision, current?.status ?? "active");
@@ -400,7 +547,7 @@ export async function applyLocalMemoryResult(input) {
400
547
  : item),
401
548
  entry,
402
549
  ];
403
- const allowEvictionForChange = change.operation === "create" && change.priority === "high";
550
+ const allowEvictionForChange = !fullStorage && change.operation === "create" && change.priority === "high";
404
551
  const protectedIds = new Set(allowEvictionForChange ? pinnedIds(entries) : activeOrPinnedIds(entries));
405
552
  if (allowEvictionForChange)
406
553
  allowActiveEviction = true;
@@ -417,7 +564,7 @@ export async function applyLocalMemoryResult(input) {
417
564
  const notePath = resolve(input.agentDir, entry.file);
418
565
  if (!isWithin(resolve(input.agentDir, "notes"), notePath))
419
566
  throw new Error("local_memory_note_path_invalid");
420
- const previous = noteUpdates.get(notePath) ?? await readNote(notePath);
567
+ const previous = await readStoredNote(notePath);
421
568
  const replacement = noteSection(id, revision, change.title, change.detailBody);
422
569
  const existingSection = replaceNote(previous, id, replacement);
423
570
  const next = existingSection ?? (current === undefined
@@ -435,7 +582,7 @@ export async function applyLocalMemoryResult(input) {
435
582
  // Pending create candidates are durable overflow, not dead ends. Promote only
436
583
  // when the current catalog has room; supersede/update candidates stay pending
437
584
  // until a fresh revision-aware decision can safely replay them.
438
- const pendingCandidatesOnDisk = await readPendingMemoryCandidates({ agentDir: input.agentDir }).catch((error) => {
585
+ const pendingCandidatesOnDisk = fullStorage ? [] : await readPendingMemoryCandidates({ agentDir: input.agentDir }).catch((error) => {
439
586
  if (!(error instanceof Error) || error.message !== "memory_pending_directory_over_budget")
440
587
  throw error;
441
588
  // 自动提升是可选工作,目录查询预算不应阻断本轮已验证的更新或腾位操作。
@@ -446,6 +593,10 @@ export async function applyLocalMemoryResult(input) {
446
593
  return [];
447
594
  });
448
595
  for (const pending of pendingCandidatesOnDisk) {
596
+ // Migration-deferred candidates require reviewed import; only capacity
597
+ // overflow is eligible for the existing automatic promotion path.
598
+ if (pending.reason !== "index_over_budget")
599
+ continue;
449
600
  const change = pending.candidate;
450
601
  if (change.operation !== "create")
451
602
  continue;
@@ -460,7 +611,7 @@ export async function applyLocalMemoryResult(input) {
460
611
  || normalize(entry.summary) === normalize(change.summary)));
461
612
  if (duplicate !== undefined) {
462
613
  const duplicatePath = resolve(input.agentDir, duplicate.file);
463
- const duplicateBody = readNoteBody(noteUpdates.get(duplicatePath) ?? await readNote(duplicatePath), duplicate.id);
614
+ const duplicateBody = readNoteBody(await readStoredNote(duplicatePath), duplicate.id);
464
615
  if (duplicateBody === change.detailBody
465
616
  || duplicateBody === unwrappedNoteBody(change.title, change.detailBody).trim()) {
466
617
  resolvedPendingIds.add(pending.pendingId);
@@ -478,7 +629,7 @@ export async function applyLocalMemoryResult(input) {
478
629
  const notePath = resolve(input.agentDir, entry.file);
479
630
  if (!isWithin(resolve(input.agentDir, "notes"), notePath))
480
631
  throw new Error("local_memory_note_path_invalid");
481
- const previous = noteUpdates.get(notePath) ?? await readNote(notePath);
632
+ const previous = await readStoredNote(notePath);
482
633
  const replacement = noteSection(id, 1, change.title, change.detailBody);
483
634
  const existingSection = replaceNote(previous, id, replacement);
484
635
  const next = existingSection ?? `${previous.trimEnd()}${previous.trimEnd() ? "\n\n" : ""}${replacement}`;
@@ -487,6 +638,17 @@ export async function applyLocalMemoryResult(input) {
487
638
  createdIds.push(id);
488
639
  resolvedPendingIds.add(pending.pendingId);
489
640
  }
641
+ if (fullStorage) {
642
+ const changedMetadataIds = new Set([
643
+ ...statusChangedIds,
644
+ ...input.result.changes.flatMap(change => change.supersedesMemoryId === null ? [] : [change.supersedesMemoryId]),
645
+ ]);
646
+ for (const id of changedMetadataIds) {
647
+ const entry = entries.find(item => item.id === id);
648
+ const notePath = resolve(input.agentDir, entry.file);
649
+ noteUpdates.set(notePath, updateNoteRevision(await readStoredNote(notePath), id, entry.revision));
650
+ }
651
+ }
490
652
  let finalProjection = projectIndex(memory, entries, allowActiveEviction ? pinnedIds(entries) : activeOrPinnedIds(entries));
491
653
  if (finalProjection.reason !== null
492
654
  && (!allowActiveEviction || finalProjection.reason === "index_over_budget_pinned")) {
@@ -498,7 +660,7 @@ export async function applyLocalMemoryResult(input) {
498
660
  .filter((entry) => entry?.status === "active");
499
661
  for (const entry of omittedActiveEntries) {
500
662
  const notePath = resolve(input.agentDir, entry.file);
501
- const content = noteUpdates.get(notePath) ?? await readNote(notePath);
663
+ const content = await readStoredNote(notePath);
502
664
  const detailBody = readNoteBody(content, entry.id);
503
665
  const change = detailBody === null ? null : archivedChange(entry, detailBody);
504
666
  if (change === null)
@@ -519,6 +681,13 @@ export async function applyLocalMemoryResult(input) {
519
681
  if (finalProjection.reason !== null)
520
682
  throw new Error(finalProjection.reason);
521
683
  }
684
+ // Complete every resource check before the first persistent write.
685
+ assertLocalMemoryStorageSize(finalProjection.memory);
686
+ for (const content of noteUpdates.values()) {
687
+ if (Buffer.byteLength(content, "utf8") > LOCAL_MEMORY_NOTE_BYTE_CAP)
688
+ throw new Error("local_memory_note_byte_limit");
689
+ }
690
+ await assertMemoryInventoryHealth(input.agentDir);
522
691
  for (const candidate of pendingCandidates)
523
692
  await persistPendingMemoryCandidate({ agentDir: input.agentDir, candidate });
524
693
  for (const [path, content] of noteUpdates)
@@ -535,5 +704,5 @@ export async function applyLocalMemoryResult(input) {
535
704
  deferredIds,
536
705
  pendingIds: deferredIds,
537
706
  };
538
- });
707
+ }));
539
708
  }
@@ -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
+ }
@@ -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";
@@ -8,6 +8,7 @@ const INJECTION_MARKERS = [
8
8
  "## [注入] 你的 MEMORY.md(索引,只读参考)\n",
9
9
  ];
10
10
  const LOCAL_MEMORY_HASH_CAP_BYTES = 10 * 1024 * 1024;
11
+ export const LOCAL_MEMORY_DIAGNOSTICS_TIMEOUT_MS = 2_000;
11
12
  function errorCode(error) {
12
13
  const code = error?.code;
13
14
  return typeof code === "string" ? code : error instanceof Error ? error.name : "unknown";
@@ -39,30 +40,75 @@ async function inspectFile(path, signal) {
39
40
  return { ...base, error_code: errorCode(error) };
40
41
  }
41
42
  }
42
- export async function inspectLocalMemoryFilesWithinDeadline(homeDir, timeoutMs) {
43
+ async function inspectPendingDirectory(path, signal) {
44
+ let metadata;
45
+ try {
46
+ metadata = await lstat(path);
47
+ }
48
+ catch (error) {
49
+ const code = errorCode(error);
50
+ return code === "ENOENT" ? { exists: false, entry_count: 0 } : { exists: false, error_code: code };
51
+ }
52
+ const base = { exists: true, mtime_ms: metadata.mtimeMs };
53
+ if (metadata.isSymbolicLink() || !metadata.isDirectory())
54
+ return { ...base, hash_skipped_reason: "not_regular_file" };
55
+ try {
56
+ const names = (await readdir(path, { withFileTypes: true }))
57
+ .filter(entry => entry.name.endsWith(".json"))
58
+ .sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
59
+ if (names.length > 128)
60
+ return { ...base, entry_count: names.length, error_code: "entry_limit" };
61
+ const hash = createHash("sha256");
62
+ let size = 0;
63
+ for (const entry of names) {
64
+ if (signal.aborted)
65
+ throw signal.reason;
66
+ if (!entry.isFile() || entry.isSymbolicLink())
67
+ return { ...base, entry_count: names.length, error_code: "invalid_entry" };
68
+ const fact = await inspectFile(join(path, entry.name), signal);
69
+ if (!fact.exists || fact.sha256 === undefined || fact.error_code !== undefined
70
+ || fact.hash_skipped_reason !== undefined)
71
+ return { ...base, entry_count: names.length, error_code: "incomplete_entry" };
72
+ size += fact.size_bytes ?? 0;
73
+ hash.update(entry.name).update("\0").update(fact.sha256).update("\0");
74
+ }
75
+ return { ...base, size_bytes: size, entry_count: names.length, sha256: hash.digest("hex") };
76
+ }
77
+ catch (error) {
78
+ return { ...base, error_code: errorCode(error) };
79
+ }
80
+ }
81
+ export async function withinLocalMemoryDiagnosticsDeadline(operation, timeoutMs) {
43
82
  const controller = new AbortController();
44
83
  let timer;
45
- const inspection = Promise.all([
46
- inspectFile(join(homeDir, "MEMORY.md"), controller.signal),
47
- inspectFile(join(homeDir, "notes", "lessons.md"), controller.signal),
48
- ]).then(([memory, lessons]) => ({ memory, lessons }));
49
84
  const timeout = new Promise((_resolve, reject) => {
50
85
  timer = setTimeout(() => {
51
- controller.abort();
52
- reject(Object.assign(new Error("local memory diagnostics timed out"), {
86
+ const error = Object.assign(new Error("local memory diagnostics timed out"), {
53
87
  code: "diagnostics_timeout",
54
- }));
88
+ });
89
+ controller.abort(error);
90
+ reject(error);
55
91
  }, Math.max(1, timeoutMs));
56
92
  timer.unref?.();
57
93
  });
58
94
  try {
59
- return await Promise.race([inspection, timeout]);
95
+ return await Promise.race([operation(controller.signal), timeout]);
60
96
  }
61
97
  finally {
62
98
  if (timer !== undefined)
63
99
  clearTimeout(timer);
64
100
  }
65
101
  }
102
+ export async function inspectLocalMemoryFilesWithinDeadline(homeDir, timeoutMs) {
103
+ return withinLocalMemoryDiagnosticsDeadline(async (signal) => {
104
+ const [memory, lessons, pending] = await Promise.all([
105
+ inspectFile(join(homeDir, "MEMORY.md"), signal),
106
+ inspectFile(join(homeDir, "notes", "lessons.md"), signal),
107
+ inspectPendingDirectory(join(homeDir, ".crew", "memory", "pending"), signal),
108
+ ]);
109
+ return { memory, lessons, pending };
110
+ }, timeoutMs);
111
+ }
66
112
  export function localMemoryContentFacts(content, maxScanBytes = LOCAL_MEMORY_HASH_CAP_BYTES) {
67
113
  const sizeBytes = Buffer.byteLength(content, "utf8");
68
114
  if (sizeBytes > Math.max(0, maxScanBytes)) {
@@ -76,12 +122,12 @@ export function localMemoryContentFacts(content, maxScanBytes = LOCAL_MEMORY_HAS
76
122
  lessons_reference_count: content.match(/\bnotes[\\/]lessons\.md\b/gi)?.length ?? 0,
77
123
  };
78
124
  }
79
- export function analyzeLocalMemoryInjection(systemPrompt, sourceMemory) {
125
+ export function analyzeLocalMemoryInjection(systemPrompt, sourceMemory, query = "") {
80
126
  const sourceBytes = Buffer.byteLength(sourceMemory, "utf8");
81
127
  if (sourceMemory.length === 0) {
82
128
  return { state: "empty_source", source_bytes: 0, bounded_bytes: 0, injected_bytes: 0 };
83
129
  }
84
- const bounded = capMemoryForInject(sourceMemory);
130
+ const bounded = capMemoryForInject(sourceMemory, undefined, Number.POSITIVE_INFINITY, query);
85
131
  const marker = INJECTION_MARKERS.find((candidate) => systemPrompt.includes(candidate));
86
132
  if (marker === undefined) {
87
133
  return {
@@ -217,15 +263,50 @@ function crewMemoryReadAccess(command, paths) {
217
263
  return null;
218
264
  }
219
265
  }
266
+ /**
267
+ * Decode only the shell wrapper shape emitted by supported runtimes. The body must be one complete
268
+ * quoted argument. Double-quoted bodies may escape only a quote or backslash; shell expansion and
269
+ * concatenated quoting stay unknown so telemetry never guesses what the shell actually executed.
270
+ */
271
+ function unwrapLiteralShellCommand(command) {
272
+ const trimmed = command.trim();
273
+ const wrapper = /^\/bin\/(?:(?:zsh|bash)\s+-lc|sh\s+-c)\s+([\s\S]+)$/u.exec(trimmed);
274
+ if (wrapper === null)
275
+ return trimmed;
276
+ const quoted = wrapper[1];
277
+ const quote = quoted[0];
278
+ if ((quote !== "'" && quote !== '"') || quoted.at(-1) !== quote || quoted.length < 2)
279
+ return null;
280
+ const body = quoted.slice(1, -1);
281
+ if (quote === "'")
282
+ return body.includes("'") ? null : body;
283
+ let decoded = "";
284
+ for (let index = 0; index < body.length; index += 1) {
285
+ const character = body[index];
286
+ if (character === '"' || character === "$" || character === "`")
287
+ return null;
288
+ if (character !== "\\") {
289
+ decoded += character;
290
+ continue;
291
+ }
292
+ const escaped = body[index + 1];
293
+ if (escaped !== '"' && escaped !== "\\")
294
+ return null;
295
+ decoded += escaped;
296
+ index += 1;
297
+ }
298
+ return decoded;
299
+ }
220
300
  function commandAccesses(command, paths) {
221
- if (/[;|&\n]/.test(command))
301
+ const unwrapped = unwrapLiteralShellCommand(command);
302
+ if (unwrapped === null || /[;|&\n]/.test(unwrapped))
222
303
  return [];
223
304
  const accesses = new Map();
224
- const memoryRead = crewMemoryReadAccess(command, paths);
305
+ const memoryRead = crewMemoryReadAccess(unwrapped, paths);
225
306
  if (memoryRead !== null) {
226
307
  accesses.set(`${memoryRead.target}:${memoryRead.relative_path ?? ""}:read`, memoryRead);
227
308
  }
228
- const expanded = command
309
+ const expanded = unwrapped
229
310
  .replaceAll("${CREW_HOME}", paths.homeDir)
230
311
  .replaceAll("$CREW_HOME", paths.homeDir)
231
312
  .replace(/["']/g, "");
@@ -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) {
@@ -103,7 +110,7 @@ export function createLocalMemoryTelemetry(options) {
103
110
  contextPrepared(context) {
104
111
  try {
105
112
  const sourceFacts = localMemoryContentFacts(context.memory);
106
- const injectionFacts = analyzeLocalMemoryInjection(context.systemPrompt, context.memory);
113
+ const injectionFacts = analyzeLocalMemoryInjection(context.systemPrompt, context.memory, context.memoryQuery);
107
114
  safeLog("local_memory.context_prepared", "本地记忆上下文已准备", {
108
115
  execution_id: options.executionId,
109
116
  agent_handle: options.agentHandle,
@@ -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
  },
@@ -26,6 +26,7 @@ import { RUNTIME_HEALTH_PROBE_CAPABILITY } from "./runtime-health.js";
26
26
  import { probeDeepSeekHarnessAcp, deepSeekHarnessLauncher } from "./runtimes/deepseek-harness.js";
27
27
  import { daemonHome } from "./computer-profile.js";
28
28
  import { daemonReleaseProvenance } from "./release-provenance.js";
29
+ import { MEMORY_MIGRATION_CAPABILITY, MEMORY_STORAGE_V2_CAPABILITY, MEMORY_STATE_GUARD_CAPABILITY } from "./memory-migration-protocol.js";
29
30
  export { executableRuntimes } from "./runtime-capabilities.js";
30
31
  const execFileP = promisify(execFile);
31
32
  export const PROJECT_WORKSPACES_CAPABILITY = "project_workspaces_v1";
@@ -41,6 +42,7 @@ export const NATIVE_PROJECT_WORKSPACE_READINESS = Object.freeze({
41
42
  });
42
43
  export const DAEMON_CAPABILITIES = Object.freeze([
43
44
  MARKETPLACE_CAPABILITY,
45
+ MEMORY_MIGRATION_CAPABILITY, MEMORY_STORAGE_V2_CAPABILITY, MEMORY_STATE_GUARD_CAPABILITY,
44
46
  MAINTENANCE_CAPABILITY,
45
47
  "scheduled_job_v1",
46
48
  "reply_origin_v1",
@@ -77,6 +79,7 @@ export const daemonCapabilities = (runtimePlatform = process.platform, readiness
77
79
  return Object.freeze(DAEMON_CAPABILITIES.filter((capability) => (((capability !== MARKETPLACE_CAPABILITY && capability !== MAINTENANCE_CAPABILITY) || nativeWorkspacePlatform)
78
80
  && (capability !== PROJECT_WORKSPACES_CAPABILITY || nativeWorkspaceReady)
79
81
  && (capability !== "message_file_preview_v1" || nativeWorkspacePlatform)
82
+ && (nativeWorkspacePlatform || ![MEMORY_MIGRATION_CAPABILITY, MEMORY_STORAGE_V2_CAPABILITY, MEMORY_STATE_GUARD_CAPABILITY].includes(capability))
80
83
  && (nativeWorkspacePlatform
81
84
  || (capability !== "project_skills_v1"
82
85
  && capability !== "project_skill_projection_v2"
package/dist/main.js CHANGED
File without changes