@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.
- package/dist/computer-main.js +0 -0
- package/dist/execution-journal.js +3 -1
- package/dist/execution-protocol.js +1 -1
- package/dist/execution-runner.js +3 -1
- package/dist/local-execution-prompt.js +8 -3
- package/dist/local-executor.js +18 -35
- package/dist/local-memory/execution-workspace.js +36 -0
- package/dist/local-memory/full-index-search.js +47 -0
- package/dist/local-memory/index-budget.js +9 -3
- package/dist/local-memory/inventory-cli.js +21 -1
- package/dist/local-memory/inventory-source.js +15 -8
- package/dist/local-memory/legacy-adapter.js +221 -0
- package/dist/local-memory/migration-access.js +169 -0
- package/dist/local-memory/migration-cli.js +54 -0
- package/dist/local-memory/migration-io.js +199 -0
- package/dist/local-memory/migration-plan.js +283 -0
- package/dist/local-memory/migration-schema.js +55 -0
- package/dist/local-memory/migration-store.js +424 -0
- package/dist/local-memory/migration-tool-entry.js +21 -0
- package/dist/local-memory/prompt.js +20 -4
- package/dist/local-memory/read-cli.js +75 -77
- package/dist/local-memory/rollout.js +53 -8
- package/dist/local-memory/runtime.js +6 -2
- package/dist/local-memory/storage-format.js +152 -0
- package/dist/local-memory/write.js +125 -19
- package/dist/machine-info.js +3 -0
- package/dist/main.js +0 -0
- package/dist/memory-migration-controller.js +84 -0
- package/dist/memory-migration-inspect.js +74 -0
- package/dist/memory-migration-journal.js +14 -0
- package/dist/memory-migration-protocol.js +14 -0
- package/dist/memory-migration-runtime.js +305 -0
- package/dist/message-file-preview.js +11 -1
- package/dist/prompt.js +5 -1
- package/dist/runner.js +1 -1
- package/dist/serve.js +36 -3
- package/dist/token.js +1 -0
- package/dist/workspace-control-handler.js +16 -3
- 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(
|
|
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
|
|
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
|
|
203
|
-
if (
|
|
231
|
+
const section = uniqueNoteSection(content, id);
|
|
232
|
+
if (section === null)
|
|
204
233
|
return null;
|
|
205
|
-
const bodyStart =
|
|
206
|
-
const bodyEnd =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
}
|
package/dist/machine-info.js
CHANGED
|
@@ -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();
|