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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/cloud-report-upload.js +10 -1
  2. package/dist/execution-journal.js +1 -3
  3. package/dist/execution-protocol.js +1 -1
  4. package/dist/execution-runner.js +1 -4
  5. package/dist/local-execution-prompt.js +3 -8
  6. package/dist/local-executor.js +39 -63
  7. package/dist/local-memory/index-budget.js +3 -9
  8. package/dist/local-memory/inventory-cli.js +8 -49
  9. package/dist/local-memory/inventory-source.js +8 -15
  10. package/dist/local-memory/pending-query.js +2 -5
  11. package/dist/local-memory/pending-schema.js +1 -1
  12. package/dist/local-memory/pending.js +2 -16
  13. package/dist/local-memory/prompt.js +5 -21
  14. package/dist/local-memory/read-cli.js +76 -98
  15. package/dist/local-memory/rollout.js +11 -79
  16. package/dist/local-memory/runtime.js +13 -181
  17. package/dist/local-memory/write.js +21 -189
  18. package/dist/local-memory-diagnostics.js +13 -94
  19. package/dist/local-memory-telemetry.js +2 -19
  20. package/dist/machine-info.js +0 -3
  21. package/dist/marketplace-protocol.js +1 -1
  22. package/dist/marketplace-remote.js +1 -1
  23. package/dist/marketplace-runner.js +1 -1
  24. package/dist/memory-prune-diagnostics.js +4 -60
  25. package/dist/message-file-preview.js +1 -11
  26. package/dist/prompt.js +2 -6
  27. package/dist/runner.js +1 -1
  28. package/dist/runtimes/claude-thread-runner.js +4 -25
  29. package/dist/serve.js +3 -38
  30. package/dist/token.js +0 -1
  31. package/dist/workspace-control-handler.js +3 -16
  32. package/dist/workspace.js +2 -13
  33. package/package.json +1 -1
  34. package/dist/local-memory/commit-telemetry.js +0 -30
  35. package/dist/local-memory/execution-workspace.js +0 -62
  36. package/dist/local-memory/full-index-search.js +0 -47
  37. package/dist/local-memory/initialization-identity.js +0 -36
  38. package/dist/local-memory/inventory-v2.js +0 -361
  39. package/dist/local-memory/legacy-adapter.js +0 -233
  40. package/dist/local-memory/migration-access.js +0 -242
  41. package/dist/local-memory/migration-cli.js +0 -54
  42. package/dist/local-memory/migration-io.js +0 -199
  43. package/dist/local-memory/migration-pending.js +0 -9
  44. package/dist/local-memory/migration-plan.js +0 -317
  45. package/dist/local-memory/migration-schema.js +0 -55
  46. package/dist/local-memory/migration-state-invariants.js +0 -65
  47. package/dist/local-memory/migration-store.js +0 -414
  48. package/dist/local-memory/migration-tool-entry.js +0 -21
  49. package/dist/local-memory/storage-format.js +0 -152
  50. package/dist/local-memory/writer-mode.js +0 -13
  51. package/dist/memory-migration-controller.js +0 -86
  52. package/dist/memory-migration-inspect.js +0 -108
  53. package/dist/memory-migration-journal.js +0 -14
  54. package/dist/memory-migration-protocol.js +0 -14
  55. package/dist/memory-migration-runtime.js +0 -305
@@ -1,414 +0,0 @@
1
- import { statfs, rm, readdir, opendir } from 'node:fs/promises';
2
- import { dirname } from 'node:path';
3
- import { z } from 'zod';
4
- import { atomicBytes, digest, inventory, readBytes, safePath, STATE_PATH, syncDirectory, validateMigrationRoot } from './migration-io.js';
5
- import { initializeExactWorkspaceSeed, planMemoryMigration, validateMigrationTarget } from './migration-plan.js';
6
- import { MigrationError, MigrationHistorySchema, MigrationProcessStoppedReceiptSchema, MigrationStateSchema } from './migration-schema.js';
7
- import { inspectManagedDetailFilesWithMappings } from './inventory-v2.js';
8
- import { assertMigrationStateIntegrity as assertIdentity, encodeMigrationData as encode, migrationOperationRoot as opRoot, migrationPlanDigest as planDigest, sameMigrationData as same, verifyMigrationIntent as verifyIntent, } from './migration-state-invariants.js';
9
- import { detectLocalMemoryStorageVersion, parseFullMemoryIndex } from './storage-format.js';
10
- import { isLocalMemoryInitializationIdentity, localMemoryInitializationIdentity } from './initialization-identity.js';
11
- async function immutable(root, path, data) {
12
- const previous = await readBytes(root, path, 64 * 1024 * 1024);
13
- if (previous) {
14
- if (digest(previous) !== digest(data))
15
- throw new MigrationError('migration_immutable_hash_mismatch', [{ path, reason: 'hash_mismatch' }]);
16
- return;
17
- }
18
- await atomicBytes(root, path, data);
19
- }
20
- async function save(root, state) {
21
- const parsed = MigrationStateSchema.parse(state);
22
- assertIdentity(parsed, parsed.identity);
23
- if (parsed.phase === 'committed' || parsed.phase === 'rolled_back') {
24
- const previous = await readBytes(root, `${opRoot(parsed.identity)}/terminal.json`, 32 * 1024 * 1024);
25
- if (previous && !same(JSON.parse(previous.toString('utf8')), parsed)) {
26
- throw new MigrationError('migration_terminal_conflict');
27
- }
28
- }
29
- await atomicBytes(root, STATE_PATH, encode(parsed));
30
- // State is the commit point. Retain the same terminal fact separately so accidental state loss cannot replay a completed plan.
31
- if (parsed.phase === 'committed' || parsed.phase === 'rolled_back')
32
- await immutable(root, `${opRoot(parsed.identity)}/terminal.json`, encode(parsed));
33
- return parsed;
34
- }
35
- async function readState(root) {
36
- const bytes = await readBytes(root, STATE_PATH, 32 * 1024 * 1024);
37
- if (!bytes)
38
- return null;
39
- let state;
40
- try {
41
- state = MigrationStateSchema.parse(JSON.parse(bytes.toString('utf8')));
42
- }
43
- catch {
44
- throw new MigrationError('migration_state_invalid');
45
- }
46
- return state;
47
- }
48
- async function load(input) {
49
- const state = await readState(input.agentDir);
50
- if (state)
51
- assertIdentity(state, input.identity);
52
- return state;
53
- }
54
- export async function statusMemoryMigration(input) {
55
- const identity = await validateMigrationRoot(input.agentDir, input.identity);
56
- return load({ ...input, identity });
57
- }
58
- export async function prepareMemoryMigration(input, hooks = {}) {
59
- const identity = await validateMigrationRoot(input.agentDir, input.identity);
60
- const history = MigrationHistorySchema.parse(input.serverHistory);
61
- const existing = await readState(input.agentDir);
62
- if (existing?.migrationId === identity.migrationId) {
63
- assertIdentity(existing, identity);
64
- return existing;
65
- }
66
- if (history.kind !== 'none' || history.writeIntentConfirmed === true)
67
- throw new MigrationError(history.kind === 'existing' ? 'migration_recovery_required' : 'migration_server_history_unavailable');
68
- const operations = await readdir(await safePath(input.agentDir, '.crew/memory/migrations')).catch((error) => {
69
- if (error.code === 'ENOENT')
70
- return [];
71
- throw error;
72
- });
73
- await verifyRolledBackPredecessors(input.agentDir, identity, history, operations, existing);
74
- const leftovers = await readBytes(input.agentDir, `${opRoot(identity)}/manifest.json`, 32 * 1024 * 1024);
75
- if (leftovers) {
76
- const prepared = MigrationStateSchema.parse(JSON.parse(leftovers.toString('utf8')));
77
- assertIdentity(prepared, identity);
78
- const intent = await readBytes(input.agentDir, `${opRoot(identity)}/intent.json`);
79
- const current = await classify(input.agentDir, prepared);
80
- if (intent || current.unknown.length || current.classes.some(value => value !== 'B' && value !== 'AB'))
81
- throw new MigrationError('migration_recovery_required');
82
- if (!await copies(input.agentDir, prepared, 'before') || !await copies(input.agentDir, prepared, 'after'))
83
- throw new MigrationError('migration_copy_invalid');
84
- return save(input.agentDir, prepared);
85
- }
86
- const before = await inventory(input.agentDir);
87
- const sourceMemory = before.files.get('MEMORY.md');
88
- if (history.isNewAgent === true && identity.sourceStorageVersion === 0 && before.files.size === 0 && before.directories.length === 0)
89
- throw new MigrationError('migration_workspace_seed_required');
90
- const previousSeed = existing?.origin === 'initialized' && existing.phase === 'rolled_back'
91
- ? existing.manifest.files.find(file => file.path === 'MEMORY.md')?.before : undefined;
92
- const retryingOriginalSeed = previousSeed?.exists === true && sourceMemory !== undefined && digest(sourceMemory) === previousSeed.sha256;
93
- const expectedInitializationIdentity = await localMemoryInitializationIdentity(input.agentDir, identity.agentId, {
94
- workspaceId: identity.workspaceId,
95
- machineId: identity.machineId,
96
- });
97
- const onlyCurrentOperationResidue = isLocalMemoryInitializationIdentity(identity, expectedInitializationIdentity)
98
- && existing === null && operations.length === 1
99
- && operations[0] === identity.migrationId;
100
- const pristineSeed = (history.isNewAgent === true && (operations.length === 0 || onlyCurrentOperationResidue) || retryingOriginalSeed) && identity.sourceStorageVersion === 0
101
- && before.files.size === 1 && sourceMemory !== undefined && before.directories.every(path => path === 'notes' || path === '.crew/memory/pending')
102
- ? initializeExactWorkspaceSeed(sourceMemory) : null;
103
- const initialized = history.isNewAgent === true && identity.sourceStorageVersion === 0
104
- && pristineSeed !== null;
105
- const after = pristineSeed !== null ? { files: new Map([['MEMORY.md', pristineSeed]]), recordMappings: [] } : planMemoryMigration(identity, before.files, input.recordMappings);
106
- if (pristineSeed !== null)
107
- validateMigrationTarget(after.files);
108
- const paths = [...new Set([...before.files.keys(), ...after.files.keys()])].sort();
109
- const manifest = { directories: before.directories, recordMappings: after.recordMappings, files: paths.map(path => {
110
- const b = before.files.get(path);
111
- const a = after.files.get(path);
112
- return { path, before: b ? { exists: true, sha256: digest(b), ref: `${opRoot(identity)}/backup/${digest(b)}`, bytes: b.length } : { exists: false },
113
- after: a ? { exists: true, sha256: digest(a), ref: `${opRoot(identity)}/staged/${digest(a)}`, bytes: a.length } : { exists: false } };
114
- }) };
115
- const sourceDigest = digest(encode({ directories: before.directories, files: manifest.files.map(file => ({ path: file.path, before: file.before.exists ? { exists: true, sha256: file.before.sha256 } : { exists: false } })) }));
116
- const state = { schemaVersion: 1, identity, storageVersion: identity.sourceStorageVersion, targetStorageVersion: 2, origin: initialized ? 'initialized' : 'migrated', migrationId: identity.migrationId, phase: 'prepared', sourceDigest,
117
- planDigest: planDigest(identity, sourceDigest, manifest), manifest, issues: [] };
118
- MigrationStateSchema.parse(state);
119
- const beforeBytes = [...before.files.values()].reduce((sum, value) => sum + value.length, 0);
120
- const afterBytes = [...after.files.values()].reduce((sum, value) => sum + value.length, 0);
121
- const max = Math.max(0, ...[...before.files.values(), ...after.files.values()].map(value => value.length));
122
- const available = hooks.availableBytes ? await hooks.availableBytes() : await statfs(input.agentDir).then(value => value.bavail * value.bsize);
123
- if (available < beforeBytes + afterBytes + max + encode(state).length * 3)
124
- throw new MigrationError('migration_insufficient_disk_space');
125
- for (const file of manifest.files) {
126
- if (file.before.exists)
127
- await immutable(input.agentDir, file.before.ref, before.files.get(file.path));
128
- if (file.after.exists)
129
- await immutable(input.agentDir, file.after.ref, after.files.get(file.path));
130
- }
131
- const current = await inventory(input.agentDir);
132
- if (!same([...current.files].map(([path, bytes]) => [path, digest(bytes)]), [...before.files].map(([path, bytes]) => [path, digest(bytes)])) || !same(current.directories, before.directories))
133
- throw new MigrationError('migration_source_changed');
134
- await immutable(input.agentDir, `${opRoot(identity)}/manifest.json`, encode(state));
135
- return save(input.agentDir, state);
136
- }
137
- /** 新操作只接纳服务端明确授权、摘要相符的回退终态;旧备份和历史保持不可变。 */
138
- async function verifyRolledBackPredecessors(root, identity, history, operations, existing) {
139
- const authorized = history.rolledBackOperations ?? [];
140
- const allowed = new Map(authorized.map(operation => [operation.migrationId, operation]));
141
- if (allowed.size !== authorized.length || allowed.has(identity.migrationId)
142
- || authorized.length > 0 && history.migrationId !== identity.migrationId
143
- || operations.some(name => name !== identity.migrationId && !allowed.has(name))
144
- || authorized.some(operation => !operations.includes(operation.migrationId))
145
- || existing && !allowed.has(existing.migrationId))
146
- throw new MigrationError('migration_operation_residue');
147
- for (const previous of authorized) {
148
- const bytes = await readBytes(root, `.crew/memory/migrations/${previous.migrationId}/terminal.json`, 32 * 1024 * 1024);
149
- if (!bytes)
150
- throw new MigrationError('migration_rollback_history_invalid');
151
- const terminal = MigrationStateSchema.parse(JSON.parse(bytes.toString('utf8')));
152
- assertIdentity(terminal, terminal.identity);
153
- if (terminal.phase !== 'rolled_back' || terminal.migrationId !== previous.migrationId
154
- || terminal.sourceDigest !== previous.sourceDigest || terminal.planDigest !== previous.planDigest
155
- || terminal.storageVersion !== terminal.identity.sourceStorageVersion
156
- || ['workspaceId', 'agentId', 'rootFingerprint', 'ownerIdentity', 'machineId'].some(key => terminal.identity[key] !== identity[key])
157
- || existing?.migrationId === terminal.migrationId && !same(existing, terminal))
158
- throw new MigrationError('migration_rollback_history_invalid');
159
- const manifest = await readBytes(root, `${opRoot(terminal.identity)}/manifest.json`, 32 * 1024 * 1024);
160
- if (!manifest)
161
- throw new MigrationError('migration_rollback_history_invalid');
162
- const prepared = MigrationStateSchema.parse(JSON.parse(manifest.toString('utf8')));
163
- assertIdentity(prepared, terminal.identity);
164
- if (prepared.planDigest !== terminal.planDigest || !await copies(root, terminal, 'before'))
165
- throw new MigrationError('migration_rollback_history_invalid');
166
- }
167
- }
168
- function matches(bytes, image) { return image.exists ? bytes !== undefined && digest(bytes) === image.sha256 : bytes === undefined; }
169
- function publicationPath(state, path, sha256) {
170
- return `${opRoot(state.identity)}/publish/${digest(path)}-${sha256}.tmp`;
171
- }
172
- async function validatePublicationCopies(root, state) {
173
- const directory = `${opRoot(state.identity)}/publish`;
174
- const allowed = new Map();
175
- for (const file of state.manifest.files)
176
- for (const image of [file.before, file.after]) {
177
- if (image.exists)
178
- allowed.set(publicationPath(state, file.path, image.sha256), image.sha256);
179
- }
180
- const stream = await opendir(await safePath(root, directory)).catch((error) => {
181
- if (error.code === 'ENOENT')
182
- return null;
183
- throw error;
184
- });
185
- if (!stream)
186
- return;
187
- for await (const entry of stream) {
188
- const path = `${directory}/${entry.name}`;
189
- const expected = allowed.get(path);
190
- if (!expected || !entry.isFile())
191
- throw new MigrationError('migration_publication_copy_unknown', [{ path, reason: 'unrecognized_publication_copy' }]);
192
- const content = await readBytes(root, path);
193
- if (!content || digest(content) !== expected)
194
- throw new MigrationError('migration_publication_copy_unknown', [{ path, reason: 'publication_hash_mismatch' }]);
195
- }
196
- }
197
- async function classify(root, state) {
198
- await validatePublicationCopies(root, state);
199
- const current = await inventory(root);
200
- const known = new Set(state.manifest.files.map(file => file.path));
201
- const allowedDirs = new Set(state.manifest.directories);
202
- for (const file of state.manifest.files)
203
- if (file.after.exists) {
204
- let dir = dirname(file.path);
205
- while (dir !== '.' && dir !== '.crew' && dir !== '.crew/memory') {
206
- allowedDirs.add(dir);
207
- dir = dirname(dir);
208
- }
209
- }
210
- const unknown = [...current.files.keys()].filter(path => !known.has(path));
211
- unknown.push(...current.directories.filter(path => !allowedDirs.has(path)), ...state.manifest.directories.filter(path => !current.directories.includes(path)));
212
- return { files: current.files, unknown, classes: state.manifest.files.map(file => { const b = matches(current.files.get(file.path), file.before), a = matches(current.files.get(file.path), file.after); return a && b ? 'AB' : a ? 'A' : b ? 'B' : 'X'; }) };
213
- }
214
- async function copies(root, state, side) {
215
- const files = new Map();
216
- try {
217
- for (const file of state.manifest.files) {
218
- const image = file[side];
219
- if (image.exists) {
220
- const bytes = await readBytes(root, image.ref);
221
- if (!bytes || bytes.length !== image.bytes || digest(bytes) !== image.sha256)
222
- return null;
223
- files.set(file.path, bytes);
224
- }
225
- }
226
- }
227
- catch {
228
- return null;
229
- }
230
- return files;
231
- }
232
- async function repair(root, state, paths, reason) { return save(root, { ...state, phase: 'repair_required', issues: paths.map(path => ({ path, reason })) }); }
233
- async function publish(root, state, side, hooks) {
234
- let index = 0;
235
- for (const file of state.manifest.files) {
236
- const current = await readBytes(root, file.path);
237
- const bytes = current ?? undefined;
238
- if (matches(bytes, file[side]))
239
- continue;
240
- const opposite = file[side === 'after' ? 'before' : 'after'];
241
- if (!matches(bytes, opposite))
242
- throw new MigrationError('migration_unknown_change', [{ path: file.path, reason: 'changed_before_replace' }]);
243
- const image = file[side];
244
- if (image.exists) {
245
- const data = await readBytes(root, image.ref);
246
- if (!data || digest(data) !== image.sha256)
247
- throw new MigrationError('migration_copy_invalid');
248
- const temporaryPath = publicationPath(state, file.path, image.sha256);
249
- await atomicBytes(root, file.path, data, { temporaryPath, beforeRename: () => hooks.beforeReplace?.(file.path, temporaryPath, index),
250
- afterWriteChunk: (writtenPath, written, total) => hooks.afterPublicationWrite?.(file.path, writtenPath, written, total) });
251
- }
252
- else {
253
- const target = await safePath(root, file.path);
254
- await rm(target);
255
- await syncDirectory(dirname(target));
256
- }
257
- await hooks.afterReplace?.(file.path, index++);
258
- }
259
- }
260
- function validTarget(files, mappings = [], sourceHashes = new Map()) {
261
- try {
262
- validateMigrationTarget(files);
263
- const memory = files.get('MEMORY.md');
264
- if (memory === undefined)
265
- return false;
266
- const entries = [...parseFullMemoryIndex(memory.toString('utf8'))];
267
- const sources = [...files]
268
- .filter(([path]) => path === 'MEMORY.md' || path.startsWith('notes/'))
269
- .map(([path, bytes]) => ({ path, bytes: bytes.length, sha256: digest(bytes), content: bytes.toString('utf8') }));
270
- const inspection = inspectManagedDetailFilesWithMappings(entries, sources, mappings, sourceHashes);
271
- return inspection.fileIssues.length === 0 && inspection.entryIssues.size === 0;
272
- }
273
- catch {
274
- return false;
275
- }
276
- }
277
- function manifestSourceHashes(manifest) {
278
- return new Map(manifest.files
279
- .flatMap(file => file.before.exists ? [[file.path, file.before.sha256]] : []));
280
- }
281
- async function finalState(input, state) {
282
- const terminalPath = `${opRoot(state.identity)}/terminal.json`;
283
- const bytes = await readBytes(input.agentDir, terminalPath, 32 * 1024 * 1024);
284
- let terminal = null;
285
- if (bytes) {
286
- terminal = MigrationStateSchema.parse(JSON.parse(bytes.toString('utf8')));
287
- assertIdentity(terminal, input.identity);
288
- if ((terminal.phase !== 'committed' && terminal.phase !== 'rolled_back') || terminal.planDigest !== state.planDigest)
289
- throw new MigrationError('migration_terminal_invalid');
290
- }
291
- else if (state.phase === 'committed' || state.phase === 'rolled_back') {
292
- // Persist the already-published commit point before any content-health update can replace the mutable state.
293
- await immutable(input.agentDir, terminalPath, encode(state));
294
- terminal = state;
295
- }
296
- if (terminal?.phase === 'committed') {
297
- let valid = false;
298
- try {
299
- valid = validTarget((await inventory(input.agentDir)).files, state.manifest.recordMappings, manifestSourceHashes(state.manifest));
300
- }
301
- catch { /* Protect damaged content without erasing commit history. */ }
302
- if (!valid)
303
- return repair(input.agentDir, terminal, ['MEMORY.md'], 'committed_target_invalid');
304
- return same(state, terminal) ? terminal : save(input.agentDir, terminal);
305
- }
306
- if (terminal?.phase === 'rolled_back')
307
- return same(state, terminal) ? terminal : save(input.agentDir, terminal);
308
- return null;
309
- }
310
- export async function applyMemoryMigration(input, hooks = {}) {
311
- await validateMigrationRoot(input.agentDir, input.identity);
312
- let state = await load(input);
313
- if (!state)
314
- throw new MigrationError('migration_state_missing');
315
- const done = await finalState(input, state);
316
- if (done)
317
- return done;
318
- if (state.phase !== 'prepared')
319
- throw new MigrationError('migration_recovery_required');
320
- const receipt = verifyIntent(input.intentReceipt, state);
321
- const classification = await classify(input.agentDir, state);
322
- if (classification.unknown.length || classification.classes.some(value => value !== 'B' && value !== 'AB'))
323
- return repair(input.agentDir, state, classification.unknown.length ? classification.unknown : ['MEMORY.md'], 'source_changed');
324
- const backup = await copies(input.agentDir, state, 'before'), staged = await copies(input.agentDir, state, 'after');
325
- if (!backup || !staged || !validTarget(staged, state.manifest.recordMappings, manifestSourceHashes(state.manifest)))
326
- throw new MigrationError('migration_copy_invalid');
327
- await immutable(input.agentDir, `${opRoot(state.identity)}/intent.json`, encode(receipt));
328
- state = await save(input.agentDir, { ...state, phase: 'applying', intentReceipt: receipt });
329
- await publish(input.agentDir, state, 'after', hooks);
330
- const current = await classify(input.agentDir, state);
331
- if (current.unknown.length || current.classes.some(value => value === 'X' || value === 'B') || !validTarget(current.files, state.manifest.recordMappings, manifestSourceHashes(state.manifest)))
332
- return repair(input.agentDir, state, ['MEMORY.md'], 'target_validation_failed');
333
- return save(input.agentDir, { ...state, phase: 'committed', storageVersion: 2 });
334
- }
335
- export async function recoverMemoryMigration(input, hooks = {}) {
336
- await validateMigrationRoot(input.agentDir, input.identity);
337
- let state = await readState(input.agentDir);
338
- if (state && state.migrationId !== input.identity.migrationId) {
339
- const history = MigrationHistorySchema.parse(input.serverHistory);
340
- if (history.kind !== 'existing' || history.migrationId !== input.identity.migrationId)
341
- throw new MigrationError('migration_history_missing_or_mismatch');
342
- const operations = await readdir(await safePath(input.agentDir, '.crew/memory/migrations'));
343
- await verifyRolledBackPredecessors(input.agentDir, input.identity, history, operations, state);
344
- state = null;
345
- }
346
- if (state)
347
- assertIdentity(state, input.identity);
348
- if (!state) {
349
- const history = MigrationHistorySchema.parse(input.serverHistory);
350
- if (history.kind !== 'existing' || history.migrationId !== input.identity.migrationId)
351
- throw new MigrationError('migration_history_missing_or_mismatch');
352
- const terminal = await readBytes(input.agentDir, `${opRoot(input.identity)}/terminal.json`, 32 * 1024 * 1024);
353
- if (terminal) {
354
- const terminalState = MigrationStateSchema.parse(JSON.parse(terminal.toString('utf8')));
355
- assertIdentity(terminalState, input.identity);
356
- if (terminalState.phase !== 'committed' && terminalState.phase !== 'rolled_back')
357
- throw new MigrationError('migration_terminal_invalid');
358
- const final = await finalState(input, terminalState);
359
- if (final)
360
- return save(input.agentDir, final);
361
- }
362
- const bytes = await readBytes(input.agentDir, `${opRoot(input.identity)}/manifest.json`, 32 * 1024 * 1024);
363
- if (!bytes)
364
- throw new MigrationError('migration_manifest_missing_repair_required');
365
- state = MigrationStateSchema.parse(JSON.parse(bytes.toString('utf8')));
366
- assertIdentity(state, input.identity);
367
- const receipt = await readBytes(input.agentDir, `${opRoot(input.identity)}/intent.json`);
368
- if (receipt)
369
- state = { ...state, phase: 'applying', intentReceipt: verifyIntent(JSON.parse(receipt.toString('utf8')), state) };
370
- else if (history.writeIntentConfirmed && !input.intentReceipt)
371
- throw new MigrationError('migration_intent_receipt_required');
372
- }
373
- const done = await finalState(input, state);
374
- if (done)
375
- return done;
376
- const stopped = MigrationProcessStoppedReceiptSchema.parse(input.stoppedReceipt);
377
- if (stopped.migrationId !== state.migrationId || stopped.machineId !== state.identity.machineId || stopped.executionId !== state.identity.executionId || stopped.ownerIdentity !== state.identity.ownerIdentity
378
- || (stopped.proof === 'not_started' && (state.phase !== 'prepared' || state.intentReceipt || input.intentReceipt)))
379
- throw new MigrationError('migration_stop_proof_invalid');
380
- const cancelled = z.boolean().parse(input.cancelled);
381
- let receipt = state.intentReceipt;
382
- if (input.intentReceipt)
383
- receipt = verifyIntent(input.intentReceipt, state);
384
- let current;
385
- try {
386
- current = await classify(input.agentDir, state);
387
- }
388
- catch {
389
- return repair(input.agentDir, state, ['MEMORY.md'], 'unsafe_or_unreadable_source');
390
- }
391
- if (current.unknown.length || current.classes.includes('X'))
392
- return repair(input.agentDir, state, [...current.unknown, ...state.manifest.files.filter((_, index) => current.classes[index] === 'X').map(file => file.path)], 'unknown_change');
393
- const allA = current.classes.every(value => value === 'A' || value === 'AB'), allB = current.classes.every(value => value === 'B' || value === 'AB');
394
- if (allA && !cancelled && validTarget(current.files, state.manifest.recordMappings, manifestSourceHashes(state.manifest)))
395
- return save(input.agentDir, { ...state, phase: 'committed', storageVersion: 2, ...(receipt ? { intentReceipt: receipt } : {}) });
396
- if (allB && (!receipt || cancelled))
397
- return save(input.agentDir, { ...state, phase: 'rolled_back', storageVersion: state.identity.sourceStorageVersion });
398
- const staged = await copies(input.agentDir, state, 'after');
399
- if (receipt && !cancelled && staged && validTarget(staged, state.manifest.recordMappings, manifestSourceHashes(state.manifest))) {
400
- state = await save(input.agentDir, { ...state, phase: 'applying', intentReceipt: receipt });
401
- await publish(input.agentDir, state, 'after', hooks);
402
- const target = await classify(input.agentDir, state);
403
- if (!target.unknown.length && target.classes.every(value => value === 'A' || value === 'AB') && validTarget(target.files, state.manifest.recordMappings, manifestSourceHashes(state.manifest)))
404
- return save(input.agentDir, { ...state, phase: 'committed', storageVersion: 2 });
405
- }
406
- const backup = await copies(input.agentDir, state, 'before');
407
- if (!backup || detectLocalMemoryStorageVersion(backup.get('MEMORY.md')?.toString('utf8') ?? '') !== state.identity.sourceStorageVersion)
408
- return repair(input.agentDir, state, ['MEMORY.md'], 'backup_unverifiable');
409
- await publish(input.agentDir, state, 'before', hooks);
410
- const restored = await classify(input.agentDir, state);
411
- if (restored.unknown.length || !restored.classes.every(value => value === 'B' || value === 'AB'))
412
- return repair(input.agentDir, state, ['MEMORY.md'], 'rollback_validation_failed');
413
- return save(input.agentDir, { ...state, phase: 'rolled_back', storageVersion: state.identity.sourceStorageVersion });
414
- }
@@ -1,21 +0,0 @@
1
- import { runMemoryMigrationCommand, MigrationCommandSchema } from "./migration-cli.js";
2
- import { withMemoryAccess } from "./migration-access.js";
3
- async function main() {
4
- const chunks = [];
5
- let bytes = 0;
6
- for await (const chunk of process.stdin) {
7
- const part = Buffer.from(chunk);
8
- bytes += part.length;
9
- if (bytes > 8 * 1024 * 1024)
10
- throw new Error("migration_input_limit");
11
- chunks.push(part);
12
- }
13
- const input = MigrationCommandSchema.parse(JSON.parse(Buffer.concat(chunks).toString("utf8")));
14
- const result = await withMemoryAccess(input.agentDir, () => runMemoryMigrationCommand(input), { maintenance: true });
15
- process.stdout.write(JSON.stringify(result) + "\n");
16
- process.exitCode = result.result === "success" || result.result === "missing" ? 0 : 1;
17
- }
18
- void main().catch(() => {
19
- process.stdout.write(JSON.stringify({ result: "failed", issues: [{ path: "", reason: "migration_tool_failed" }] }) + "\n");
20
- process.exitCode = 1;
21
- });
@@ -1,152 +0,0 @@
1
- import { LOCAL_MEMORY_INDEX_CODE_POINT_CAP, LOCAL_MEMORY_INDEX_ENTRY_CAP, LOCAL_MEMORY_TOTAL_CODE_POINT_CAP, renderGeneratedIndex, renderLocalMemoryIndexCard, selectLocalMemoryIndexEntries, } from "./index-budget.js";
2
- export const LOCAL_MEMORY_STORAGE_BYTE_CAP = 16 * 1024 * 1024;
3
- export const LOCAL_MEMORY_STORAGE_ENTRY_CAP = 10_000;
4
- export const LOCAL_MEMORY_NOTE_BYTE_CAP = 32 * 1024 * 1024;
5
- export const LOCAL_MEMORY_READ_BYTE_CAP = 64 * 1024 * 1024;
6
- export const LOCAL_MEMORY_GENERATED_INDEX_PATTERN = /<!--\s*NOWWORK:MEMORY_INDEX:BEGIN\s+schema=[12]\s*-->[\s\S]*?<!--\s*NOWWORK:MEMORY_INDEX:END\s*-->/u;
7
- const length = (text) => {
8
- let count = 0;
9
- for (const _character of text)
10
- count += 1;
11
- return count;
12
- };
13
- const TYPES = new Set(["procedural", "semantic", "episodic", "preference"]);
14
- const STATUSES = new Set(["candidate", "active", "pinned", "stale", "superseded", "archived"]);
15
- const PRIORITIES = new Set(["high", "medium", "low"]);
16
- const FIELDS = new Set(["id", "file", "type", "status", "summary", "triggers", "priority", "revision", "anchor", "updated", "review_after"]);
17
- /** A malformed/unknown marker is never treated as an unmarked legacy file. */
18
- export function detectLocalMemoryStorageVersion(memory) {
19
- const markerCount = (memory.match(/NOWWORK:MEMORY_INDEX:/gu) ?? []).length;
20
- if (markerCount === 0)
21
- return 0;
22
- const block = LOCAL_MEMORY_GENERATED_INDEX_PATTERN.exec(memory);
23
- if (markerCount !== 2 || block?.index === undefined)
24
- throw new Error("local_memory_storage_version_invalid");
25
- const headings = [...memory.matchAll(/^##[\t ]+Knowledge Index[\t ]*\r?$/gmu)];
26
- const heading = headings[0];
27
- if (headings.length !== 1 || heading?.index === undefined)
28
- throw new Error("local_memory_storage_index_invalid");
29
- const sectionStart = heading.index + heading[0].length;
30
- const nextSection = /^##[\t ]+(?!#)/mu.exec(memory.slice(sectionStart));
31
- const sectionEnd = nextSection?.index === undefined ? memory.length : sectionStart + nextSection.index;
32
- if (block.index < sectionStart || block.index + block[0].length > sectionEnd) {
33
- throw new Error("local_memory_storage_index_invalid");
34
- }
35
- return /schema=2\s*-->/u.test(block[0].slice(0, block[0].indexOf("\n") + 1) || block[0]) ? 2 : 1;
36
- }
37
- function validateEntry(entry) {
38
- const singleLine = (value) => !/[\r\n\u0000-\u001f\u007f]/u.test(value)
39
- && !/<!--/u.test(value);
40
- if (!entry.title.trim() || !entry.summary.trim()
41
- || ![entry.title, entry.summary, entry.updated, ...entry.triggers].every(singleLine)
42
- || !/^[A-Za-z0-9_-]+$/u.test(entry.id) || entry.anchor !== entry.id
43
- || !/^notes\/(?:preferences|project-rules|testing|debugging|git|incidents|lessons)\.md$/u.test(entry.file)
44
- || !TYPES.has(entry.type) || !STATUSES.has(entry.status) || !PRIORITIES.has(entry.priority)
45
- || !Number.isSafeInteger(entry.revision) || entry.revision < 1
46
- || !entry.updated.trim()
47
- || (entry.reviewAfter !== null && !singleLine(entry.reviewAfter))) {
48
- throw new Error("local_memory_storage_entry_invalid");
49
- }
50
- }
51
- function validateEntries(entries) {
52
- if (entries.length > LOCAL_MEMORY_STORAGE_ENTRY_CAP)
53
- throw new Error("local_memory_storage_entry_limit");
54
- const ids = new Set();
55
- for (const entry of entries) {
56
- validateEntry(entry);
57
- if (ids.has(entry.id))
58
- throw new Error("local_memory_storage_duplicate_id");
59
- ids.add(entry.id);
60
- }
61
- }
62
- export function assertLocalMemoryStorageSize(memory) {
63
- if (Buffer.byteLength(memory, "utf8") > LOCAL_MEMORY_STORAGE_BYTE_CAP)
64
- throw new Error("local_memory_storage_byte_limit");
65
- }
66
- /** schema=2 stores every lifecycle state, unlike the schema=1 prompt projection. */
67
- export function parseFullMemoryIndex(memory) {
68
- assertLocalMemoryStorageSize(memory);
69
- if (detectLocalMemoryStorageVersion(memory) !== 2)
70
- throw new Error("local_memory_storage_version_invalid");
71
- const block = LOCAL_MEMORY_GENERATED_INDEX_PATTERN.exec(memory)[0];
72
- const body = block.replace(/^<!--[^>]*-->\s*/u, "").replace(/<!--[^>]*-->$/u, "");
73
- const headings = [...body.matchAll(/^###[\t ]+(.+?)[\t ]*\r?$/gmu)];
74
- if (headings.length > LOCAL_MEMORY_STORAGE_ENTRY_CAP)
75
- throw new Error("local_memory_storage_entry_limit");
76
- if (body.slice(0, headings[0]?.index ?? body.length).trim())
77
- throw new Error("local_memory_storage_entry_invalid");
78
- const entries = headings.map((heading, index) => {
79
- const fields = {};
80
- const tail = body.slice(heading.index + heading[0].length, headings[index + 1]?.index ?? body.length);
81
- for (const line of tail.split(/\r?\n/u)) {
82
- if (!line.trim())
83
- continue;
84
- const field = /^-\s+([a-z][a-z_]*)\s*:\s*(.*?)\s*$/u.exec(line);
85
- if (!field || !FIELDS.has(field[1]) || fields[field[1]] !== undefined)
86
- throw new Error("local_memory_storage_entry_invalid");
87
- fields[field[1]] = field[2];
88
- }
89
- if ([...FIELDS].filter(field => field !== "review_after").some(field => fields[field] === undefined)) {
90
- throw new Error("local_memory_storage_entry_invalid");
91
- }
92
- if (!/^[1-9][0-9]*$/u.test(fields.revision))
93
- throw new Error("local_memory_storage_entry_invalid");
94
- return {
95
- id: fields.id, title: heading[1], file: fields.file, type: fields.type,
96
- status: fields.status, summary: fields.summary,
97
- triggers: fields.triggers.split(/[,,、]/u).map(value => value.trim()).filter(Boolean),
98
- priority: fields.priority, revision: Number(fields.revision),
99
- anchor: fields.anchor, updated: fields.updated, reviewAfter: fields.review_after ?? null,
100
- };
101
- });
102
- validateEntries(entries);
103
- const section = memory.slice(memory.indexOf("## Knowledge Index") + "## Knowledge Index".length);
104
- const remaining = section.replace(LOCAL_MEMORY_GENERATED_INDEX_PATTERN, "").split(/^##[\t ]+(?!#)/mu)[0] ?? "";
105
- if (/^###[\t ]+/mu.test(remaining.replace(/<!--[\s\S]*?-->/gu, "")))
106
- throw new Error("local_memory_legacy_index_requires_migration");
107
- return entries;
108
- }
109
- export function renderFullMemoryIndex(entries) {
110
- validateEntries(entries);
111
- const block = `<!-- NOWWORK:MEMORY_INDEX:BEGIN schema=2 -->\n${entries.map(renderLocalMemoryIndexCard).join("\n")}<!-- NOWWORK:MEMORY_INDEX:END -->`;
112
- assertLocalMemoryStorageSize(block);
113
- return block;
114
- }
115
- export function replaceFullMemoryIndex(memory, entries) {
116
- if (detectLocalMemoryStorageVersion(memory) !== 2)
117
- throw new Error("local_memory_storage_version_invalid");
118
- const updated = memory.replace(LOCAL_MEMORY_GENERATED_INDEX_PATTERN, () => renderFullMemoryIndex(entries));
119
- assertLocalMemoryStorageSize(updated);
120
- return updated;
121
- }
122
- export function selectLocalMemoryPreview(memory, query = "", options = {}) {
123
- const entries = parseFullMemoryIndex(memory);
124
- const totalActiveEntries = entries.filter(entry => entry.status === "active" || entry.status === "pinned").length;
125
- const emptyIndex = renderGeneratedIndex([]);
126
- // Reserve the largest count label before selecting; all discovery text counts toward the same cap.
127
- const hint = (shown) => `\nMemory preview: ${shown}/${totalActiveEntries} active entries. Use crew-memory search --query <text> and crew-memory read --file <file> for more.\n`;
128
- const base = memory.replace(LOCAL_MEMORY_GENERATED_INDEX_PATTERN, () => `${emptyIndex}${hint(totalActiveEntries)}`);
129
- const remaining = Math.min(LOCAL_MEMORY_TOTAL_CODE_POINT_CAP, options.maxCodePoints ?? Infinity) - length(base);
130
- const remainingBytes = (options.maxBytes ?? Infinity) - Buffer.byteLength(base, "utf8");
131
- if (remaining < 0 || remainingBytes < 0 || Number.isNaN(remaining) || Number.isNaN(remainingBytes)) {
132
- throw new Error("local_memory_identity_budget_exceeded");
133
- }
134
- const terms = query.normalize("NFKC").toLowerCase().split(/\s+/u).filter(Boolean);
135
- const selection = selectLocalMemoryIndexEntries(entries, {
136
- entryCap: LOCAL_MEMORY_INDEX_ENTRY_CAP,
137
- indexCodePointCap: Math.min(LOCAL_MEMORY_INDEX_CODE_POINT_CAP - length(emptyIndex), remaining),
138
- indexByteCap: remainingBytes,
139
- }, {
140
- protectedIds: new Set(entries.filter(entry => entry.status === "pinned").map(entry => entry.id)),
141
- relevance: entry => {
142
- const searchable = `${entry.title} ${entry.summary} ${entry.triggers.join(" ")}`.normalize("NFKC").toLowerCase();
143
- return terms.filter(term => searchable.includes(term)).length;
144
- },
145
- });
146
- if (selection.reason === "index_over_budget_pinned")
147
- throw new Error("local_memory_pinned_budget_exceeded");
148
- return {
149
- ...selection, totalActiveEntries,
150
- memory: memory.replace(LOCAL_MEMORY_GENERATED_INDEX_PATTERN, () => `${renderGeneratedIndex(selection.entries)}${hint(selection.entries.length)}`),
151
- };
152
- }
@@ -1,13 +0,0 @@
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
- }