@nowcrew/daemon 0.6.93-beta.10 → 0.6.93-beta.12

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.
@@ -46,7 +46,10 @@ export async function runMemoryInventoryCommand(args, io) {
46
46
  : v2?.analysis ?? previewMemoryInventory(scan.sources, scanComplete);
47
47
  const complete = scanComplete && !analysis.issues.some((issue) => issue.code === "missing_detail");
48
48
  const issues = [...scan.issues, ...analysis.issues];
49
- const referenced = new Set(analysis.entries.map((entry) => entry.sourceFields.file));
49
+ const referenced = new Set([
50
+ ...analysis.entries.map((entry) => entry.sourceFields.file),
51
+ ...(v2?.preservedSourcePaths ?? []),
52
+ ]);
50
53
  const reportSources = v2?.stateSource ? [...scan.sources, v2.stateSource] : scan.sources;
51
54
  const files = reportSources.map(({ path, bytes, sha256 }) => ({ path, bytes, sha256 }));
52
55
  const orphanNotes = files.filter((file) => file.path.startsWith("notes/") && !referenced.has(file.path)).map((file) => file.path);
@@ -4,6 +4,7 @@ import { renderLocalMemoryIndexCard } from "./index-budget.js";
4
4
  import { inventoryLimits, readMemoryInventorySource } from "./inventory-source.js";
5
5
  import { MigrationStateSchema } from "./migration-schema.js";
6
6
  import { assertMigrationStateIntegrity } from "./migration-state-invariants.js";
7
+ import { isHarmlessMigrationRemainder } from "./migration-plan.js";
7
8
  import { parseFullMemoryIndex, selectLocalMemoryPreview } from "./storage-format.js";
8
9
  const STATE_PATH = ".crew/memory/migration.v1.json";
9
10
  const DETAIL_MARKER_PATTERN = /<!--\s*NOWWORK:MEMORY_ENTRY:(?:BEGIN\s+id=([A-Za-z0-9_-]+)\s+revision=([1-9][0-9]*)|END\s+id=([A-Za-z0-9_-]+))\s*-->/gu;
@@ -28,6 +29,15 @@ function sourceFields(entry) {
28
29
  ...(entry.reviewAfter === null ? {} : { review_after: entry.reviewAfter }),
29
30
  };
30
31
  }
32
+ function exactManagedDetailBodies(detail, targetId) {
33
+ const escapedId = targetId.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
34
+ const pattern = new RegExp(`<!--\\s*NOWWORK:MEMORY_ENTRY:BEGIN\\s+id=${escapedId}\\s+revision=[1-9][0-9]*\\s*-->[\\s\\S]*?<!--\\s*NOWWORK:MEMORY_ENTRY:END\\s+id=${escapedId}\\s*-->`, "gu");
35
+ return [...detail.matchAll(pattern)].map(match => {
36
+ const beginEnd = match[0].indexOf("-->") + 3;
37
+ const endStart = match[0].lastIndexOf("<!--");
38
+ return match[0].slice(beginEnd, endStart);
39
+ });
40
+ }
31
41
  function harmlessDetailRemainder(text) {
32
42
  let cursor = 0;
33
43
  let lineStart = true;
@@ -60,6 +70,63 @@ function harmlessDetailRemainder(text) {
60
70
  }
61
71
  return true;
62
72
  }
73
+ /**
74
+ * A migrated legacy note can remain on disk for external references. It is
75
+ * safe to exclude it from the V2 orphan scan only when the immutable
76
+ * migration mapping proves that the complete, unchanged source was copied to
77
+ * the indexed target entry.
78
+ */
79
+ export function findControlledPreservedSourcePaths(entries, sources, mappings, sourceHashes = new Map()) {
80
+ const entriesById = new Map(entries.map(entry => [entry.id, entry]));
81
+ const sourcesByPath = new Map(sources.map(source => [source.path, source]));
82
+ const grouped = new Map();
83
+ for (const mapping of mappings) {
84
+ if (!mapping.sourceFile.startsWith("notes/"))
85
+ continue;
86
+ const current = grouped.get(mapping.sourceFile) ?? [];
87
+ grouped.set(mapping.sourceFile, [...current, mapping]);
88
+ }
89
+ const preserved = [];
90
+ for (const [path, values] of grouped) {
91
+ const source = sourcesByPath.get(path);
92
+ if (source === undefined || values.length === 0)
93
+ continue;
94
+ const ranges = [...values].sort((left, right) => left.start - right.start);
95
+ let cursor = 0;
96
+ let valid = true;
97
+ for (const mapping of ranges) {
98
+ const targetFile = `notes/${mapping.category}.md`;
99
+ const target = mapping.targetId === undefined ? undefined : entriesById.get(mapping.targetId);
100
+ const targetSource = target === undefined ? undefined : sourcesByPath.get(target.file);
101
+ const expectedSourceSha256 = mapping.sourceSha256 ?? sourceHashes.get(path);
102
+ const sourceBody = source.content.slice(mapping.start, mapping.end);
103
+ const renderedBody = mapping.sourceFile === targetFile
104
+ ? sourceBody
105
+ : `\n## ${mapping.title}\n\nScope: ${mapping.scope}\n\n${sourceBody}\n`;
106
+ const sourceBlockBodies = mapping.targetId === undefined
107
+ ? []
108
+ : exactManagedDetailBodies(sourceBody, mapping.targetId);
109
+ const expectedBody = mapping.sourceFile === targetFile && sourceBlockBodies.length === 1
110
+ ? sourceBlockBodies[0]
111
+ : renderedBody;
112
+ const targetBodies = mapping.targetId === undefined || targetSource === undefined
113
+ ? []
114
+ : exactManagedDetailBodies(targetSource.content, mapping.targetId);
115
+ if (expectedSourceSha256 === undefined || expectedSourceSha256 !== source.sha256
116
+ || mapping.start < cursor || !isHarmlessMigrationRemainder(source.content.slice(cursor, mapping.start))
117
+ || mapping.end > source.content.length
118
+ || target === undefined || target.file !== targetFile || targetSource === undefined
119
+ || targetBodies.length !== 1 || targetBodies[0] !== expectedBody) {
120
+ valid = false;
121
+ break;
122
+ }
123
+ cursor = mapping.end;
124
+ }
125
+ if (valid && isHarmlessMigrationRemainder(source.content.slice(cursor)))
126
+ preserved.push(path);
127
+ }
128
+ return preserved.sort((left, right) => left.localeCompare(right));
129
+ }
63
130
  export function* scanManagedDetailMarkers(detail) {
64
131
  for (const marker of detail.matchAll(DETAIL_MARKER_PATTERN)) {
65
132
  const beginId = marker[1];
@@ -131,7 +198,12 @@ function inspectDetailEntry(entry, analysis) {
131
198
  return revisions[0] > entry.revision ? "managed_revision_invalid" : null;
132
199
  }
133
200
  export function inspectManagedDetailFiles(entries, sources, analyze = analyzeManagedDetailFile) {
201
+ return inspectManagedDetailFilesWithMappings(entries, sources, [], new Map(), analyze);
202
+ }
203
+ export function inspectManagedDetailFilesWithMappings(entries, sources, mappings, sourceHashes = new Map(), analyze = analyzeManagedDetailFile) {
134
204
  const sourceMap = new Map(sources.map(source => [source.path, source]));
205
+ const preservedSourcePaths = findControlledPreservedSourcePaths(entries, sources, mappings, sourceHashes);
206
+ const preserved = new Set(preservedSourcePaths);
135
207
  const expectedByFile = new Map();
136
208
  for (const entry of entries) {
137
209
  const expected = expectedByFile.get(entry.file) ?? new Set();
@@ -150,8 +222,10 @@ export function inspectManagedDetailFiles(entries, sources, analyze = analyzeMan
150
222
  continue;
151
223
  const analysis = analyze(source.content, expectedByFile.get(path) ?? new Set());
152
224
  analyses.set(path, analysis);
153
- if (analysis.coverageIssue !== null)
225
+ if (analysis.coverageIssue !== null
226
+ && !(preserved.has(path) && analysis.coverageIssue === "unindexed_text_requires_mapping")) {
154
227
  fileIssues.push({ code: analysis.coverageIssue, path });
228
+ }
155
229
  }
156
230
  const entryIssues = new Map();
157
231
  for (const entry of entries) {
@@ -159,7 +233,7 @@ export function inspectManagedDetailFiles(entries, sources, analyze = analyzeMan
159
233
  if (issue !== null)
160
234
  entryIssues.set(entry.id, issue);
161
235
  }
162
- return { entryIssues, fileIssues };
236
+ return { entryIssues, fileIssues, preservedSourcePaths };
163
237
  }
164
238
  async function inspectCommittedState(root, budget) {
165
239
  let source;
@@ -170,6 +244,7 @@ async function inspectCommittedState(root, budget) {
170
244
  return {
171
245
  valid: false,
172
246
  source: null,
247
+ state: null,
173
248
  issue: {
174
249
  code: error.code === "ENOENT"
175
250
  ? "memory_migration_state_missing"
@@ -190,14 +265,15 @@ async function inspectCommittedState(root, budget) {
190
265
  assertMigrationStateIntegrity(state, state.identity);
191
266
  const confirmed = await readMemoryInventorySource(root, STATE_PATH, Math.min(inventoryLimits.fileBytes, budget));
192
267
  if (confirmed.sha256 !== source.sha256) {
193
- return { valid: false, source, issue: { code: "source_changed", path: STATE_PATH } };
268
+ return { valid: false, source, state: null, issue: { code: "source_changed", path: STATE_PATH } };
194
269
  }
195
- return { valid: true, source, issue: null };
270
+ return { valid: true, source, state, issue: null };
196
271
  }
197
272
  catch (error) {
198
273
  return {
199
274
  valid: false,
200
275
  source,
276
+ state: null,
201
277
  issue: {
202
278
  code: "memory_migration_state_invalid",
203
279
  path: STATE_PATH,
@@ -225,7 +301,9 @@ export async function inspectV2MemoryInventory(root, sources, readComplete) {
225
301
  const state = await inspectCommittedState(root, Math.max(0, inventoryLimits.totalBytes - scannedBytes));
226
302
  if (state.issue)
227
303
  addIssue(state.issue.code, state.issue.path);
228
- const detailInspection = inspectManagedDetailFiles(entries, sources);
304
+ const sourceHashes = new Map((state.state?.manifest.files ?? [])
305
+ .flatMap(file => file.before.exists ? [[file.path, file.before.sha256]] : []));
306
+ const detailInspection = inspectManagedDetailFilesWithMappings(entries, sources, state.state?.manifest.recordMappings ?? [], sourceHashes);
229
307
  for (const issue of detailInspection.fileIssues)
230
308
  addIssue(issue.code, issue.path);
231
309
  for (const entry of entries) {
@@ -260,6 +338,7 @@ export async function inspectV2MemoryInventory(root, sources, readComplete) {
260
338
  stateValid: state.valid,
261
339
  writeReady,
262
340
  stateSource: state.source,
341
+ preservedSourcePaths: detailInspection.preservedSourcePaths,
263
342
  analysis: {
264
343
  entries: entries.map(entry => {
265
344
  const detail = shownDetails.has(entry.file) ? null : sourceMap.get(entry.file)?.content ?? null;
@@ -6,7 +6,8 @@ import { defaultProcessController } from "../execution-journal.js";
6
6
  import { createHash } from "node:crypto";
7
7
  import { MigrationStateSchema } from "./migration-schema.js";
8
8
  import { detectLocalMemoryStorageVersion, parseFullMemoryIndex } from "./storage-format.js";
9
- import { readMemoryInventorySource, inventoryLimits } from "./inventory-source.js";
9
+ import { inventoryLimits, readMemoryInventorySource, scanMemorySources } from "./inventory-source.js";
10
+ import { inspectV2MemoryInventory } from "./inventory-v2.js";
10
11
  import { MEMORY_STATE_GUARD_CAPABILITY, MEMORY_STORAGE_V2_CAPABILITY } from "../memory-migration-protocol.js";
11
12
  import { isLocalMemoryInitializationIdentity, localMemoryInitializationIdentity } from "./initialization-identity.js";
12
13
  import { assertMigrationStateIntegrity } from "./migration-state-invariants.js";
@@ -59,6 +60,40 @@ export async function assertMemoryWriterAllowed(agentDir, capabilities = []) {
59
60
  throw new Error("memory_v2_writer_capability_missing");
60
61
  }
61
62
  }
63
+ /**
64
+ * Check the committed V2 catalog immediately before a write is persisted.
65
+ * Keep this separate from the boot/state gate so legacy writer-specific
66
+ * validation (size limits, entry limits, note limits) retains its established
67
+ * error precedence.
68
+ */
69
+ export async function assertMemoryInventoryHealth(agentDir) {
70
+ const root = await realpath(agentDir);
71
+ let state;
72
+ try {
73
+ const source = await readMemoryInventorySource(root, ".crew/memory/migration.v1.json", inventoryLimits.fileBytes);
74
+ state = StateGate.parse(JSON.parse(source.content));
75
+ }
76
+ catch (error) {
77
+ if (error.code === "ENOENT")
78
+ return;
79
+ if (error instanceof z.ZodError || error instanceof SyntaxError)
80
+ throw new Error("memory_migration_state_invalid");
81
+ throw error;
82
+ }
83
+ // Legacy/schema1 writers do not need a V2 inventory scan. In particular,
84
+ // a large pending directory must keep the old writer's normal latency and
85
+ // budget behavior.
86
+ if (state.phase !== "committed" || state.storageVersion !== 2)
87
+ return;
88
+ const scan = await scanMemorySources(root, { includeOperationalFiles: false });
89
+ const health = await inspectV2MemoryInventory(root, scan.sources, scan.issues.length === 0);
90
+ const blockingNoteIssues = health.analysis.issues.filter(issue => issue.path.startsWith("notes/"));
91
+ if (scan.issues.length === 0
92
+ && !health.analysis.issues.some(issue => issue.path === "MEMORY.md")
93
+ && blockingNoteIssues.length > 0) {
94
+ throw new Error("memory_migration_inventory_invalid");
95
+ }
96
+ }
62
97
  const StateGate = z.object({
63
98
  schemaVersion: z.literal(1), migrationId: z.string().min(1),
64
99
  phase: z.enum(["prepared", "applying", "committed", "rolled_back", "repair_required"]),
@@ -25,7 +25,7 @@ export function initializeExactWorkspaceSeed(bytes) {
25
25
  const categories = /^(?:preferences|project-rules|testing|debugging|git|incidents|lessons)$/u;
26
26
  const blockPattern = /<!--\s*NOWWORK:MEMORY_ENTRY:BEGIN\s+id=([A-Za-z0-9_-]+)\s+revision=([1-9][0-9]*)\s*-->([\s\S]*?)<!--\s*NOWWORK:MEMORY_ENTRY:END\s+id=\1\s*-->/gu;
27
27
  const controlMarker = /<!--\s*NOWWORK:MEMORY_/u;
28
- const harmless = (text) => !text.replace(/<!--[\s\S]*?-->/gu, '').replace(/^#{1,2}[^\n]*$/gmu, '').trim();
28
+ export const isHarmlessMigrationRemainder = (text) => !text.replace(/<!--[\s\S]*?-->/gu, '').replace(/^#{1,2}[^\n]*$/gmu, '').trim();
29
29
  function migrationSummary(body, title) {
30
30
  const clean = (value) => value.replace(/<!--[\s\S]*?-->/gu, ' ')
31
31
  .replace(/[\u0000-\u001f\u007f]+/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, 180);
@@ -148,7 +148,7 @@ export function planMemoryMigration(identity, files, supplied = []) {
148
148
  cursor = Math.max(cursor, range.end);
149
149
  }
150
150
  remainder += content.slice(cursor);
151
- if (!harmless(remainder))
151
+ if (!isHarmlessMigrationRemainder(remainder))
152
152
  issues.push({ path, reason: 'unindexed_text_requires_mapping' });
153
153
  }
154
154
  for (const mapping of additions) {
@@ -4,8 +4,9 @@ import { z } from 'zod';
4
4
  import { atomicBytes, digest, inventory, readBytes, safePath, STATE_PATH, syncDirectory, validateMigrationRoot } from './migration-io.js';
5
5
  import { initializeExactWorkspaceSeed, planMemoryMigration, validateMigrationTarget } from './migration-plan.js';
6
6
  import { MigrationError, MigrationHistorySchema, MigrationProcessStoppedReceiptSchema, MigrationStateSchema } from './migration-schema.js';
7
+ import { inspectManagedDetailFilesWithMappings } from './inventory-v2.js';
7
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';
8
- import { detectLocalMemoryStorageVersion } from './storage-format.js';
9
+ import { detectLocalMemoryStorageVersion, parseFullMemoryIndex } from './storage-format.js';
9
10
  import { isLocalMemoryInitializationIdentity, localMemoryInitializationIdentity } from './initialization-identity.js';
10
11
  async function immutable(root, path, data) {
11
12
  const previous = await readBytes(root, path, 64 * 1024 * 1024);
@@ -256,15 +257,27 @@ async function publish(root, state, side, hooks) {
256
257
  await hooks.afterReplace?.(file.path, index++);
257
258
  }
258
259
  }
259
- function validTarget(files) {
260
+ function validTarget(files, mappings = [], sourceHashes = new Map()) {
260
261
  try {
261
262
  validateMigrationTarget(files);
262
- return true;
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;
263
272
  }
264
273
  catch {
265
274
  return false;
266
275
  }
267
276
  }
277
+ function manifestSourceHashes(manifest) {
278
+ return new Map(manifest.files
279
+ .flatMap(file => file.before.exists ? [[file.path, file.before.sha256]] : []));
280
+ }
268
281
  async function finalState(input, state) {
269
282
  const terminalPath = `${opRoot(state.identity)}/terminal.json`;
270
283
  const bytes = await readBytes(input.agentDir, terminalPath, 32 * 1024 * 1024);
@@ -283,7 +296,7 @@ async function finalState(input, state) {
283
296
  if (terminal?.phase === 'committed') {
284
297
  let valid = false;
285
298
  try {
286
- valid = validTarget((await inventory(input.agentDir)).files);
299
+ valid = validTarget((await inventory(input.agentDir)).files, state.manifest.recordMappings, manifestSourceHashes(state.manifest));
287
300
  }
288
301
  catch { /* Protect damaged content without erasing commit history. */ }
289
302
  if (!valid)
@@ -309,13 +322,13 @@ export async function applyMemoryMigration(input, hooks = {}) {
309
322
  if (classification.unknown.length || classification.classes.some(value => value !== 'B' && value !== 'AB'))
310
323
  return repair(input.agentDir, state, classification.unknown.length ? classification.unknown : ['MEMORY.md'], 'source_changed');
311
324
  const backup = await copies(input.agentDir, state, 'before'), staged = await copies(input.agentDir, state, 'after');
312
- if (!backup || !staged || !validTarget(staged))
325
+ if (!backup || !staged || !validTarget(staged, state.manifest.recordMappings, manifestSourceHashes(state.manifest)))
313
326
  throw new MigrationError('migration_copy_invalid');
314
327
  await immutable(input.agentDir, `${opRoot(state.identity)}/intent.json`, encode(receipt));
315
328
  state = await save(input.agentDir, { ...state, phase: 'applying', intentReceipt: receipt });
316
329
  await publish(input.agentDir, state, 'after', hooks);
317
330
  const current = await classify(input.agentDir, state);
318
- if (current.unknown.length || current.classes.some(value => value === 'X' || value === 'B') || !validTarget(current.files))
331
+ if (current.unknown.length || current.classes.some(value => value === 'X' || value === 'B') || !validTarget(current.files, state.manifest.recordMappings, manifestSourceHashes(state.manifest)))
319
332
  return repair(input.agentDir, state, ['MEMORY.md'], 'target_validation_failed');
320
333
  return save(input.agentDir, { ...state, phase: 'committed', storageVersion: 2 });
321
334
  }
@@ -378,16 +391,16 @@ export async function recoverMemoryMigration(input, hooks = {}) {
378
391
  if (current.unknown.length || current.classes.includes('X'))
379
392
  return repair(input.agentDir, state, [...current.unknown, ...state.manifest.files.filter((_, index) => current.classes[index] === 'X').map(file => file.path)], 'unknown_change');
380
393
  const allA = current.classes.every(value => value === 'A' || value === 'AB'), allB = current.classes.every(value => value === 'B' || value === 'AB');
381
- if (allA && !cancelled && validTarget(current.files))
394
+ if (allA && !cancelled && validTarget(current.files, state.manifest.recordMappings, manifestSourceHashes(state.manifest)))
382
395
  return save(input.agentDir, { ...state, phase: 'committed', storageVersion: 2, ...(receipt ? { intentReceipt: receipt } : {}) });
383
396
  if (allB && (!receipt || cancelled))
384
397
  return save(input.agentDir, { ...state, phase: 'rolled_back', storageVersion: state.identity.sourceStorageVersion });
385
398
  const staged = await copies(input.agentDir, state, 'after');
386
- if (receipt && !cancelled && staged && validTarget(staged)) {
399
+ if (receipt && !cancelled && staged && validTarget(staged, state.manifest.recordMappings, manifestSourceHashes(state.manifest))) {
387
400
  state = await save(input.agentDir, { ...state, phase: 'applying', intentReceipt: receipt });
388
401
  await publish(input.agentDir, state, 'after', hooks);
389
402
  const target = await classify(input.agentDir, state);
390
- if (!target.unknown.length && target.classes.every(value => value === 'A' || value === 'AB') && validTarget(target.files))
403
+ if (!target.unknown.length && target.classes.every(value => value === 'A' || value === 'AB') && validTarget(target.files, state.manifest.recordMappings, manifestSourceHashes(state.manifest)))
391
404
  return save(input.agentDir, { ...state, phase: 'committed', storageVersion: 2 });
392
405
  }
393
406
  const backup = await copies(input.agentDir, state, 'before');
@@ -10,6 +10,9 @@ import { detectLocalMemoryStorageVersion, parseFullMemoryIndex } from "./storage
10
10
  import { searchFullMemoryIndex } from "./full-index-search.js";
11
11
  import { searchLegacyMemorySources } from "./legacy-adapter.js";
12
12
  import { withMemoryAccess } from "./migration-access.js";
13
+ import { readMemoryInventorySource, scanMemorySources } from "./inventory-source.js";
14
+ import { MigrationStateSchema } from "./migration-schema.js";
15
+ import { findControlledPreservedSourcePaths } from "./inventory-v2.js";
13
16
  const OUTPUT_BYTES = 12 * 1024;
14
17
  // A V2 page may carry coverage metadata for legacy sources. Reserve room for
15
18
  // that metadata so a mixed result never exceeds the command's output limit.
@@ -125,6 +128,22 @@ async function readSource(home, file) {
125
128
  await handle.close();
126
129
  }
127
130
  }
131
+ async function readControlledPreservedSourcePaths(home, memory) {
132
+ try {
133
+ const root = await realpath(home);
134
+ const stateSource = await readMemoryInventorySource(root, ".crew/memory/migration.v1.json", FILE_BYTES);
135
+ const state = MigrationStateSchema.parse(JSON.parse(stateSource.content));
136
+ if (state.phase !== "committed" || state.storageVersion !== 2)
137
+ return new Set();
138
+ const scan = await scanMemorySources(root, { includeOperationalFiles: false });
139
+ const sourceHashes = new Map(state.manifest.files
140
+ .flatMap(file => file.before.exists ? [[file.path, file.before.sha256]] : []));
141
+ return new Set(findControlledPreservedSourcePaths(parseFullMemoryIndex(memory), scan.sources, state.manifest.recordMappings, sourceHashes));
142
+ }
143
+ catch {
144
+ return new Set();
145
+ }
146
+ }
128
147
  function normalizeLimit(requestedLimit, max, safe) {
129
148
  if (requestedLimit <= max)
130
149
  return { limit: requestedLimit };
@@ -256,10 +275,14 @@ async function runMemoryReadUnlocked(argv, io) {
256
275
  ...command, limit: normalized.limit, history: command.command === "search-history",
257
276
  }, FULL_INDEX_RESULT_BYTES);
258
277
  const indexedFiles = new Set(["MEMORY.md", ...parseFullMemoryIndex(source.text).map((match) => match.file)]);
278
+ const preservedSourceFiles = await readControlledPreservedSourcePaths(io.env.CREW_HOME, source.text);
279
+ const legacyExcludedFiles = fileWasExplicit
280
+ ? indexedFiles
281
+ : new Set([...indexedFiles, ...preservedSourceFiles]);
259
282
  const legacy = await searchLegacyMemorySources({ agentDir: io.env.CREW_HOME, query: command.query,
260
283
  limit: normalized.limit, outputBytes: FULL_INDEX_LEGACY_SCAN_BYTES,
261
284
  ...(command.cursor === undefined ? {} : { cursor: command.cursor }),
262
- ...(fileWasExplicit ? { files: [command.file], excludeFiles: indexedFiles } : { excludeFiles: indexedFiles }) });
285
+ ...(fileWasExplicit ? { files: [command.file], excludeFiles: legacyExcludedFiles } : { excludeFiles: legacyExcludedFiles }) });
263
286
  if (result.matches.length === 0 && (command.cursor === undefined || legacy.matchCount > 0)) {
264
287
  if (legacy.unindexedFiles.length > 0 && (legacy.matchCount > 0 || result.matchCount === 0)) {
265
288
  writeResult({ ...legacy, ...(normalized.warning ? { warning: normalized.warning,
@@ -7,7 +7,7 @@ import { LocalMemoryChangeSchema, LocalMemorySingleLineText, validateLocalMemory
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
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";
10
+ import { assertMemoryInventoryHealth, assertMemoryWriterAllowed, withMemoryAccess } from "./migration-access.js";
11
11
  const StatusChangeSchema = z.object({
12
12
  memoryId: LocalMemorySingleLineText,
13
13
  expectedRevision: z.number().int().positive(),
@@ -686,6 +686,7 @@ export async function applyLocalMemoryResult(input) {
686
686
  if (Buffer.byteLength(content, "utf8") > LOCAL_MEMORY_NOTE_BYTE_CAP)
687
687
  throw new Error("local_memory_note_byte_limit");
688
688
  }
689
+ await assertMemoryInventoryHealth(input.agentDir);
689
690
  for (const candidate of pendingCandidates)
690
691
  await persistPendingMemoryCandidate({ agentDir: input.agentDir, candidate });
691
692
  for (const [path, content] of noteUpdates)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.93-beta.10",
3
+ "version": "0.6.93-beta.12",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",