@fgv/ts-agent-memory 5.1.0-49 → 5.1.0-50

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 (164) hide show
  1. package/dist/packlets/index/memoryIndex.js +23 -16
  2. package/dist/packlets/index/memoryIndex.js.map +1 -1
  3. package/dist/packlets/ingest/orchestrator.js +13 -1
  4. package/dist/packlets/ingest/orchestrator.js.map +1 -1
  5. package/dist/packlets/retrieve/linkTraversalRetriever.js +12 -26
  6. package/dist/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  7. package/dist/packlets/retrieve/recencyRetriever.js +7 -7
  8. package/dist/packlets/retrieve/recencyRetriever.js.map +1 -1
  9. package/dist/packlets/retrieve/retriever.js +91 -10
  10. package/dist/packlets/retrieve/retriever.js.map +1 -1
  11. package/dist/packlets/retrieve/semanticRetriever.js +16 -16
  12. package/dist/packlets/retrieve/semanticRetriever.js.map +1 -1
  13. package/dist/packlets/retrieve/structuredFilterRetriever.js +7 -7
  14. package/dist/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  15. package/dist/packlets/retrieve/tagRetriever.js +7 -7
  16. package/dist/packlets/retrieve/tagRetriever.js.map +1 -1
  17. package/dist/packlets/retrieve/temporalRetrievers.js +23 -20
  18. package/dist/packlets/retrieve/temporalRetrievers.js.map +1 -1
  19. package/dist/packlets/store/coverage.js +6 -0
  20. package/dist/packlets/store/coverage.js.map +1 -0
  21. package/dist/packlets/store/fileTreeMemoryStore.js +221 -79
  22. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  23. package/dist/packlets/store/index.js +4 -0
  24. package/dist/packlets/store/index.js.map +1 -1
  25. package/dist/packlets/store/listSelection.js +36 -0
  26. package/dist/packlets/store/listSelection.js.map +1 -0
  27. package/dist/packlets/store/memoryStore.js +6 -0
  28. package/dist/packlets/store/memoryStore.js.map +1 -0
  29. package/dist/packlets/store/reconcile.js +6 -0
  30. package/dist/packlets/store/reconcile.js.map +1 -0
  31. package/dist/packlets/store/storeCoverage.js +102 -0
  32. package/dist/packlets/store/storeCoverage.js.map +1 -0
  33. package/dist/packlets/store/storeReconcile.js +122 -0
  34. package/dist/packlets/store/storeReconcile.js.map +1 -0
  35. package/dist/packlets/store/vectorMaintenance.js +116 -8
  36. package/dist/packlets/store/vectorMaintenance.js.map +1 -1
  37. package/dist/packlets/store/vectorRecordSource.js +44 -0
  38. package/dist/packlets/store/vectorRecordSource.js.map +1 -0
  39. package/dist/packlets/tools/memoryTools.js +25 -2
  40. package/dist/packlets/tools/memoryTools.js.map +1 -1
  41. package/dist/packlets/types/envelope.js +25 -0
  42. package/dist/packlets/types/envelope.js.map +1 -1
  43. package/dist/packlets/types/index.js +1 -0
  44. package/dist/packlets/types/index.js.map +1 -1
  45. package/dist/packlets/types/recordResolver.js +6 -0
  46. package/dist/packlets/types/recordResolver.js.map +1 -0
  47. package/dist/packlets/types/temporal.js.map +1 -1
  48. package/dist/packlets/vector/inMemoryCosineIndex.js +39 -18
  49. package/dist/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  50. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js +67 -12
  51. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  52. package/dist/packlets/vector/rebuildHelpers.js +38 -0
  53. package/dist/packlets/vector/rebuildHelpers.js.map +1 -0
  54. package/dist/packlets/vector/vectorIndex.js.map +1 -1
  55. package/dist/ts-agent-memory.d.ts +1035 -106
  56. package/lib/packlets/index/memoryIndex.d.ts +118 -27
  57. package/lib/packlets/index/memoryIndex.d.ts.map +1 -1
  58. package/lib/packlets/index/memoryIndex.js +23 -16
  59. package/lib/packlets/index/memoryIndex.js.map +1 -1
  60. package/lib/packlets/ingest/orchestrator.d.ts.map +1 -1
  61. package/lib/packlets/ingest/orchestrator.js +13 -1
  62. package/lib/packlets/ingest/orchestrator.js.map +1 -1
  63. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts +3 -10
  64. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts.map +1 -1
  65. package/lib/packlets/retrieve/linkTraversalRetriever.js +11 -25
  66. package/lib/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  67. package/lib/packlets/retrieve/recencyRetriever.d.ts +3 -3
  68. package/lib/packlets/retrieve/recencyRetriever.d.ts.map +1 -1
  69. package/lib/packlets/retrieve/recencyRetriever.js +6 -6
  70. package/lib/packlets/retrieve/recencyRetriever.js.map +1 -1
  71. package/lib/packlets/retrieve/retriever.d.ts +88 -7
  72. package/lib/packlets/retrieve/retriever.d.ts.map +1 -1
  73. package/lib/packlets/retrieve/retriever.js +94 -9
  74. package/lib/packlets/retrieve/retriever.js.map +1 -1
  75. package/lib/packlets/retrieve/semanticRetriever.d.ts +3 -5
  76. package/lib/packlets/retrieve/semanticRetriever.d.ts.map +1 -1
  77. package/lib/packlets/retrieve/semanticRetriever.js +15 -15
  78. package/lib/packlets/retrieve/semanticRetriever.js.map +1 -1
  79. package/lib/packlets/retrieve/structuredFilterRetriever.d.ts +3 -3
  80. package/lib/packlets/retrieve/structuredFilterRetriever.d.ts.map +1 -1
  81. package/lib/packlets/retrieve/structuredFilterRetriever.js +6 -6
  82. package/lib/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  83. package/lib/packlets/retrieve/tagRetriever.d.ts +3 -3
  84. package/lib/packlets/retrieve/tagRetriever.d.ts.map +1 -1
  85. package/lib/packlets/retrieve/tagRetriever.js +6 -6
  86. package/lib/packlets/retrieve/tagRetriever.js.map +1 -1
  87. package/lib/packlets/retrieve/temporalRetrievers.d.ts +7 -5
  88. package/lib/packlets/retrieve/temporalRetrievers.d.ts.map +1 -1
  89. package/lib/packlets/retrieve/temporalRetrievers.js +22 -19
  90. package/lib/packlets/retrieve/temporalRetrievers.js.map +1 -1
  91. package/lib/packlets/store/coverage.d.ts +102 -0
  92. package/lib/packlets/store/coverage.d.ts.map +1 -0
  93. package/lib/packlets/store/coverage.js +7 -0
  94. package/lib/packlets/store/coverage.js.map +1 -0
  95. package/lib/packlets/store/fileTreeMemoryStore.d.ts +53 -166
  96. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  97. package/lib/packlets/store/fileTreeMemoryStore.js +221 -79
  98. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  99. package/lib/packlets/store/index.d.ts +4 -0
  100. package/lib/packlets/store/index.d.ts.map +1 -1
  101. package/lib/packlets/store/index.js +4 -0
  102. package/lib/packlets/store/index.js.map +1 -1
  103. package/lib/packlets/store/listSelection.d.ts +101 -0
  104. package/lib/packlets/store/listSelection.d.ts.map +1 -0
  105. package/lib/packlets/store/listSelection.js +40 -0
  106. package/lib/packlets/store/listSelection.js.map +1 -0
  107. package/lib/packlets/store/memoryStore.d.ts +237 -0
  108. package/lib/packlets/store/memoryStore.d.ts.map +1 -0
  109. package/lib/packlets/store/memoryStore.js +7 -0
  110. package/lib/packlets/store/memoryStore.js.map +1 -0
  111. package/lib/packlets/store/reconcile.d.ts +82 -0
  112. package/lib/packlets/store/reconcile.d.ts.map +1 -0
  113. package/lib/packlets/store/reconcile.js +7 -0
  114. package/lib/packlets/store/reconcile.js.map +1 -0
  115. package/lib/packlets/store/storeCoverage.d.ts +45 -0
  116. package/lib/packlets/store/storeCoverage.d.ts.map +1 -0
  117. package/lib/packlets/store/storeCoverage.js +105 -0
  118. package/lib/packlets/store/storeCoverage.js.map +1 -0
  119. package/lib/packlets/store/storeReconcile.d.ts +41 -0
  120. package/lib/packlets/store/storeReconcile.d.ts.map +1 -0
  121. package/lib/packlets/store/storeReconcile.js +125 -0
  122. package/lib/packlets/store/storeReconcile.js.map +1 -0
  123. package/lib/packlets/store/vectorMaintenance.d.ts +74 -0
  124. package/lib/packlets/store/vectorMaintenance.d.ts.map +1 -1
  125. package/lib/packlets/store/vectorMaintenance.js +117 -8
  126. package/lib/packlets/store/vectorMaintenance.js.map +1 -1
  127. package/lib/packlets/store/vectorRecordSource.d.ts +36 -0
  128. package/lib/packlets/store/vectorRecordSource.d.ts.map +1 -0
  129. package/lib/packlets/store/vectorRecordSource.js +47 -0
  130. package/lib/packlets/store/vectorRecordSource.js.map +1 -0
  131. package/lib/packlets/tools/memoryTools.d.ts.map +1 -1
  132. package/lib/packlets/tools/memoryTools.js +25 -2
  133. package/lib/packlets/tools/memoryTools.js.map +1 -1
  134. package/lib/packlets/types/envelope.d.ts +24 -2
  135. package/lib/packlets/types/envelope.d.ts.map +1 -1
  136. package/lib/packlets/types/envelope.js +26 -0
  137. package/lib/packlets/types/envelope.js.map +1 -1
  138. package/lib/packlets/types/index.d.ts +1 -0
  139. package/lib/packlets/types/index.d.ts.map +1 -1
  140. package/lib/packlets/types/index.js +1 -0
  141. package/lib/packlets/types/index.js.map +1 -1
  142. package/lib/packlets/types/recordResolver.d.ts +39 -0
  143. package/lib/packlets/types/recordResolver.d.ts.map +1 -0
  144. package/lib/packlets/types/recordResolver.js +7 -0
  145. package/lib/packlets/types/recordResolver.js.map +1 -0
  146. package/lib/packlets/types/temporal.d.ts +26 -6
  147. package/lib/packlets/types/temporal.d.ts.map +1 -1
  148. package/lib/packlets/types/temporal.js.map +1 -1
  149. package/lib/packlets/vector/inMemoryCosineIndex.d.ts +9 -2
  150. package/lib/packlets/vector/inMemoryCosineIndex.d.ts.map +1 -1
  151. package/lib/packlets/vector/inMemoryCosineIndex.js +40 -19
  152. package/lib/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  153. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts +6 -3
  154. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts.map +1 -1
  155. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js +66 -11
  156. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  157. package/lib/packlets/vector/rebuildHelpers.d.ts +30 -0
  158. package/lib/packlets/vector/rebuildHelpers.d.ts.map +1 -0
  159. package/lib/packlets/vector/rebuildHelpers.js +42 -0
  160. package/lib/packlets/vector/rebuildHelpers.js.map +1 -0
  161. package/lib/packlets/vector/vectorIndex.d.ts +270 -15
  162. package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
  163. package/lib/packlets/vector/vectorIndex.js.map +1 -1
  164. package/package.json +7 -7
@@ -7,8 +7,12 @@ import { FileTree } from '@fgv/ts-json-base';
7
7
  import { DEFAULT_DEDUP_SCOPE, KnowledgeLwwPolicy, isTemporalIdentityCodec, isTemporalRecord, isVersionCurrent, selectCurrentVersion, selectVersionAsOf } from '../types';
8
8
  import { parseMemoryFile, serializeMemoryFile, splitFrontmatter } from '../converters';
9
9
  import { VectorMaintenance } from './vectorMaintenance';
10
+ import { computeCoverage } from './storeCoverage';
11
+ import { reconcileVectors } from './storeReconcile';
10
12
  import { MemoryIndex } from '../index';
11
13
  import { defaultMemoryScopeEncoding } from './scopeEncoding';
14
+ import { isWholeVaultScan } from './listSelection';
15
+ import { vectorRecordSource } from './vectorRecordSource';
12
16
  /** The on-disk extension for a memory record file. */
13
17
  const MEMORY_FILE_EXTENSION = '.md';
14
18
  /**
@@ -124,7 +128,7 @@ export class FileTreeMemoryStore {
124
128
  // version whose `invalid_at` is null/absent) from the entity subtree,
125
129
  // read off the derived index. `asOf` resolution is via `list({ asOf })`
126
130
  // and the temporal retrievers.
127
- return succeed(this._readVersionedCurrent(addr.scope));
131
+ return this._readVersionedCurrent(addr.scope);
128
132
  }
129
133
  return this._readRecord(addr.scope, addr.idStem);
130
134
  }));
@@ -140,50 +144,130 @@ export class FileTreeMemoryStore {
140
144
  return this._readRecord(scope, id);
141
145
  }
142
146
  /** {@inheritDoc IMemoryStore.list} */
143
- async list(filter) {
144
- const matches = this._index
145
- .entries()
146
- .filter((entry) => {
147
- if ((filter === null || filter === void 0 ? void 0 : filter.scope) !== undefined && entry.scope !== filter.scope) {
147
+ async list(selection) {
148
+ // Deliberately NOT annotated `: boolean`. An explicit annotation discards the
149
+ // type predicate, and TypeScript's aliased-condition narrowing is what lets
150
+ // the ternary below see `selection` as a filter on the false branch. With the
151
+ // annotation this line compiles and the next one does not.
152
+ const scan = isWholeVaultScan(selection);
153
+ const filter = scan ? {} : selection;
154
+ if (!scan && filter.scope === undefined && filter.kind === undefined && filter.tag === undefined) {
155
+ // `asOf` alone does not narrow — it collapses versions, it does not exclude
156
+ // entities — so a selection carrying only `asOf` lands here too.
157
+ return fail('memory list: a selection must narrow by scope, kind or tag; ' +
158
+ 'pass scanEveryRecord() to read every record in the vault deliberately');
159
+ }
160
+ // Select over envelopes, project temporally, and materialize LAST — so a
161
+ // narrowed list reads only the files it is going to return.
162
+ const selected = this._index.entries().filter((entry) => {
163
+ if (filter.scope !== undefined && entry.scope !== filter.scope) {
148
164
  return false;
149
165
  }
150
- if ((filter === null || filter === void 0 ? void 0 : filter.kind) !== undefined && entry.record.envelope.kind !== filter.kind) {
166
+ if (filter.kind !== undefined && entry.envelope.kind !== filter.kind) {
151
167
  return false;
152
168
  }
153
- if ((filter === null || filter === void 0 ? void 0 : filter.tag) !== undefined && !entry.record.envelope.tags.includes(filter.tag)) {
169
+ if (filter.tag !== undefined && !entry.envelope.tags.includes(filter.tag)) {
154
170
  return false;
155
171
  }
156
172
  return true;
157
- })
158
- .map((entry) => entry.record);
159
- if ((filter === null || filter === void 0 ? void 0 : filter.asOf) === undefined) {
160
- // No temporal projection requested: byte-identical to the pre-temporal
161
- // behavior (the flat-path guarantee — every version is returned).
162
- return succeed(matches);
163
- }
164
- return succeed(FileTreeMemoryStore._projectAsOf(matches, filter.asOf));
173
+ });
174
+ const projected = selection.asOf === undefined
175
+ ? // No temporal projection requested: the flat-path guarantee every
176
+ // version is returned.
177
+ selected
178
+ : FileTreeMemoryStore._projectAsOf(selected, selection.asOf);
179
+ return this._materialize(projected);
180
+ }
181
+ /** {@inheritDoc IMemoryStore.coverage} */
182
+ coverage() {
183
+ return Promise.resolve(computeCoverage({
184
+ entries: this._index.entries(),
185
+ hasRankProjector: (kind) => this._rankProjectors.has(kind),
186
+ anyRankProjector: this._rankProjectors.size > 0,
187
+ embedsKind: (kind) => this.embedsKind(kind),
188
+ vectorIndex: this._vectors.vectorIndex,
189
+ fragmentIndex: this._vectors.fragmentIndex
190
+ }));
191
+ }
192
+ /** {@inheritDoc IMemoryStore.listEntries} */
193
+ async listEntries() {
194
+ return succeed(this._index.entries());
165
195
  }
166
196
  /** {@inheritDoc IMemoryStore.listScoped} */
167
197
  async listScoped() {
168
- // The derived index already carries each record's scope
169
- // ({@link IIndexedMemoryRecord.scope}), so the scoped projection is a direct
170
- // map the record's `(scope, id)` is exactly the address the vector index
171
- // keys on. No filter/temporal projection: the seam re-embeds the whole vault.
172
- return succeed(this._index.entries().map((entry) => ({
173
- target: { scope: entry.scope, id: entry.record.envelope.id },
174
- record: entry.record
175
- })));
198
+ // The derived index already carries each record's scope, so the scoped
199
+ // projection keys straight off it — `(scope, id)` is exactly the address the
200
+ // vector index uses. Bodies ARE materialized here: this seam feeds an
201
+ // embedder, which is the one consumer that genuinely needs every body. No
202
+ // filter/temporal projection — it re-embeds the whole vault by contract.
203
+ //
204
+ // **Complete or failed — never quietly short.** This call is the sole feed
205
+ // for `asRecordSource()`, and therefore for `IVectorIndex.rebuild`'s coverage
206
+ // report. A record dropped here would land in none of `records` / `excluded`
207
+ // / `indexed` / `declined` / `skipped`, so a caller computing coverage would
208
+ // undercount *in the direction of looking healthier* — which is precisely the
209
+ // failure `vectorRecordSource`'s own tally exists to prevent, reintroduced one
210
+ // layer down. A loud failure is recoverable; a silent undercount is not.
211
+ //
212
+ // Two consequences worth stating rather than discovering. Before the index
213
+ // was projected this method read nothing and could not fail, because the
214
+ // index held the records themselves; it now costs a full-vault read and is
215
+ // fallible. And it is NOT write-locked, so a concurrent `put` whose cap-cull
216
+ // physically evicts a record between the snapshot and the read will fail it.
217
+ // That window is real and is tracked in `docs/FUTURE.md` — the answer is to
218
+ // make the loss *countable*, not to swallow it.
219
+ return mapResults(this._index.entries().map((entry) => {
220
+ const target = { scope: entry.scope, id: entry.envelope.id };
221
+ return this._resolveRequired(entry).onSuccess((record) => succeed({ target, record }));
222
+ }));
223
+ }
224
+ /** {@inheritDoc IMemoryRecordResolver.resolveRecord} */
225
+ resolveRecord(scope, id) {
226
+ return this._readRecord(scope, id);
227
+ }
228
+ /**
229
+ * Materialize a selected set of entries into records, dropping any that have
230
+ * vanished since selection.
231
+ *
232
+ * @remarks
233
+ * A miss is not a failure. Selection reads the in-memory index and
234
+ * materialization reads storage, so a record deleted in between is a legitimate
235
+ * race and yields a shorter list rather than an error. A read that FAILS is a
236
+ * real fault and propagates.
237
+ */
238
+ _materialize(entries) {
239
+ return mapResults(entries.map((entry) => this._readRecord(entry.scope, entry.envelope.id))).onSuccess((records) => succeed(records.filter((r) => r !== undefined)));
240
+ }
241
+ /**
242
+ * Materialize one entry, treating "gone" as a fault rather than a miss.
243
+ *
244
+ * @remarks
245
+ * For paths where a vanished record really does mean the index and the vault
246
+ * disagree, rather than that something legitimately removed it in between.
247
+ *
248
+ * Three of the four callers hold the write lock, so nothing can have removed
249
+ * the record since the entry was read. `get()`'s versioned path does not, and
250
+ * is safe only because temporal kinds never physically delete a version — they
251
+ * invalidate in place, and cap-cull does not apply to them. **If eviction is
252
+ * ever added to the temporal path, that caller must change**, or it
253
+ * reintroduces the race this method exists to detect.
254
+ *
255
+ * `listScoped` also does not hold the lock, and uses this deliberately anyway:
256
+ * it feeds a coverage report, so a silent drop there is worse than a loud
257
+ * failure. See its comment, and `docs/FUTURE.md` for the eviction window.
258
+ *
259
+ * The drop-tolerant counterpart is {@link FileTreeMemoryStore._materialize},
260
+ * for readers where a record that vanished between selection and
261
+ * materialization is a miss rather than a fault.
262
+ */
263
+ _resolveRequired(entry) {
264
+ return this._readRecord(entry.scope, entry.envelope.id).onSuccess((record) => record === undefined
265
+ ? fail(`memory: index entry '${entry.scope}/${entry.envelope.id}' has no record in the vault`)
266
+ : succeed(record));
176
267
  }
177
268
  /** {@inheritDoc IMemoryStore.asRecordSource} */
178
269
  asRecordSource() {
179
- return {
180
- // Filtered to the kinds that participate in the record vector index. This
181
- // source exists to drive `IVectorIndex` rebuilds, so a kind excluded from the
182
- // index has no business being re-embedded on open — which is where the cost
183
- // is worst, since a rebuild embeds the whole vault serially. With no
184
- // `embedKinds` declaration every kind passes and this is the identity filter.
185
- list: async () => (await this.listScoped()).onSuccess((scoped) => succeed(scoped.filter((s) => this.embedsKind(s.record.envelope.kind))))
186
- };
270
+ return vectorRecordSource(this);
187
271
  }
188
272
  /**
189
273
  * Collapse temporal records to the single version valid at `asOf` per entity;
@@ -192,21 +276,21 @@ export class FileTreeMemoryStore {
192
276
  * filtering is deferred — OQ-9). An entity with no version valid at `asOf`
193
277
  * contributes nothing.
194
278
  */
195
- static _projectAsOf(records, asOf) {
279
+ static _projectAsOf(entries, asOf) {
196
280
  const passthrough = [];
197
281
  const groups = new Map();
198
- for (const record of records) {
199
- if (!isTemporalRecord(record)) {
200
- passthrough.push(record);
282
+ for (const entry of entries) {
283
+ if (!isTemporalRecord(entry)) {
284
+ passthrough.push(entry);
201
285
  continue;
202
286
  }
203
- const key = `${record.envelope.kind}\0${record.envelope.entityId}`;
287
+ const key = `${entry.envelope.kind}\0${entry.envelope.entityId}`;
204
288
  const existing = groups.get(key);
205
289
  if (existing === undefined) {
206
- groups.set(key, [record]);
290
+ groups.set(key, [entry]);
207
291
  }
208
292
  else {
209
- existing.push(record);
293
+ existing.push(entry);
210
294
  }
211
295
  }
212
296
  const result = [...passthrough];
@@ -218,9 +302,9 @@ export class FileTreeMemoryStore {
218
302
  }
219
303
  return result;
220
304
  }
221
- /** {@inheritDoc IMemoryStore.reconcileRank} */
222
- async reconcileRank(kind) {
223
- return this._enqueue(() => this._reconcileRankLocked(kind));
305
+ /** {@inheritDoc IMemoryStore.reconcile} */
306
+ async reconcile(kind, artifact) {
307
+ return this._enqueue(() => this._reconcileLocked(kind, artifact));
224
308
  }
225
309
  /** {@inheritDoc IMemoryStore.put} */
226
310
  async put(record) {
@@ -391,8 +475,11 @@ export class FileTreeMemoryStore {
391
475
  // mutableFields) actually applies. Content-dedup must never shadow LWW for
392
476
  // the same entity.
393
477
  const duplicate = this._findByContentHash(scope, hash, record.envelope.id);
394
- if (duplicate !== undefined) {
395
- return succeed({ record: duplicate, evicted: [] });
478
+ if (duplicate.isFailure()) {
479
+ return fail(duplicate.message);
480
+ }
481
+ if (duplicate.value !== undefined) {
482
+ return succeed({ record: duplicate.value, evicted: [] });
396
483
  }
397
484
  }
398
485
  return this._readRecord(scope, idStem).thenOnSuccess((existing) => {
@@ -413,9 +500,8 @@ export class FileTreeMemoryStore {
413
500
  // (a replace, not a grow). The same-id `existing` record is threaded
414
501
  // separately into `_buildRecord` for the merge-patch. Knowledge LWW ignores
415
502
  // this argument, so its behavior is unchanged by the wider cohort.
416
- const cohort = this._admissionCohort(scope, record.envelope.kind, idStem);
417
- return policy
418
- .admit(record, cohort)
503
+ return this._admissionCohort(scope, record.envelope.kind, idStem)
504
+ .onSuccess((cohort) => policy.admit(record, cohort))
419
505
  .thenOnSuccess((decision) => this._admitWrite(record, body, scope, idStem, hash, policy, existing, decision));
420
506
  });
421
507
  }
@@ -565,7 +651,16 @@ export class FileTreeMemoryStore {
565
651
  * written, or fully invalidated / soft-deleted).
566
652
  */
567
653
  _readVersionedCurrent(scope) {
568
- return selectCurrentVersion(this._versionsForEntity(scope));
654
+ // Select over ENVELOPES, then materialize the one winner — which is what
655
+ // `IMemoryStore.get`'s docstring promises and what `selectCurrentVersion`
656
+ // being generic over `IEnvelopeCarrier` exists for. Materializing every
657
+ // version first (as this did) cost N file reads and N body validations to
658
+ // return one record, and made that docstring false.
659
+ const current = selectCurrentVersion(this._index.entries().filter((entry) => entry.scope === scope));
660
+ if (current === undefined) {
661
+ return succeed(undefined);
662
+ }
663
+ return this._resolveRequired(current);
569
664
  }
570
665
  /**
571
666
  * Every persisted version of the entity whose subtree is `scope`. All version
@@ -573,10 +668,13 @@ export class FileTreeMemoryStore {
573
668
  * entityId), so a scope filter over the index isolates one entity's versions.
574
669
  */
575
670
  _versionsForEntity(scope) {
576
- return this._index
671
+ // Selected on the envelope (scope), materialized after — so a versioned write
672
+ // reads only that entity's versions, never the vault. Bounded by the entity's
673
+ // version count.
674
+ return mapResults(this._index
577
675
  .entries()
578
676
  .filter((entry) => entry.scope === scope)
579
- .map((entry) => entry.record);
677
+ .map((entry) => this._resolveRequired(entry)));
580
678
  }
581
679
  /**
582
680
  * Versioned write (invalidate-don't-delete). Builds the new version's content
@@ -598,7 +696,11 @@ export class FileTreeMemoryStore {
598
696
  // still-current version at snapshot time — normally one, but two-or-more if a
599
697
  // prior invalidation partially failed; invalidating all of them lets the write
600
698
  // self-heal a stuck state (P2-7).
601
- const versions = this._versionsForEntity(scope);
699
+ const snapshot = this._versionsForEntity(scope);
700
+ if (snapshot.isFailure()) {
701
+ return fail(snapshot.message);
702
+ }
703
+ const versions = snapshot.value;
602
704
  const priorCurrents = versions.filter(isVersionCurrent);
603
705
  const current = selectCurrentVersion(versions);
604
706
  const policy = this._policyFor(kind);
@@ -724,7 +826,11 @@ export class FileTreeMemoryStore {
724
826
  * builds on it), so a hard delete would defeat the purpose.
725
827
  */
726
828
  async _deleteVersioned(entityId, scope) {
727
- const versions = this._versionsForEntity(scope);
829
+ const snapshot = this._versionsForEntity(scope);
830
+ if (snapshot.isFailure()) {
831
+ return fail(snapshot.message);
832
+ }
833
+ const versions = snapshot.value;
728
834
  const currents = versions.filter(isVersionCurrent);
729
835
  const current = selectCurrentVersion(versions);
730
836
  if (current === undefined) {
@@ -771,10 +877,12 @@ export class FileTreeMemoryStore {
771
877
  * across first-writes and updates.
772
878
  */
773
879
  _admissionCohort(scope, kind, idStem) {
774
- return this._index
880
+ // Envelope-only selection; the cohort is per-(scope, kind) and bounded by the
881
+ // cull cap, so materializing it is small by construction.
882
+ return mapResults(this._index
775
883
  .entries()
776
- .filter((entry) => entry.scope === scope && entry.record.envelope.kind === kind && entry.record.envelope.id !== idStem)
777
- .map((entry) => entry.record);
884
+ .filter((entry) => entry.scope === scope && entry.envelope.kind === kind && entry.envelope.id !== idStem)
885
+ .map((entry) => this._resolveRequired(entry)));
778
886
  }
779
887
  /**
780
888
  * Find a record in `scope` whose `contentHash` equals `hash`, if any,
@@ -782,12 +890,13 @@ export class FileTreeMemoryStore {
782
890
  * dedup skip the same-id record so it does not shadow the LWW update path.
783
891
  */
784
892
  _findByContentHash(scope, hash, excludeId) {
893
+ // The clearest win of the projection: the hash lives on the envelope, so this
894
+ // scans envelopes and reads exactly ONE file — the match — where it used to
895
+ // hold every body in the scope to look at one field.
785
896
  const match = this._index
786
897
  .entries()
787
- .find((entry) => entry.scope === scope &&
788
- entry.record.envelope.contentHash === hash &&
789
- entry.record.envelope.id !== excludeId);
790
- return match === null || match === void 0 ? void 0 : match.record;
898
+ .find((entry) => entry.scope === scope && entry.envelope.contentHash === hash && entry.envelope.id !== excludeId);
899
+ return match === undefined ? succeed(undefined) : this._resolveRequired(match);
791
900
  }
792
901
  /**
793
902
  * True when `incoming`'s caller-authored mutable metadata (`tags` / `provenance`)
@@ -813,7 +922,7 @@ export class FileTreeMemoryStore {
813
922
  return this._hasher.computeHash({ kind, body, links });
814
923
  }
815
924
  /**
816
- * The locked body of {@link FileTreeMemoryStore.reconcileRank}.
925
+ * The rank branch of {@link FileTreeMemoryStore.reconcile}, under the write lock.
817
926
  *
818
927
  * @remarks
819
928
  * Re-reads each record's file rather than trusting the in-memory index, for
@@ -842,31 +951,54 @@ export class FileTreeMemoryStore {
842
951
  * another on a reconcile. `_stampRank` itself is reused verbatim, which also
843
952
  * inherits its throw semantics (logged at `warn`, `rank` cleared).
844
953
  */
845
- async _reconcileRankLocked(kind) {
846
- if (!this._rankProjectors.has(kind)) {
847
- return fail(`memory reconcileRank '${kind}': no rank projector is registered for this kind`);
848
- }
954
+ async _reconcileLocked(kind, artifact) {
955
+ // Envelope-only selection: the walk needs `(scope, id)` and the kind, and
956
+ // each branch materializes only the records it decides to repair.
849
957
  const targets = this._index
850
958
  .entries()
851
- .filter((entry) => entry.record.envelope.kind === kind);
852
- let restamped = 0;
959
+ .filter((entry) => entry.envelope.kind === kind);
960
+ if (artifact === 'rank') {
961
+ return this._reconcileRankLocked(kind, targets);
962
+ }
963
+ return reconcileVectors({
964
+ kind,
965
+ artifact,
966
+ targets,
967
+ maintenance: this._vectors,
968
+ embedsKind: (k) => this.embedsKind(k),
969
+ resolve: (scope, id) => this._readRecord(scope, id),
970
+ stampRef: (scope, id, ref) => this._rewriteEnvelope(scope, id, (r) => r.envelope.embeddingRef === ref
971
+ ? undefined
972
+ : { envelope: Object.assign(Object.assign({}, r.envelope), { embeddingRef: ref }), body: r.body })
973
+ });
974
+ }
975
+ /** The rank branch of {@link FileTreeMemoryStore._reconcileLocked}. */
976
+ async _reconcileRankLocked(kind, targets) {
977
+ if (!this._rankProjectors.has(kind)) {
978
+ return fail(`memory reconcile '${kind}' rank: no rank projector is registered for this kind`);
979
+ }
980
+ // No materialization at all: `_rewriteEnvelope` re-reads each record itself.
981
+ let repaired = 0;
853
982
  for (const target of targets) {
854
- const applied = this._restampOne(target.scope, target.record.envelope.id);
983
+ const applied = this._rewriteEnvelope(target.scope, target.envelope.id, (r) => {
984
+ const stamped = this._stampRank(r);
985
+ return stamped.envelope.rank === r.envelope.rank ? undefined : stamped;
986
+ });
855
987
  if (applied.isFailure()) {
856
- return fail(`memory reconcileRank '${kind}': ${applied.message}`);
988
+ return fail(`memory reconcile '${kind}' rank: ${applied.message}`);
857
989
  }
858
990
  if (applied.value) {
859
- restamped++;
991
+ repaired++;
860
992
  }
861
993
  }
862
- return succeed(restamped);
994
+ return succeed({ artifact: 'rank', kind, examined: targets.length, repaired, failed: [] });
863
995
  }
864
996
  /**
865
997
  * Re-apply the rank projector to one record on disk. Returns whether `rank`
866
998
  * actually changed — an unchanged rank writes nothing, so a reconcile over an
867
999
  * already-consistent store touches no files.
868
1000
  */
869
- _restampOne(scope, id) {
1001
+ _rewriteEnvelope(scope, id, mutate) {
870
1002
  return this._resolveScopeDir(scope).onSuccess((scopeDir) => {
871
1003
  /* c8 ignore next 3 - defensive: the scope dir exists for any indexed record */
872
1004
  if (scopeDir === undefined) {
@@ -894,12 +1026,16 @@ export class FileTreeMemoryStore {
894
1026
  .onSuccess((parsed) => splitFrontmatter(raw)
895
1027
  .withErrorFormat((msg) => `'${id}': ${msg}`)
896
1028
  .onSuccess((parts) => {
897
- const before = parsed.envelope.rank;
898
- const stamped = this._stampRank({
1029
+ // The mutator says "nothing to change" with `undefined` rather
1030
+ // than by returning an equal record: persisting unconditionally
1031
+ // would bump `updated` on every record of the kind, trading a
1032
+ // wrong value for a wrong timestamp, and a deep comparison here
1033
+ // would have to know which fields each caller touches.
1034
+ const stamped = mutate({
899
1035
  envelope: parsed.envelope,
900
1036
  body: parts.body
901
1037
  });
902
- if (stamped.envelope.rank === before) {
1038
+ if (stamped === undefined) {
903
1039
  return succeed(false);
904
1040
  }
905
1041
  return this._persist(stamped, scope, id).onSuccess(() => succeed(true));
@@ -1095,8 +1231,8 @@ export class FileTreeMemoryStore {
1095
1231
  _initialIndex(onRecordError) {
1096
1232
  return this._collectEntries(this._root, [], onRecordError).onSuccess((entries) => this._index.rebuild(entries).onSuccess(() => {
1097
1233
  for (const entry of entries) {
1098
- if (entry.record.envelope.seq > this._seq) {
1099
- this._seq = entry.record.envelope.seq;
1234
+ if (entry.envelope.seq > this._seq) {
1235
+ this._seq = entry.envelope.seq;
1100
1236
  }
1101
1237
  }
1102
1238
  return succeed(true);
@@ -1137,11 +1273,17 @@ export class FileTreeMemoryStore {
1137
1273
  * failure passes through untouched so the historical error is byte-identical.
1138
1274
  */
1139
1275
  _loadRecordFile(scope, child, onRecordError) {
1140
- return child
1276
+ return (child
1141
1277
  .getRawContents()
1142
1278
  .onSuccess((raw) => parseMemoryFile(raw, this._registry))
1143
1279
  .onSuccess((parsedRecord) => this._verifyLoaded(scope, child, parsedRecord))
1144
- .onSuccess((verified) => succeed([{ scope, record: verified }]))
1280
+ // Parse validate → PROJECT → discard, per file. The body is read and
1281
+ // fully validated (which is what gives `onRecordError` its meaning), then
1282
+ // dropped here rather than carried into the index. Peak body residency
1283
+ // across the whole open is therefore ONE record, not N — which is the
1284
+ // resident-memory moment this whole surface exists to fix, and it is why
1285
+ // the projection had to reach `rebuild` and not just the read methods.
1286
+ .onSuccess((verified) => succeed([{ scope, envelope: verified.envelope }]))
1145
1287
  .onFailure((message) => {
1146
1288
  if (onRecordError === 'skip') {
1147
1289
  const path = `${scope}/${child.name}`;
@@ -1150,7 +1292,7 @@ export class FileTreeMemoryStore {
1150
1292
  this._warnSwallowed(error);
1151
1293
  }
1152
1294
  return fail(message);
1153
- });
1295
+ }));
1154
1296
  }
1155
1297
  }
1156
1298
  /**