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

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 (182) 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/fragmentSemanticRetriever.js +94 -5
  6. package/dist/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -1
  7. package/dist/packlets/retrieve/linkTraversalRetriever.js +12 -26
  8. package/dist/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  9. package/dist/packlets/retrieve/recencyRetriever.js +7 -7
  10. package/dist/packlets/retrieve/recencyRetriever.js.map +1 -1
  11. package/dist/packlets/retrieve/retriever.js +91 -10
  12. package/dist/packlets/retrieve/retriever.js.map +1 -1
  13. package/dist/packlets/retrieve/semanticRetriever.js +16 -16
  14. package/dist/packlets/retrieve/semanticRetriever.js.map +1 -1
  15. package/dist/packlets/retrieve/structuredFilterRetriever.js +7 -7
  16. package/dist/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  17. package/dist/packlets/retrieve/tagRetriever.js +7 -7
  18. package/dist/packlets/retrieve/tagRetriever.js.map +1 -1
  19. package/dist/packlets/retrieve/temporalRetrievers.js +23 -20
  20. package/dist/packlets/retrieve/temporalRetrievers.js.map +1 -1
  21. package/dist/packlets/store/coverage.js +6 -0
  22. package/dist/packlets/store/coverage.js.map +1 -0
  23. package/dist/packlets/store/fileTreeMemoryStore.js +234 -103
  24. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  25. package/dist/packlets/store/index.js +4 -0
  26. package/dist/packlets/store/index.js.map +1 -1
  27. package/dist/packlets/store/listSelection.js +36 -0
  28. package/dist/packlets/store/listSelection.js.map +1 -0
  29. package/dist/packlets/store/memoryStore.js +6 -0
  30. package/dist/packlets/store/memoryStore.js.map +1 -0
  31. package/dist/packlets/store/reconcile.js +6 -0
  32. package/dist/packlets/store/reconcile.js.map +1 -0
  33. package/dist/packlets/store/storeCoverage.js +102 -0
  34. package/dist/packlets/store/storeCoverage.js.map +1 -0
  35. package/dist/packlets/store/storeIdentity.js +62 -0
  36. package/dist/packlets/store/storeIdentity.js.map +1 -0
  37. package/dist/packlets/store/storeReconcile.js +122 -0
  38. package/dist/packlets/store/storeReconcile.js.map +1 -0
  39. package/dist/packlets/store/vectorMaintenance.js +116 -8
  40. package/dist/packlets/store/vectorMaintenance.js.map +1 -1
  41. package/dist/packlets/store/vectorRecordSource.js +44 -0
  42. package/dist/packlets/store/vectorRecordSource.js.map +1 -0
  43. package/dist/packlets/tools/memoryTools.js +25 -2
  44. package/dist/packlets/tools/memoryTools.js.map +1 -1
  45. package/dist/packlets/types/envelope.js +25 -0
  46. package/dist/packlets/types/envelope.js.map +1 -1
  47. package/dist/packlets/types/identityResolver.js +6 -0
  48. package/dist/packlets/types/identityResolver.js.map +1 -0
  49. package/dist/packlets/types/index.js +2 -0
  50. package/dist/packlets/types/index.js.map +1 -1
  51. package/dist/packlets/types/recordResolver.js +6 -0
  52. package/dist/packlets/types/recordResolver.js.map +1 -0
  53. package/dist/packlets/types/temporal.js.map +1 -1
  54. package/dist/packlets/vector/inMemoryCosineIndex.js +39 -18
  55. package/dist/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  56. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js +109 -14
  57. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  58. package/dist/packlets/vector/rebuildHelpers.js +38 -0
  59. package/dist/packlets/vector/rebuildHelpers.js.map +1 -0
  60. package/dist/packlets/vector/vectorIndex.js.map +1 -1
  61. package/dist/ts-agent-memory.d.ts +1226 -114
  62. package/lib/packlets/index/memoryIndex.d.ts +118 -27
  63. package/lib/packlets/index/memoryIndex.d.ts.map +1 -1
  64. package/lib/packlets/index/memoryIndex.js +23 -16
  65. package/lib/packlets/index/memoryIndex.js.map +1 -1
  66. package/lib/packlets/ingest/orchestrator.d.ts.map +1 -1
  67. package/lib/packlets/ingest/orchestrator.js +13 -1
  68. package/lib/packlets/ingest/orchestrator.js.map +1 -1
  69. package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts +79 -1
  70. package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts.map +1 -1
  71. package/lib/packlets/retrieve/fragmentSemanticRetriever.js +94 -5
  72. package/lib/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -1
  73. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts +3 -10
  74. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts.map +1 -1
  75. package/lib/packlets/retrieve/linkTraversalRetriever.js +11 -25
  76. package/lib/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  77. package/lib/packlets/retrieve/recencyRetriever.d.ts +3 -3
  78. package/lib/packlets/retrieve/recencyRetriever.d.ts.map +1 -1
  79. package/lib/packlets/retrieve/recencyRetriever.js +6 -6
  80. package/lib/packlets/retrieve/recencyRetriever.js.map +1 -1
  81. package/lib/packlets/retrieve/retriever.d.ts +88 -7
  82. package/lib/packlets/retrieve/retriever.d.ts.map +1 -1
  83. package/lib/packlets/retrieve/retriever.js +94 -9
  84. package/lib/packlets/retrieve/retriever.js.map +1 -1
  85. package/lib/packlets/retrieve/semanticRetriever.d.ts +3 -5
  86. package/lib/packlets/retrieve/semanticRetriever.d.ts.map +1 -1
  87. package/lib/packlets/retrieve/semanticRetriever.js +15 -15
  88. package/lib/packlets/retrieve/semanticRetriever.js.map +1 -1
  89. package/lib/packlets/retrieve/structuredFilterRetriever.d.ts +3 -3
  90. package/lib/packlets/retrieve/structuredFilterRetriever.d.ts.map +1 -1
  91. package/lib/packlets/retrieve/structuredFilterRetriever.js +6 -6
  92. package/lib/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  93. package/lib/packlets/retrieve/tagRetriever.d.ts +3 -3
  94. package/lib/packlets/retrieve/tagRetriever.d.ts.map +1 -1
  95. package/lib/packlets/retrieve/tagRetriever.js +6 -6
  96. package/lib/packlets/retrieve/tagRetriever.js.map +1 -1
  97. package/lib/packlets/retrieve/temporalRetrievers.d.ts +7 -5
  98. package/lib/packlets/retrieve/temporalRetrievers.d.ts.map +1 -1
  99. package/lib/packlets/retrieve/temporalRetrievers.js +22 -19
  100. package/lib/packlets/retrieve/temporalRetrievers.js.map +1 -1
  101. package/lib/packlets/store/coverage.d.ts +102 -0
  102. package/lib/packlets/store/coverage.d.ts.map +1 -0
  103. package/lib/packlets/store/coverage.js +7 -0
  104. package/lib/packlets/store/coverage.js.map +1 -0
  105. package/lib/packlets/store/fileTreeMemoryStore.d.ts +56 -167
  106. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  107. package/lib/packlets/store/fileTreeMemoryStore.js +234 -103
  108. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  109. package/lib/packlets/store/index.d.ts +4 -0
  110. package/lib/packlets/store/index.d.ts.map +1 -1
  111. package/lib/packlets/store/index.js +4 -0
  112. package/lib/packlets/store/index.js.map +1 -1
  113. package/lib/packlets/store/listSelection.d.ts +101 -0
  114. package/lib/packlets/store/listSelection.d.ts.map +1 -0
  115. package/lib/packlets/store/listSelection.js +40 -0
  116. package/lib/packlets/store/listSelection.js.map +1 -0
  117. package/lib/packlets/store/memoryStore.d.ts +237 -0
  118. package/lib/packlets/store/memoryStore.d.ts.map +1 -0
  119. package/lib/packlets/store/memoryStore.js +7 -0
  120. package/lib/packlets/store/memoryStore.js.map +1 -0
  121. package/lib/packlets/store/reconcile.d.ts +82 -0
  122. package/lib/packlets/store/reconcile.d.ts.map +1 -0
  123. package/lib/packlets/store/reconcile.js +7 -0
  124. package/lib/packlets/store/reconcile.js.map +1 -0
  125. package/lib/packlets/store/storeCoverage.d.ts +45 -0
  126. package/lib/packlets/store/storeCoverage.d.ts.map +1 -0
  127. package/lib/packlets/store/storeCoverage.js +105 -0
  128. package/lib/packlets/store/storeCoverage.js.map +1 -0
  129. package/lib/packlets/store/storeIdentity.d.ts +38 -0
  130. package/lib/packlets/store/storeIdentity.d.ts.map +1 -0
  131. package/lib/packlets/store/storeIdentity.js +67 -0
  132. package/lib/packlets/store/storeIdentity.js.map +1 -0
  133. package/lib/packlets/store/storeReconcile.d.ts +41 -0
  134. package/lib/packlets/store/storeReconcile.d.ts.map +1 -0
  135. package/lib/packlets/store/storeReconcile.js +125 -0
  136. package/lib/packlets/store/storeReconcile.js.map +1 -0
  137. package/lib/packlets/store/vectorMaintenance.d.ts +74 -0
  138. package/lib/packlets/store/vectorMaintenance.d.ts.map +1 -1
  139. package/lib/packlets/store/vectorMaintenance.js +117 -8
  140. package/lib/packlets/store/vectorMaintenance.js.map +1 -1
  141. package/lib/packlets/store/vectorRecordSource.d.ts +36 -0
  142. package/lib/packlets/store/vectorRecordSource.d.ts.map +1 -0
  143. package/lib/packlets/store/vectorRecordSource.js +47 -0
  144. package/lib/packlets/store/vectorRecordSource.js.map +1 -0
  145. package/lib/packlets/tools/memoryTools.d.ts.map +1 -1
  146. package/lib/packlets/tools/memoryTools.js +25 -2
  147. package/lib/packlets/tools/memoryTools.js.map +1 -1
  148. package/lib/packlets/types/envelope.d.ts +24 -2
  149. package/lib/packlets/types/envelope.d.ts.map +1 -1
  150. package/lib/packlets/types/envelope.js +26 -0
  151. package/lib/packlets/types/envelope.js.map +1 -1
  152. package/lib/packlets/types/identityResolver.d.ts +42 -0
  153. package/lib/packlets/types/identityResolver.d.ts.map +1 -0
  154. package/lib/packlets/types/identityResolver.js +7 -0
  155. package/lib/packlets/types/identityResolver.js.map +1 -0
  156. package/lib/packlets/types/index.d.ts +2 -0
  157. package/lib/packlets/types/index.d.ts.map +1 -1
  158. package/lib/packlets/types/index.js +2 -0
  159. package/lib/packlets/types/index.js.map +1 -1
  160. package/lib/packlets/types/recordResolver.d.ts +39 -0
  161. package/lib/packlets/types/recordResolver.d.ts.map +1 -0
  162. package/lib/packlets/types/recordResolver.js +7 -0
  163. package/lib/packlets/types/recordResolver.js.map +1 -0
  164. package/lib/packlets/types/temporal.d.ts +26 -6
  165. package/lib/packlets/types/temporal.d.ts.map +1 -1
  166. package/lib/packlets/types/temporal.js.map +1 -1
  167. package/lib/packlets/vector/inMemoryCosineIndex.d.ts +9 -2
  168. package/lib/packlets/vector/inMemoryCosineIndex.d.ts.map +1 -1
  169. package/lib/packlets/vector/inMemoryCosineIndex.js +40 -19
  170. package/lib/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  171. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts +18 -4
  172. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts.map +1 -1
  173. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js +108 -13
  174. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  175. package/lib/packlets/vector/rebuildHelpers.d.ts +30 -0
  176. package/lib/packlets/vector/rebuildHelpers.d.ts.map +1 -0
  177. package/lib/packlets/vector/rebuildHelpers.js +42 -0
  178. package/lib/packlets/vector/rebuildHelpers.js.map +1 -0
  179. package/lib/packlets/vector/vectorIndex.d.ts +326 -20
  180. package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
  181. package/lib/packlets/vector/vectorIndex.js.map +1 -1
  182. package/package.json +7 -7
@@ -7,8 +7,13 @@ 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 { codecFor, resolveIdentity, verifyLoadedIdentity } from './storeIdentity';
12
+ import { reconcileVectors } from './storeReconcile';
10
13
  import { MemoryIndex } from '../index';
11
14
  import { defaultMemoryScopeEncoding } from './scopeEncoding';
15
+ import { isWholeVaultScan } from './listSelection';
16
+ import { vectorRecordSource } from './vectorRecordSource';
12
17
  /** The on-disk extension for a memory record file. */
13
18
  const MEMORY_FILE_EXTENSION = '.md';
14
19
  /**
@@ -115,7 +120,7 @@ export class FileTreeMemoryStore {
115
120
  /** {@inheritDoc IMemoryStore.get} */
116
121
  async get(kind, entityId) {
117
122
  var _a;
118
- const result = this._codecFor(kind).onSuccess((codec) => codec.encode(entityId).onSuccess((addr) => {
123
+ const result = codecFor(this._codecs, this._defaultCodec, kind).onSuccess((codec) => codec.encode(entityId).onSuccess((addr) => {
119
124
  if (addr.isVersioned) {
120
125
  if (!isTemporalIdentityCodec(codec)) {
121
126
  return fail(`memory get '${entityId}': codec for versioned kind '${kind}' does not implement the temporal codec interface`);
@@ -124,7 +129,7 @@ export class FileTreeMemoryStore {
124
129
  // version whose `invalid_at` is null/absent) from the entity subtree,
125
130
  // read off the derived index. `asOf` resolution is via `list({ asOf })`
126
131
  // and the temporal retrievers.
127
- return succeed(this._readVersionedCurrent(addr.scope));
132
+ return this._readVersionedCurrent(addr.scope);
128
133
  }
129
134
  return this._readRecord(addr.scope, addr.idStem);
130
135
  }));
@@ -140,50 +145,137 @@ export class FileTreeMemoryStore {
140
145
  return this._readRecord(scope, id);
141
146
  }
142
147
  /** {@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) {
148
+ async list(selection) {
149
+ // Deliberately NOT annotated `: boolean`. An explicit annotation discards the
150
+ // type predicate, and TypeScript's aliased-condition narrowing is what lets
151
+ // the ternary below see `selection` as a filter on the false branch. With the
152
+ // annotation this line compiles and the next one does not.
153
+ const scan = isWholeVaultScan(selection);
154
+ const filter = scan ? {} : selection;
155
+ if (!scan && filter.scope === undefined && filter.kind === undefined && filter.tag === undefined) {
156
+ // `asOf` alone does not narrow — it collapses versions, it does not exclude
157
+ // entities — so a selection carrying only `asOf` lands here too.
158
+ return fail('memory list: a selection must narrow by scope, kind or tag; ' +
159
+ 'pass scanEveryRecord() to read every record in the vault deliberately');
160
+ }
161
+ // Select over envelopes, project temporally, and materialize LAST — so a
162
+ // narrowed list reads only the files it is going to return.
163
+ const selected = this._index.entries().filter((entry) => {
164
+ if (filter.scope !== undefined && entry.scope !== filter.scope) {
148
165
  return false;
149
166
  }
150
- if ((filter === null || filter === void 0 ? void 0 : filter.kind) !== undefined && entry.record.envelope.kind !== filter.kind) {
167
+ if (filter.kind !== undefined && entry.envelope.kind !== filter.kind) {
151
168
  return false;
152
169
  }
153
- if ((filter === null || filter === void 0 ? void 0 : filter.tag) !== undefined && !entry.record.envelope.tags.includes(filter.tag)) {
170
+ if (filter.tag !== undefined && !entry.envelope.tags.includes(filter.tag)) {
154
171
  return false;
155
172
  }
156
173
  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));
174
+ });
175
+ const projected = selection.asOf === undefined
176
+ ? // No temporal projection requested: the flat-path guarantee every
177
+ // version is returned.
178
+ selected
179
+ : FileTreeMemoryStore._projectAsOf(selected, selection.asOf);
180
+ return this._materialize(projected);
181
+ }
182
+ /** {@inheritDoc IMemoryStore.coverage} */
183
+ coverage() {
184
+ return Promise.resolve(computeCoverage({
185
+ entries: this._index.entries(),
186
+ hasRankProjector: (kind) => this._rankProjectors.has(kind),
187
+ anyRankProjector: this._rankProjectors.size > 0,
188
+ embedsKind: (kind) => this.embedsKind(kind),
189
+ vectorIndex: this._vectors.vectorIndex,
190
+ fragmentIndex: this._vectors.fragmentIndex
191
+ }));
192
+ }
193
+ /** {@inheritDoc IMemoryStore.listEntries} */
194
+ async listEntries() {
195
+ return succeed(this._index.entries());
165
196
  }
166
197
  /** {@inheritDoc IMemoryStore.listScoped} */
167
198
  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
- })));
199
+ // The derived index already carries each record's scope, so the scoped
200
+ // projection keys straight off it — `(scope, id)` is exactly the address the
201
+ // vector index uses. Bodies ARE materialized here: this seam feeds an
202
+ // embedder, which is the one consumer that genuinely needs every body. No
203
+ // filter/temporal projection — it re-embeds the whole vault by contract.
204
+ //
205
+ // **Complete or failed — never quietly short.** This call is the sole feed
206
+ // for `asRecordSource()`, and therefore for `IVectorIndex.rebuild`'s coverage
207
+ // report. A record dropped here would land in none of `records` / `excluded`
208
+ // / `indexed` / `declined` / `skipped`, so a caller computing coverage would
209
+ // undercount *in the direction of looking healthier* — which is precisely the
210
+ // failure `vectorRecordSource`'s own tally exists to prevent, reintroduced one
211
+ // layer down. A loud failure is recoverable; a silent undercount is not.
212
+ //
213
+ // Two consequences worth stating rather than discovering. Before the index
214
+ // was projected this method read nothing and could not fail, because the
215
+ // index held the records themselves; it now costs a full-vault read and is
216
+ // fallible. And it is NOT write-locked, so a concurrent `put` whose cap-cull
217
+ // physically evicts a record between the snapshot and the read will fail it.
218
+ // That window is real and is tracked in `docs/FUTURE.md` — the answer is to
219
+ // make the loss *countable*, not to swallow it.
220
+ return mapResults(this._index.entries().map((entry) => {
221
+ const target = { scope: entry.scope, id: entry.envelope.id };
222
+ return this._resolveRequired(entry).onSuccess((record) => succeed({ target, record }));
223
+ }));
224
+ }
225
+ /** {@inheritDoc IMemoryRecordResolver.resolveRecord} */
226
+ resolveRecord(scope, id) {
227
+ return this._readRecord(scope, id);
228
+ }
229
+ _codec(kind) {
230
+ return codecFor(this._codecs, this._defaultCodec, kind);
231
+ }
232
+ /** {@inheritDoc IIdentityResolver.resolveIdentity} */
233
+ resolveIdentity(kind, entityId) {
234
+ return resolveIdentity(this._codecs, this._defaultCodec, kind, entityId);
235
+ }
236
+ /**
237
+ * Materialize a selected set of entries into records, dropping any that have
238
+ * vanished since selection.
239
+ *
240
+ * @remarks
241
+ * A miss is not a failure. Selection reads the in-memory index and
242
+ * materialization reads storage, so a record deleted in between is a legitimate
243
+ * race and yields a shorter list rather than an error. A read that FAILS is a
244
+ * real fault and propagates.
245
+ */
246
+ _materialize(entries) {
247
+ return mapResults(entries.map((entry) => this._readRecord(entry.scope, entry.envelope.id))).onSuccess((records) => succeed(records.filter((r) => r !== undefined)));
248
+ }
249
+ /**
250
+ * Materialize one entry, treating "gone" as a fault rather than a miss.
251
+ *
252
+ * @remarks
253
+ * For paths where a vanished record really does mean the index and the vault
254
+ * disagree, rather than that something legitimately removed it in between.
255
+ *
256
+ * Three of the four callers hold the write lock, so nothing can have removed
257
+ * the record since the entry was read. `get()`'s versioned path does not, and
258
+ * is safe only because temporal kinds never physically delete a version — they
259
+ * invalidate in place, and cap-cull does not apply to them. **If eviction is
260
+ * ever added to the temporal path, that caller must change**, or it
261
+ * reintroduces the race this method exists to detect.
262
+ *
263
+ * `listScoped` also does not hold the lock, and uses this deliberately anyway:
264
+ * it feeds a coverage report, so a silent drop there is worse than a loud
265
+ * failure. See its comment, and `docs/FUTURE.md` for the eviction window.
266
+ *
267
+ * The drop-tolerant counterpart is {@link FileTreeMemoryStore._materialize},
268
+ * for readers where a record that vanished between selection and
269
+ * materialization is a miss rather than a fault.
270
+ */
271
+ _resolveRequired(entry) {
272
+ return this._readRecord(entry.scope, entry.envelope.id).onSuccess((record) => record === undefined
273
+ ? fail(`memory: index entry '${entry.scope}/${entry.envelope.id}' has no record in the vault`)
274
+ : succeed(record));
176
275
  }
177
276
  /** {@inheritDoc IMemoryStore.asRecordSource} */
178
277
  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
- };
278
+ return vectorRecordSource(this);
187
279
  }
188
280
  /**
189
281
  * Collapse temporal records to the single version valid at `asOf` per entity;
@@ -192,21 +284,21 @@ export class FileTreeMemoryStore {
192
284
  * filtering is deferred — OQ-9). An entity with no version valid at `asOf`
193
285
  * contributes nothing.
194
286
  */
195
- static _projectAsOf(records, asOf) {
287
+ static _projectAsOf(entries, asOf) {
196
288
  const passthrough = [];
197
289
  const groups = new Map();
198
- for (const record of records) {
199
- if (!isTemporalRecord(record)) {
200
- passthrough.push(record);
290
+ for (const entry of entries) {
291
+ if (!isTemporalRecord(entry)) {
292
+ passthrough.push(entry);
201
293
  continue;
202
294
  }
203
- const key = `${record.envelope.kind}\0${record.envelope.entityId}`;
295
+ const key = `${entry.envelope.kind}\0${entry.envelope.entityId}`;
204
296
  const existing = groups.get(key);
205
297
  if (existing === undefined) {
206
- groups.set(key, [record]);
298
+ groups.set(key, [entry]);
207
299
  }
208
300
  else {
209
- existing.push(record);
301
+ existing.push(entry);
210
302
  }
211
303
  }
212
304
  const result = [...passthrough];
@@ -218,9 +310,9 @@ export class FileTreeMemoryStore {
218
310
  }
219
311
  return result;
220
312
  }
221
- /** {@inheritDoc IMemoryStore.reconcileRank} */
222
- async reconcileRank(kind) {
223
- return this._enqueue(() => this._reconcileRankLocked(kind));
313
+ /** {@inheritDoc IMemoryStore.reconcile} */
314
+ async reconcile(kind, artifact) {
315
+ return this._enqueue(() => this._reconcileLocked(kind, artifact));
224
316
  }
225
317
  /** {@inheritDoc IMemoryStore.put} */
226
318
  async put(record) {
@@ -330,7 +422,7 @@ export class FileTreeMemoryStore {
330
422
  }
331
423
  /** Resolve a scope for an observation, best-effort (undefined when unresolvable). */
332
424
  _scopeBestEffort(kind, entityId) {
333
- return this._codecFor(kind)
425
+ return this._codec(kind)
334
426
  .onSuccess((codec) => codec.encode(entityId))
335
427
  .onSuccess((addr) => succeed(addr.scope))
336
428
  .orDefault();
@@ -348,7 +440,7 @@ export class FileTreeMemoryStore {
348
440
  return this._registry
349
441
  .convert(envelope.kind, body)
350
442
  .withErrorFormat((msg) => `memory put '${envelope.id}': invalid body: ${msg}`)
351
- .onSuccess(() => this._codecFor(envelope.kind))
443
+ .onSuccess(() => this._codec(envelope.kind))
352
444
  .thenOnSuccess((codec) => codec.encode(envelope.entityId).thenOnSuccess((addr) => {
353
445
  if (addr.isVersioned) {
354
446
  if (!isTemporalIdentityCodec(codec)) {
@@ -391,8 +483,11 @@ export class FileTreeMemoryStore {
391
483
  // mutableFields) actually applies. Content-dedup must never shadow LWW for
392
484
  // the same entity.
393
485
  const duplicate = this._findByContentHash(scope, hash, record.envelope.id);
394
- if (duplicate !== undefined) {
395
- return succeed({ record: duplicate, evicted: [] });
486
+ if (duplicate.isFailure()) {
487
+ return fail(duplicate.message);
488
+ }
489
+ if (duplicate.value !== undefined) {
490
+ return succeed({ record: duplicate.value, evicted: [] });
396
491
  }
397
492
  }
398
493
  return this._readRecord(scope, idStem).thenOnSuccess((existing) => {
@@ -413,9 +508,8 @@ export class FileTreeMemoryStore {
413
508
  // (a replace, not a grow). The same-id `existing` record is threaded
414
509
  // separately into `_buildRecord` for the merge-patch. Knowledge LWW ignores
415
510
  // 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)
511
+ return this._admissionCohort(scope, record.envelope.kind, idStem)
512
+ .onSuccess((cohort) => policy.admit(record, cohort))
419
513
  .thenOnSuccess((decision) => this._admitWrite(record, body, scope, idStem, hash, policy, existing, decision));
420
514
  });
421
515
  }
@@ -527,7 +621,7 @@ export class FileTreeMemoryStore {
527
621
  .onSuccess(() => succeed(record));
528
622
  }
529
623
  async _deleteLocked(kind, entityId) {
530
- return this._codecFor(kind).thenOnSuccess((codec) => codec.encode(entityId).thenOnSuccess((addr) => {
624
+ return this._codec(kind).thenOnSuccess((codec) => codec.encode(entityId).thenOnSuccess((addr) => {
531
625
  if (addr.isVersioned) {
532
626
  if (!isTemporalIdentityCodec(codec)) {
533
627
  return Promise.resolve(fail(`memory delete '${entityId}': codec for versioned kind '${kind}' does not implement the temporal codec interface`));
@@ -565,7 +659,16 @@ export class FileTreeMemoryStore {
565
659
  * written, or fully invalidated / soft-deleted).
566
660
  */
567
661
  _readVersionedCurrent(scope) {
568
- return selectCurrentVersion(this._versionsForEntity(scope));
662
+ // Select over ENVELOPES, then materialize the one winner — which is what
663
+ // `IMemoryStore.get`'s docstring promises and what `selectCurrentVersion`
664
+ // being generic over `IEnvelopeCarrier` exists for. Materializing every
665
+ // version first (as this did) cost N file reads and N body validations to
666
+ // return one record, and made that docstring false.
667
+ const current = selectCurrentVersion(this._index.entries().filter((entry) => entry.scope === scope));
668
+ if (current === undefined) {
669
+ return succeed(undefined);
670
+ }
671
+ return this._resolveRequired(current);
569
672
  }
570
673
  /**
571
674
  * Every persisted version of the entity whose subtree is `scope`. All version
@@ -573,10 +676,13 @@ export class FileTreeMemoryStore {
573
676
  * entityId), so a scope filter over the index isolates one entity's versions.
574
677
  */
575
678
  _versionsForEntity(scope) {
576
- return this._index
679
+ // Selected on the envelope (scope), materialized after — so a versioned write
680
+ // reads only that entity's versions, never the vault. Bounded by the entity's
681
+ // version count.
682
+ return mapResults(this._index
577
683
  .entries()
578
684
  .filter((entry) => entry.scope === scope)
579
- .map((entry) => entry.record);
685
+ .map((entry) => this._resolveRequired(entry)));
580
686
  }
581
687
  /**
582
688
  * Versioned write (invalidate-don't-delete). Builds the new version's content
@@ -598,7 +704,11 @@ export class FileTreeMemoryStore {
598
704
  // still-current version at snapshot time — normally one, but two-or-more if a
599
705
  // prior invalidation partially failed; invalidating all of them lets the write
600
706
  // self-heal a stuck state (P2-7).
601
- const versions = this._versionsForEntity(scope);
707
+ const snapshot = this._versionsForEntity(scope);
708
+ if (snapshot.isFailure()) {
709
+ return fail(snapshot.message);
710
+ }
711
+ const versions = snapshot.value;
602
712
  const priorCurrents = versions.filter(isVersionCurrent);
603
713
  const current = selectCurrentVersion(versions);
604
714
  const policy = this._policyFor(kind);
@@ -724,7 +834,11 @@ export class FileTreeMemoryStore {
724
834
  * builds on it), so a hard delete would defeat the purpose.
725
835
  */
726
836
  async _deleteVersioned(entityId, scope) {
727
- const versions = this._versionsForEntity(scope);
837
+ const snapshot = this._versionsForEntity(scope);
838
+ if (snapshot.isFailure()) {
839
+ return fail(snapshot.message);
840
+ }
841
+ const versions = snapshot.value;
728
842
  const currents = versions.filter(isVersionCurrent);
729
843
  const current = selectCurrentVersion(versions);
730
844
  if (current === undefined) {
@@ -771,10 +885,12 @@ export class FileTreeMemoryStore {
771
885
  * across first-writes and updates.
772
886
  */
773
887
  _admissionCohort(scope, kind, idStem) {
774
- return this._index
888
+ // Envelope-only selection; the cohort is per-(scope, kind) and bounded by the
889
+ // cull cap, so materializing it is small by construction.
890
+ return mapResults(this._index
775
891
  .entries()
776
- .filter((entry) => entry.scope === scope && entry.record.envelope.kind === kind && entry.record.envelope.id !== idStem)
777
- .map((entry) => entry.record);
892
+ .filter((entry) => entry.scope === scope && entry.envelope.kind === kind && entry.envelope.id !== idStem)
893
+ .map((entry) => this._resolveRequired(entry)));
778
894
  }
779
895
  /**
780
896
  * Find a record in `scope` whose `contentHash` equals `hash`, if any,
@@ -782,12 +898,13 @@ export class FileTreeMemoryStore {
782
898
  * dedup skip the same-id record so it does not shadow the LWW update path.
783
899
  */
784
900
  _findByContentHash(scope, hash, excludeId) {
901
+ // The clearest win of the projection: the hash lives on the envelope, so this
902
+ // scans envelopes and reads exactly ONE file — the match — where it used to
903
+ // hold every body in the scope to look at one field.
785
904
  const match = this._index
786
905
  .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;
906
+ .find((entry) => entry.scope === scope && entry.envelope.contentHash === hash && entry.envelope.id !== excludeId);
907
+ return match === undefined ? succeed(undefined) : this._resolveRequired(match);
791
908
  }
792
909
  /**
793
910
  * True when `incoming`'s caller-authored mutable metadata (`tags` / `provenance`)
@@ -813,7 +930,7 @@ export class FileTreeMemoryStore {
813
930
  return this._hasher.computeHash({ kind, body, links });
814
931
  }
815
932
  /**
816
- * The locked body of {@link FileTreeMemoryStore.reconcileRank}.
933
+ * The rank branch of {@link FileTreeMemoryStore.reconcile}, under the write lock.
817
934
  *
818
935
  * @remarks
819
936
  * Re-reads each record's file rather than trusting the in-memory index, for
@@ -842,31 +959,54 @@ export class FileTreeMemoryStore {
842
959
  * another on a reconcile. `_stampRank` itself is reused verbatim, which also
843
960
  * inherits its throw semantics (logged at `warn`, `rank` cleared).
844
961
  */
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
- }
962
+ async _reconcileLocked(kind, artifact) {
963
+ // Envelope-only selection: the walk needs `(scope, id)` and the kind, and
964
+ // each branch materializes only the records it decides to repair.
849
965
  const targets = this._index
850
966
  .entries()
851
- .filter((entry) => entry.record.envelope.kind === kind);
852
- let restamped = 0;
967
+ .filter((entry) => entry.envelope.kind === kind);
968
+ if (artifact === 'rank') {
969
+ return this._reconcileRankLocked(kind, targets);
970
+ }
971
+ return reconcileVectors({
972
+ kind,
973
+ artifact,
974
+ targets,
975
+ maintenance: this._vectors,
976
+ embedsKind: (k) => this.embedsKind(k),
977
+ resolve: (scope, id) => this._readRecord(scope, id),
978
+ stampRef: (scope, id, ref) => this._rewriteEnvelope(scope, id, (r) => r.envelope.embeddingRef === ref
979
+ ? undefined
980
+ : { envelope: Object.assign(Object.assign({}, r.envelope), { embeddingRef: ref }), body: r.body })
981
+ });
982
+ }
983
+ /** The rank branch of {@link FileTreeMemoryStore._reconcileLocked}. */
984
+ async _reconcileRankLocked(kind, targets) {
985
+ if (!this._rankProjectors.has(kind)) {
986
+ return fail(`memory reconcile '${kind}' rank: no rank projector is registered for this kind`);
987
+ }
988
+ // No materialization at all: `_rewriteEnvelope` re-reads each record itself.
989
+ let repaired = 0;
853
990
  for (const target of targets) {
854
- const applied = this._restampOne(target.scope, target.record.envelope.id);
991
+ const applied = this._rewriteEnvelope(target.scope, target.envelope.id, (r) => {
992
+ const stamped = this._stampRank(r);
993
+ return stamped.envelope.rank === r.envelope.rank ? undefined : stamped;
994
+ });
855
995
  if (applied.isFailure()) {
856
- return fail(`memory reconcileRank '${kind}': ${applied.message}`);
996
+ return fail(`memory reconcile '${kind}' rank: ${applied.message}`);
857
997
  }
858
998
  if (applied.value) {
859
- restamped++;
999
+ repaired++;
860
1000
  }
861
1001
  }
862
- return succeed(restamped);
1002
+ return succeed({ artifact: 'rank', kind, examined: targets.length, repaired, failed: [] });
863
1003
  }
864
1004
  /**
865
1005
  * Re-apply the rank projector to one record on disk. Returns whether `rank`
866
1006
  * actually changed — an unchanged rank writes nothing, so a reconcile over an
867
1007
  * already-consistent store touches no files.
868
1008
  */
869
- _restampOne(scope, id) {
1009
+ _rewriteEnvelope(scope, id, mutate) {
870
1010
  return this._resolveScopeDir(scope).onSuccess((scopeDir) => {
871
1011
  /* c8 ignore next 3 - defensive: the scope dir exists for any indexed record */
872
1012
  if (scopeDir === undefined) {
@@ -894,12 +1034,16 @@ export class FileTreeMemoryStore {
894
1034
  .onSuccess((parsed) => splitFrontmatter(raw)
895
1035
  .withErrorFormat((msg) => `'${id}': ${msg}`)
896
1036
  .onSuccess((parts) => {
897
- const before = parsed.envelope.rank;
898
- const stamped = this._stampRank({
1037
+ // The mutator says "nothing to change" with `undefined` rather
1038
+ // than by returning an equal record: persisting unconditionally
1039
+ // would bump `updated` on every record of the kind, trading a
1040
+ // wrong value for a wrong timestamp, and a deep comparison here
1041
+ // would have to know which fields each caller touches.
1042
+ const stamped = mutate({
899
1043
  envelope: parsed.envelope,
900
1044
  body: parts.body
901
1045
  });
902
- if (stamped.envelope.rank === before) {
1046
+ if (stamped === undefined) {
903
1047
  return succeed(false);
904
1048
  }
905
1049
  return this._persist(stamped, scope, id).onSuccess(() => succeed(true));
@@ -938,14 +1082,6 @@ export class FileTreeMemoryStore {
938
1082
  return { envelope: Object.assign(Object.assign({}, record.envelope), { rank: undefined }), body: record.body };
939
1083
  }
940
1084
  }
941
- _codecFor(kind) {
942
- var _a;
943
- const codec = (_a = this._codecs.get(kind)) !== null && _a !== void 0 ? _a : this._defaultCodec;
944
- if (codec === undefined) {
945
- return fail(`no identity codec registered for kind '${kind}'`);
946
- }
947
- return succeed(codec);
948
- }
949
1085
  _policyFor(kind) {
950
1086
  var _a;
951
1087
  return (_a = this._writePolicies.get(kind)) !== null && _a !== void 0 ? _a : this._defaultPolicy;
@@ -994,18 +1130,7 @@ export class FileTreeMemoryStore {
994
1130
  * verbatim downstream (e.g. merge-into re-addressing). Cross-check it here.
995
1131
  */
996
1132
  _verifyLoaded(scope, file, record) {
997
- if (record.envelope.id !== file.baseName) {
998
- return fail(`memory file '${file.absolutePath}': envelope id '${record.envelope.id}' does not match filename stem '${file.baseName}'`);
999
- }
1000
- return this._codecFor(record.envelope.kind)
1001
- .onSuccess((codec) => codec.verifyRoundTrip(scope, file.baseName).onSuccess(() => codec.decode(scope, file.baseName)))
1002
- .withErrorFormat((msg) => `memory file '${file.absolutePath}': ${msg}`)
1003
- .onSuccess((decodedEntityId) => {
1004
- if (decodedEntityId !== record.envelope.entityId) {
1005
- return fail(`memory file '${file.absolutePath}': envelope entityId '${record.envelope.entityId}' does not match scope-derived entityId '${decodedEntityId}'`);
1006
- }
1007
- return succeed(record);
1008
- });
1133
+ return verifyLoadedIdentity(this._codec(record.envelope.kind), scope, file, record);
1009
1134
  }
1010
1135
  /**
1011
1136
  * Resolve the directory for a scope, returning `undefined` when it does not
@@ -1095,8 +1220,8 @@ export class FileTreeMemoryStore {
1095
1220
  _initialIndex(onRecordError) {
1096
1221
  return this._collectEntries(this._root, [], onRecordError).onSuccess((entries) => this._index.rebuild(entries).onSuccess(() => {
1097
1222
  for (const entry of entries) {
1098
- if (entry.record.envelope.seq > this._seq) {
1099
- this._seq = entry.record.envelope.seq;
1223
+ if (entry.envelope.seq > this._seq) {
1224
+ this._seq = entry.envelope.seq;
1100
1225
  }
1101
1226
  }
1102
1227
  return succeed(true);
@@ -1137,11 +1262,17 @@ export class FileTreeMemoryStore {
1137
1262
  * failure passes through untouched so the historical error is byte-identical.
1138
1263
  */
1139
1264
  _loadRecordFile(scope, child, onRecordError) {
1140
- return child
1265
+ return (child
1141
1266
  .getRawContents()
1142
1267
  .onSuccess((raw) => parseMemoryFile(raw, this._registry))
1143
1268
  .onSuccess((parsedRecord) => this._verifyLoaded(scope, child, parsedRecord))
1144
- .onSuccess((verified) => succeed([{ scope, record: verified }]))
1269
+ // Parse validate → PROJECT → discard, per file. The body is read and
1270
+ // fully validated (which is what gives `onRecordError` its meaning), then
1271
+ // dropped here rather than carried into the index. Peak body residency
1272
+ // across the whole open is therefore ONE record, not N — which is the
1273
+ // resident-memory moment this whole surface exists to fix, and it is why
1274
+ // the projection had to reach `rebuild` and not just the read methods.
1275
+ .onSuccess((verified) => succeed([{ scope, envelope: verified.envelope }]))
1145
1276
  .onFailure((message) => {
1146
1277
  if (onRecordError === 'skip') {
1147
1278
  const path = `${scope}/${child.name}`;
@@ -1150,7 +1281,7 @@ export class FileTreeMemoryStore {
1150
1281
  this._warnSwallowed(error);
1151
1282
  }
1152
1283
  return fail(message);
1153
- });
1284
+ }));
1154
1285
  }
1155
1286
  }
1156
1287
  /**