@nowcrew/daemon 0.6.91-beta.12 → 0.6.91-beta.13

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 (39) hide show
  1. package/dist/computer-main.js +0 -0
  2. package/dist/execution-journal.js +3 -1
  3. package/dist/execution-protocol.js +1 -1
  4. package/dist/execution-runner.js +3 -1
  5. package/dist/local-execution-prompt.js +8 -3
  6. package/dist/local-executor.js +18 -35
  7. package/dist/local-memory/execution-workspace.js +36 -0
  8. package/dist/local-memory/full-index-search.js +47 -0
  9. package/dist/local-memory/index-budget.js +9 -3
  10. package/dist/local-memory/inventory-cli.js +21 -1
  11. package/dist/local-memory/inventory-source.js +15 -8
  12. package/dist/local-memory/legacy-adapter.js +221 -0
  13. package/dist/local-memory/migration-access.js +169 -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 +424 -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 +53 -8
  23. package/dist/local-memory/runtime.js +6 -2
  24. package/dist/local-memory/storage-format.js +152 -0
  25. package/dist/local-memory/write.js +125 -19
  26. package/dist/machine-info.js +3 -0
  27. package/dist/main.js +0 -0
  28. package/dist/memory-migration-controller.js +84 -0
  29. package/dist/memory-migration-inspect.js +74 -0
  30. package/dist/memory-migration-journal.js +14 -0
  31. package/dist/memory-migration-protocol.js +14 -0
  32. package/dist/memory-migration-runtime.js +305 -0
  33. package/dist/message-file-preview.js +11 -1
  34. package/dist/prompt.js +5 -1
  35. package/dist/runner.js +1 -1
  36. package/dist/serve.js +36 -3
  37. package/dist/token.js +1 -0
  38. package/dist/workspace-control-handler.js +16 -3
  39. package/package.json +9 -8
@@ -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
  }
@@ -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"
package/dist/main.js CHANGED
File without changes
@@ -0,0 +1,84 @@
1
+ import { z } from "zod";
2
+ import { createRuntimeCancellation } from "./runtime-cancellation.js";
3
+ import { MemoryMigrationStartSchema } from "./memory-migration-protocol.js";
4
+ import { runMemoryMigration } from "./memory-migration-runtime.js";
5
+ import { inspectMemoryForMigration } from "./memory-migration-inspect.js";
6
+ const Inspect = MemoryMigrationStartSchema.pick({ reqId: true, protocolVersion: true, workspaceId: true,
7
+ agentId: true, handle: true, machineId: true }).extend({ type: z.literal("memory:migration:inspect"), executionId: z.string().uuid().optional() }).strict();
8
+ export function createMemoryMigrationController(input) {
9
+ let identity = null;
10
+ const runs = new Map();
11
+ return {
12
+ setIdentity(value) { identity = value; },
13
+ cancel(executionId) { const run = runs.get(executionId); if (!run)
14
+ return false; run.cancel(); return true; },
15
+ async handle(raw) {
16
+ const type = raw?.type;
17
+ if (type !== "memory:migration:start" && type !== "memory:migration:inspect")
18
+ return false;
19
+ const parsed = type === "memory:migration:start" ? MemoryMigrationStartSchema.safeParse(raw) : Inspect.safeParse(raw);
20
+ if (!parsed.success) {
21
+ const reqId = raw.reqId;
22
+ if (typeof reqId === "string" && reqId.length <= 200)
23
+ input.send({ type: "fs:result", reqId, ok: false, error: "memory_migration_invalid_request" });
24
+ return true;
25
+ }
26
+ const request = parsed.data;
27
+ if (!identity || request.workspaceId !== identity.workspaceId || request.machineId !== identity.machineId) {
28
+ input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_identity_mismatch" });
29
+ return true;
30
+ }
31
+ let journal;
32
+ try {
33
+ journal = input.journal();
34
+ }
35
+ catch {
36
+ input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_journal_unavailable" });
37
+ return true;
38
+ }
39
+ if (request.type === "memory:migration:inspect") {
40
+ try {
41
+ const data = await inspectMemoryForMigration(input.config.agentsRoot, request, journal);
42
+ input.send({ type: "fs:result", reqId: request.reqId, ok: true, data });
43
+ }
44
+ catch {
45
+ input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_inspection_failed" });
46
+ }
47
+ return true;
48
+ }
49
+ const existing = runs.get(request.executionId);
50
+ if (existing) {
51
+ input.send({ type: "fs:result", reqId: request.reqId, ok: true, data: { kind: "running" } });
52
+ return true;
53
+ }
54
+ if (input.isAdmissionClosed?.()) {
55
+ input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_storage_busy" });
56
+ return true;
57
+ }
58
+ const cancellation = createRuntimeCancellation();
59
+ const promise = runMemoryMigration(request, {
60
+ agentsRoot: input.config.agentsRoot, coordinator: input.coordinator, journal,
61
+ cancellation: cancellation.cancellation, send: input.send,
62
+ control: async (step, body) => {
63
+ const base = `/daemon/memory-migrations/${encodeURIComponent(request.agentId)}`;
64
+ const path = step === "history" ? `${base}/history` : `${base}/${request.migrationId}/${step === "needs-review" ? "review" : step}`;
65
+ const response = await fetch(new URL(path, input.config.serverUrl), {
66
+ method: step === "history" ? "GET" : "POST", signal: AbortSignal.timeout(10_000),
67
+ headers: { authorization: `Bearer ${input.config.machineToken}`, "content-type": "application/json" },
68
+ ...(step === "history" ? {} : { body: JSON.stringify(body) }),
69
+ });
70
+ const result = await response.json();
71
+ if (!response.ok || result.success !== true)
72
+ throw new Error("migration_control_unconfirmed");
73
+ return result.data;
74
+ },
75
+ }).then(data => input.send({ type: "fs:result", reqId: request.reqId, ok: true, data }))
76
+ .catch(() => input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_failed" }))
77
+ .finally(() => runs.delete(request.executionId));
78
+ runs.set(request.executionId, { promise, cancel: cancellation.request });
79
+ return true;
80
+ },
81
+ async stop() { for (const run of runs.values())
82
+ run.cancel(); await Promise.allSettled([...runs.values()].map(run => run.promise)); },
83
+ };
84
+ }
@@ -0,0 +1,74 @@
1
+ import { lstat, readFile, realpath, readdir } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { withMemoryAccess } from "./local-memory/migration-access.js";
5
+ import { MigrationStateSchema } from "./local-memory/migration-schema.js";
6
+ import { detectLocalMemoryStorageVersion, parseFullMemoryIndex } from "./local-memory/storage-format.js";
7
+ import { resolveLocalMemoryV2Rollout } from "./local-memory/rollout.js";
8
+ import { readMemoryInventorySource, inventoryLimits } from "./local-memory/inventory-source.js";
9
+ export async function inspectMemoryForMigration(agentsRoot, input, journal) {
10
+ const parent = await realpath(agentsRoot);
11
+ const path = join(parent, input.handle);
12
+ const base = { workspaceId: input.workspaceId, agentId: input.agentId, machineId: input.machineId,
13
+ storageRootFingerprint: createHash("sha256").update(resolve(path)).digest("hex"),
14
+ ownerIdentity: String((await lstat(parent)).uid) };
15
+ const prior = input.executionId ? await journal?.get(input.executionId) : null;
16
+ const stoppedExecutionId = prior?.requestedRuntime === "memory_migration" && (prior.state === "completed" || prior.state === "interrupted") ? prior.executionId : null;
17
+ const empty = { ...base, storageVersion: null, classification: "empty", emptyWithoutResidue: true, localState: null, issueCodes: [], stoppedExecutionId };
18
+ try {
19
+ await lstat(path);
20
+ }
21
+ catch (e) {
22
+ if (e.code === "ENOENT")
23
+ return empty;
24
+ throw e;
25
+ }
26
+ try {
27
+ return await withMemoryAccess(path, async () => {
28
+ let localState = null;
29
+ try {
30
+ const record = await readMemoryInventorySource(path, ".crew/memory/migration.v1.json", inventoryLimits.fileBytes);
31
+ const state = MigrationStateSchema.parse(JSON.parse(record.content));
32
+ if (state.identity.rootFingerprint !== base.storageRootFingerprint || state.identity.agentId !== input.agentId
33
+ || state.identity.workspaceId !== input.workspaceId || state.identity.machineId !== input.machineId)
34
+ throw new Error("migration_identity_mismatch");
35
+ localState = { migrationId: state.migrationId, phase: state.phase, storageVersion: state.storageVersion,
36
+ sourceDigest: state.sourceDigest, planDigest: state.planDigest };
37
+ if (state.phase === "applying" || state.phase === "repair_required")
38
+ return {
39
+ ...empty, localState, classification: "repair_required", emptyWithoutResidue: false, issueCodes: ["migration_recovery_required"],
40
+ };
41
+ }
42
+ catch (e) {
43
+ if (e.code !== "ENOENT")
44
+ throw e;
45
+ }
46
+ let source;
47
+ try {
48
+ source = await readMemoryInventorySource(path, "MEMORY.md", inventoryLimits.fileBytes);
49
+ }
50
+ catch (e) {
51
+ if (e.code !== "ENOENT")
52
+ throw e;
53
+ const names = await readdir(path);
54
+ const nonMetadata = names.filter(name => name !== ".crew");
55
+ return { ...empty, localState, emptyWithoutResidue: nonMetadata.length === 0 && localState === null,
56
+ classification: nonMetadata.length === 0 && localState === null ? "empty" : "repair_required", issueCodes: ["memory_source_missing"] };
57
+ }
58
+ const storageVersion = detectLocalMemoryStorageVersion(source.content);
59
+ if (storageVersion === 2) {
60
+ parseFullMemoryIndex(source.content);
61
+ return { ...base, storageVersion, classification: "valid_v2", emptyWithoutResidue: false, localState, issueCodes: [], stoppedExecutionId };
62
+ }
63
+ const eligibility = await resolveLocalMemoryV2Rollout({ workspace: { dir: path, memory: source.content },
64
+ serverUrl: "https://nowwork-dev.nowcoder.com", handle: input.handle, requested: true });
65
+ return { ...base, storageVersion, classification: storageVersion === 1 && eligibility.enabled ? "valid_v1"
66
+ : eligibility.legacyFallback || eligibility.enabled ? "legacy" : "repair_required",
67
+ emptyWithoutResidue: false, localState, stoppedExecutionId, issueCodes: eligibility.enabled || eligibility.legacyFallback ? [] : ["memory_source_incompatible"] };
68
+ }, { maintenance: true, waitMs: 0 });
69
+ }
70
+ catch (error) {
71
+ const code = error instanceof Error && /^[a-z_]+$/u.test(error.message) ? error.message : "memory_inspection_failed";
72
+ return { ...empty, emptyWithoutResidue: false, classification: code === "memory_migration_access_busy" ? "busy" : "repair_required", issueCodes: [code] };
73
+ }
74
+ }
@@ -0,0 +1,14 @@
1
+ import { join } from "node:path";
2
+ import { createExecutionJournal } from "./execution-journal.js";
3
+ import { dslog } from "./slog.js";
4
+ /** 复用journal的监管/恢复实现;旧daemon只读普通目录,不会解析维护类型。 */
5
+ export function createMemoryMigrationJournal(agentsRoot) {
6
+ return createExecutionJournal(join(agentsRoot, ".crew", "memory-migration-runtime"), { retainCompleted: true });
7
+ }
8
+ export async function replayExecutionJournals(normal, maintenance) {
9
+ const [ordinary, migration] = await Promise.all([normal.replay(), maintenance.replay().catch(() => {
10
+ dslog("memory_migration.journal_unavailable", "迁移日志暂不可读,保留普通执行能力", { level: "ERROR" });
11
+ return [];
12
+ })]);
13
+ return [...ordinary, ...migration];
14
+ }
@@ -0,0 +1,14 @@
1
+ import { z } from "zod";
2
+ import { MigrationRecordMappingSchema } from "./local-memory/migration-schema.js";
3
+ export const MEMORY_MIGRATION_CAPABILITY = "memory_migration_v1";
4
+ export const MEMORY_STORAGE_V2_CAPABILITY = "memory_storage_v2";
5
+ export const MEMORY_STATE_GUARD_CAPABILITY = "memory_state_guard_v1";
6
+ export const MemoryMigrationStartSchema = z.object({
7
+ type: z.literal("memory:migration:start"), reqId: z.string().min(1).max(200), protocolVersion: z.literal(1),
8
+ workspaceId: z.string().uuid(), agentId: z.string().uuid(), handle: z.string().min(1).max(128).regex(/^[A-Za-z0-9_-]+$/u),
9
+ machineId: z.string().uuid(), executionId: z.string().uuid(), migrationId: z.string().uuid(),
10
+ expectedRevision: z.number().int().nonnegative(), attemptKey: z.string().min(1).max(200),
11
+ sourceStorageVersion: z.union([z.literal(0), z.literal(1)]), targetStorageVersion: z.literal(2),
12
+ action: z.enum(["migrate", "recover"]),
13
+ recordMappings: z.array(MigrationRecordMappingSchema).max(10_000).optional(),
14
+ }).strict();