@fgv/ts-agent-memory 5.1.0-39 → 5.1.0-41

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 (210) hide show
  1. package/.rush/temp/{b82cf6bdece20481260e6bab946179eeec9d7b46.tar.log → cbbdbe09515171b4eba8f2592be2dace1e4e8142.tar.log} +14 -2
  2. package/.rush/temp/chunked-rush-logs/ts-agent-memory.build.chunks.jsonl +2 -2
  3. package/.rush/temp/operation/build/all.log +2 -2
  4. package/.rush/temp/operation/build/log-chunks.jsonl +2 -2
  5. package/.rush/temp/operation/build/state.json +1 -1
  6. package/dist/packlets/converters/envelopeConverter.js +17 -3
  7. package/dist/packlets/converters/envelopeConverter.js.map +1 -1
  8. package/dist/packlets/index/memoryIndex.js +58 -10
  9. package/dist/packlets/index/memoryIndex.js.map +1 -1
  10. package/dist/packlets/ingest/cycleGuard.js +13 -6
  11. package/dist/packlets/ingest/cycleGuard.js.map +1 -1
  12. package/dist/packlets/ingest/hostStages.js.map +1 -1
  13. package/dist/packlets/ingest/model.js.map +1 -1
  14. package/dist/packlets/ingest/orchestrator.js +94 -49
  15. package/dist/packlets/ingest/orchestrator.js.map +1 -1
  16. package/dist/packlets/retrieve/hybridRetriever.js +13 -2
  17. package/dist/packlets/retrieve/hybridRetriever.js.map +1 -1
  18. package/dist/packlets/retrieve/linkTraversalRetriever.js +46 -57
  19. package/dist/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  20. package/dist/packlets/retrieve/recencyRetriever.js +3 -3
  21. package/dist/packlets/retrieve/recencyRetriever.js.map +1 -1
  22. package/dist/packlets/retrieve/retriever.js +50 -7
  23. package/dist/packlets/retrieve/retriever.js.map +1 -1
  24. package/dist/packlets/retrieve/semanticRetriever.js +9 -3
  25. package/dist/packlets/retrieve/semanticRetriever.js.map +1 -1
  26. package/dist/packlets/retrieve/structuredFilterRetriever.js +3 -3
  27. package/dist/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  28. package/dist/packlets/retrieve/tagRetriever.js +3 -3
  29. package/dist/packlets/retrieve/tagRetriever.js.map +1 -1
  30. package/dist/packlets/retrieve/temporalRetrievers.js +3 -3
  31. package/dist/packlets/retrieve/temporalRetrievers.js.map +1 -1
  32. package/dist/packlets/store/fileTreeMemoryStore.js +72 -16
  33. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  34. package/dist/packlets/tools/memoryTools.js +104 -21
  35. package/dist/packlets/tools/memoryTools.js.map +1 -1
  36. package/dist/packlets/types/envelope.js +13 -1
  37. package/dist/packlets/types/envelope.js.map +1 -1
  38. package/dist/packlets/vector/inMemoryCosineIndex.js +22 -17
  39. package/dist/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  40. package/dist/packlets/vector/vectorIndex.js.map +1 -1
  41. package/dist/test/unit/converters/antagonistRoundTrip.test.js +3 -3
  42. package/dist/test/unit/converters/antagonistRoundTrip.test.js.map +1 -1
  43. package/dist/test/unit/converters/envelopeConverter.test.js +125 -8
  44. package/dist/test/unit/converters/envelopeConverter.test.js.map +1 -1
  45. package/dist/test/unit/index/memoryIndex.test.js +87 -25
  46. package/dist/test/unit/index/memoryIndex.test.js.map +1 -1
  47. package/dist/test/unit/ingest/antagonistCycleAndParity.test.js +20 -16
  48. package/dist/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -1
  49. package/dist/test/unit/ingest/cycleGuard.test.js +28 -1
  50. package/dist/test/unit/ingest/cycleGuard.test.js.map +1 -1
  51. package/dist/test/unit/ingest/orchestrator.test.js +187 -45
  52. package/dist/test/unit/ingest/orchestrator.test.js.map +1 -1
  53. package/dist/test/unit/retrieve/linkTraversalRetriever.test.js +106 -31
  54. package/dist/test/unit/retrieve/linkTraversalRetriever.test.js.map +1 -1
  55. package/dist/test/unit/retrieve/retrievers.test.js +299 -37
  56. package/dist/test/unit/retrieve/retrievers.test.js.map +1 -1
  57. package/dist/test/unit/store/embedOnWrite.test.js +69 -12
  58. package/dist/test/unit/store/embedOnWrite.test.js.map +1 -1
  59. package/dist/test/unit/store/listScoped.test.js +109 -0
  60. package/dist/test/unit/store/listScoped.test.js.map +1 -0
  61. package/dist/test/unit/store/rankAxis.test.js +254 -0
  62. package/dist/test/unit/store/rankAxis.test.js.map +1 -0
  63. package/dist/test/unit/tools/memoryTools.test.js +280 -11
  64. package/dist/test/unit/tools/memoryTools.test.js.map +1 -1
  65. package/dist/test/unit/types/writePolicy.test.js +9 -2
  66. package/dist/test/unit/types/writePolicy.test.js.map +1 -1
  67. package/dist/test/unit/vector/inMemoryCosineIndex.test.js +95 -35
  68. package/dist/test/unit/vector/inMemoryCosineIndex.test.js.map +1 -1
  69. package/dist/test/unit/vector/vectorIndex.test.js +24 -15
  70. package/dist/test/unit/vector/vectorIndex.test.js.map +1 -1
  71. package/dist/ts-agent-memory.d.ts +384 -97
  72. package/etc/ts-agent-memory.api.md +64 -22
  73. package/lib/packlets/converters/envelopeConverter.d.ts +8 -1
  74. package/lib/packlets/converters/envelopeConverter.d.ts.map +1 -1
  75. package/lib/packlets/converters/envelopeConverter.js +18 -4
  76. package/lib/packlets/converters/envelopeConverter.js.map +1 -1
  77. package/lib/packlets/index/memoryIndex.d.ts +42 -10
  78. package/lib/packlets/index/memoryIndex.d.ts.map +1 -1
  79. package/lib/packlets/index/memoryIndex.js +58 -10
  80. package/lib/packlets/index/memoryIndex.js.map +1 -1
  81. package/lib/packlets/ingest/cycleGuard.d.ts +5 -5
  82. package/lib/packlets/ingest/cycleGuard.d.ts.map +1 -1
  83. package/lib/packlets/ingest/cycleGuard.js +13 -6
  84. package/lib/packlets/ingest/cycleGuard.js.map +1 -1
  85. package/lib/packlets/ingest/hostStages.d.ts +3 -3
  86. package/lib/packlets/ingest/hostStages.d.ts.map +1 -1
  87. package/lib/packlets/ingest/hostStages.js.map +1 -1
  88. package/lib/packlets/ingest/model.d.ts +25 -14
  89. package/lib/packlets/ingest/model.d.ts.map +1 -1
  90. package/lib/packlets/ingest/model.js.map +1 -1
  91. package/lib/packlets/ingest/orchestrator.d.ts +18 -4
  92. package/lib/packlets/ingest/orchestrator.d.ts.map +1 -1
  93. package/lib/packlets/ingest/orchestrator.js +93 -48
  94. package/lib/packlets/ingest/orchestrator.js.map +1 -1
  95. package/lib/packlets/retrieve/hybridRetriever.d.ts.map +1 -1
  96. package/lib/packlets/retrieve/hybridRetriever.js +12 -1
  97. package/lib/packlets/retrieve/hybridRetriever.js.map +1 -1
  98. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts +18 -23
  99. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts.map +1 -1
  100. package/lib/packlets/retrieve/linkTraversalRetriever.js +45 -56
  101. package/lib/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  102. package/lib/packlets/retrieve/recencyRetriever.js +2 -2
  103. package/lib/packlets/retrieve/recencyRetriever.js.map +1 -1
  104. package/lib/packlets/retrieve/retriever.d.ts +70 -11
  105. package/lib/packlets/retrieve/retriever.d.ts.map +1 -1
  106. package/lib/packlets/retrieve/retriever.js +52 -7
  107. package/lib/packlets/retrieve/retriever.js.map +1 -1
  108. package/lib/packlets/retrieve/semanticRetriever.d.ts.map +1 -1
  109. package/lib/packlets/retrieve/semanticRetriever.js +9 -3
  110. package/lib/packlets/retrieve/semanticRetriever.js.map +1 -1
  111. package/lib/packlets/retrieve/structuredFilterRetriever.js +2 -2
  112. package/lib/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  113. package/lib/packlets/retrieve/tagRetriever.js +2 -2
  114. package/lib/packlets/retrieve/tagRetriever.js.map +1 -1
  115. package/lib/packlets/retrieve/temporalRetrievers.js +3 -3
  116. package/lib/packlets/retrieve/temporalRetrievers.js.map +1 -1
  117. package/lib/packlets/store/fileTreeMemoryStore.d.ts +57 -3
  118. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  119. package/lib/packlets/store/fileTreeMemoryStore.js +72 -16
  120. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  121. package/lib/packlets/tools/memoryTools.d.ts +24 -0
  122. package/lib/packlets/tools/memoryTools.d.ts.map +1 -1
  123. package/lib/packlets/tools/memoryTools.js +104 -21
  124. package/lib/packlets/tools/memoryTools.js.map +1 -1
  125. package/lib/packlets/types/envelope.d.ts +61 -8
  126. package/lib/packlets/types/envelope.d.ts.map +1 -1
  127. package/lib/packlets/types/envelope.js +14 -0
  128. package/lib/packlets/types/envelope.js.map +1 -1
  129. package/lib/packlets/vector/inMemoryCosineIndex.d.ts +9 -5
  130. package/lib/packlets/vector/inMemoryCosineIndex.d.ts.map +1 -1
  131. package/lib/packlets/vector/inMemoryCosineIndex.js +22 -17
  132. package/lib/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  133. package/lib/packlets/vector/vectorIndex.d.ts +46 -20
  134. package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
  135. package/lib/packlets/vector/vectorIndex.js.map +1 -1
  136. package/lib/test/unit/converters/antagonistRoundTrip.test.js +3 -3
  137. package/lib/test/unit/converters/antagonistRoundTrip.test.js.map +1 -1
  138. package/lib/test/unit/converters/envelopeConverter.test.js +124 -7
  139. package/lib/test/unit/converters/envelopeConverter.test.js.map +1 -1
  140. package/lib/test/unit/index/memoryIndex.test.js +86 -24
  141. package/lib/test/unit/index/memoryIndex.test.js.map +1 -1
  142. package/lib/test/unit/ingest/antagonistCycleAndParity.test.js +20 -16
  143. package/lib/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -1
  144. package/lib/test/unit/ingest/cycleGuard.test.js +28 -1
  145. package/lib/test/unit/ingest/cycleGuard.test.js.map +1 -1
  146. package/lib/test/unit/ingest/orchestrator.test.js +186 -44
  147. package/lib/test/unit/ingest/orchestrator.test.js.map +1 -1
  148. package/lib/test/unit/retrieve/linkTraversalRetriever.test.js +106 -31
  149. package/lib/test/unit/retrieve/linkTraversalRetriever.test.js.map +1 -1
  150. package/lib/test/unit/retrieve/retrievers.test.js +298 -36
  151. package/lib/test/unit/retrieve/retrievers.test.js.map +1 -1
  152. package/lib/test/unit/store/embedOnWrite.test.js +68 -11
  153. package/lib/test/unit/store/embedOnWrite.test.js.map +1 -1
  154. package/lib/test/unit/store/listScoped.test.d.ts +2 -0
  155. package/lib/test/unit/store/listScoped.test.d.ts.map +1 -0
  156. package/lib/test/unit/store/listScoped.test.js +111 -0
  157. package/lib/test/unit/store/listScoped.test.js.map +1 -0
  158. package/lib/test/unit/store/rankAxis.test.d.ts +2 -0
  159. package/lib/test/unit/store/rankAxis.test.d.ts.map +1 -0
  160. package/lib/test/unit/store/rankAxis.test.js +256 -0
  161. package/lib/test/unit/store/rankAxis.test.js.map +1 -0
  162. package/lib/test/unit/tools/memoryTools.test.js +280 -11
  163. package/lib/test/unit/tools/memoryTools.test.js.map +1 -1
  164. package/lib/test/unit/types/writePolicy.test.js +9 -2
  165. package/lib/test/unit/types/writePolicy.test.js.map +1 -1
  166. package/lib/test/unit/vector/inMemoryCosineIndex.test.js +95 -35
  167. package/lib/test/unit/vector/inMemoryCosineIndex.test.js.map +1 -1
  168. package/lib/test/unit/vector/vectorIndex.test.js +24 -15
  169. package/lib/test/unit/vector/vectorIndex.test.js.map +1 -1
  170. package/package.json +7 -7
  171. package/rush-logs/ts-agent-memory.build.cache.log +1 -1
  172. package/rush-logs/ts-agent-memory.build.log +2 -2
  173. package/src/packlets/converters/envelopeConverter.ts +27 -4
  174. package/src/packlets/index/memoryIndex.ts +86 -22
  175. package/src/packlets/ingest/cycleGuard.ts +22 -11
  176. package/src/packlets/ingest/hostStages.ts +3 -3
  177. package/src/packlets/ingest/model.ts +25 -14
  178. package/src/packlets/ingest/orchestrator.ts +143 -67
  179. package/src/packlets/retrieve/hybridRetriever.ts +14 -1
  180. package/src/packlets/retrieve/linkTraversalRetriever.ts +51 -62
  181. package/src/packlets/retrieve/recencyRetriever.ts +3 -3
  182. package/src/packlets/retrieve/retriever.ts +97 -13
  183. package/src/packlets/retrieve/semanticRetriever.ts +10 -5
  184. package/src/packlets/retrieve/structuredFilterRetriever.ts +3 -3
  185. package/src/packlets/retrieve/tagRetriever.ts +3 -3
  186. package/src/packlets/retrieve/temporalRetrievers.ts +3 -3
  187. package/src/packlets/store/fileTreeMemoryStore.ts +117 -12
  188. package/src/packlets/tools/memoryTools.ts +152 -25
  189. package/src/packlets/types/envelope.ts +66 -8
  190. package/src/packlets/vector/inMemoryCosineIndex.ts +45 -22
  191. package/src/packlets/vector/vectorIndex.ts +47 -20
  192. package/src/test/unit/converters/antagonistRoundTrip.test.ts +3 -3
  193. package/src/test/unit/converters/envelopeConverter.test.ts +168 -11
  194. package/src/test/unit/index/memoryIndex.test.ts +99 -14
  195. package/src/test/unit/ingest/antagonistCycleAndParity.test.ts +23 -18
  196. package/src/test/unit/ingest/cycleGuard.test.ts +44 -2
  197. package/src/test/unit/ingest/orchestrator.test.ts +234 -41
  198. package/src/test/unit/retrieve/linkTraversalRetriever.test.ts +134 -35
  199. package/src/test/unit/retrieve/retrievers.test.ts +381 -25
  200. package/src/test/unit/store/embedOnWrite.test.ts +83 -11
  201. package/src/test/unit/store/listScoped.test.ts +138 -0
  202. package/src/test/unit/store/rankAxis.test.ts +349 -0
  203. package/src/test/unit/tools/memoryTools.test.ts +362 -13
  204. package/src/test/unit/types/writePolicy.test.ts +11 -2
  205. package/src/test/unit/vector/inMemoryCosineIndex.test.ts +115 -39
  206. package/src/test/unit/vector/vectorIndex.test.ts +33 -17
  207. package/temp/build/lint/_eslint-5eVG3S6w.json +41 -33
  208. package/temp/build/typescript/ts_8nwakTlr.json +1 -1
  209. package/temp/ts-agent-memory.api.json +1080 -136
  210. package/temp/ts-agent-memory.api.md +64 -22
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { Result, fail, succeed } from '@fgv/ts-utils';
7
- import { IMemoryRecord, Kind, MemoryId, MemoryScopeKey, Tag } from '../types';
7
+ import { IEdgeTarget, IMemoryRecord, Kind, MemoryScopeKey, Tag } from '../types';
8
8
  import { IIndexedMemoryRecord } from '../index';
9
9
 
10
10
  /**
@@ -34,12 +34,24 @@ export interface IMemoryQuery {
34
34
  readonly scope?: MemoryScopeKey;
35
35
  /** Restrict to records carrying this tag (exact match). */
36
36
  readonly tag?: Tag;
37
- /** Restrict to records of this kind. */
37
+ /**
38
+ * Restrict to records of this kind — the single-kind shorthand for
39
+ * {@link IMemoryQuery.kinds | kinds}. When both are set they compose as AND
40
+ * (the record's kind must satisfy both), so `kind` must itself be a member of
41
+ * `kinds` for anything to match.
42
+ */
38
43
  readonly kind?: Kind;
39
- /** Restrict to records linked FROM this id (outbound). */
40
- readonly linkedFrom?: MemoryId;
41
- /** Restrict to records linked TO this id (inbound / backlinks). */
42
- readonly linkedTo?: MemoryId;
44
+ /**
45
+ * Restrict to records in ANY of these kinds — the general (multi-kind) form of
46
+ * {@link IMemoryQuery.kind | kind}. Absent no kind-set constraint (today's
47
+ * behavior). An explicit empty array `[]` matches NOTHING (mirroring the
48
+ * non-positive-`limit` "explicit empty" convention), never "match all".
49
+ */
50
+ readonly kinds?: ReadonlyArray<Kind>;
51
+ /** Restrict to records linked FROM this scope-qualified seed (outbound). */
52
+ readonly linkedFrom?: IEdgeTarget;
53
+ /** Restrict to records linked TO this scope-qualified seed (inbound / backlinks). */
54
+ readonly linkedTo?: IEdgeTarget;
43
55
  /** BFS hop count for link traversal. Default: 1. */
44
56
  readonly hops?: number;
45
57
  /**
@@ -56,8 +68,32 @@ export interface IMemoryQuery {
56
68
  * `Result.fail` — never a silent empty.
57
69
  */
58
70
  readonly asOf?: number;
71
+ /**
72
+ * Ordering for the result set. `'recency'` (the default when absent — today's
73
+ * exact behavior) orders most-recently-updated first; `'rank'` orders by the
74
+ * store-computed {@link IMemoryEnvelope.rank} descending (records with an absent
75
+ * `rank` last), with recency as the tiebreak. Combined with `{ limit, offset }`
76
+ * this yields a bounded top-M rank-ordered page with no full-vault scan.
77
+ *
78
+ * @remarks
79
+ * `orderBy` governs the ordered non-semantic retrievers (recency / tag /
80
+ * structured-filter / link-traversal) and the {@link HybridRetriever}'s
81
+ * post-merge ordering. The {@link SemanticRetriever} is the sole exception: it
82
+ * preserves its native vector-similarity order regardless of `orderBy` —
83
+ * re-sorting semantic hits by `rank` would discard the similarity ranking that
84
+ * is the whole point of that path; a consumer that wants rank ordering uses a
85
+ * non-semantic query.
86
+ */
87
+ readonly orderBy?: 'recency' | 'rank';
59
88
  /** Maximum records to return. Applied after all other filters. */
60
89
  readonly limit?: number;
90
+ /**
91
+ * Records to skip after ordering, before `limit` — so `{ offset, limit }` is a
92
+ * stable page window over the ordered result set. Default 0. A non-positive or
93
+ * absent offset is today's behavior (no skip); an offset past the end yields an
94
+ * empty page, never a throw.
95
+ */
96
+ readonly offset?: number;
61
97
  /** Arbitrary predicate applied after the scope / kind / tag pre-filter. */
62
98
  readonly filter?: (record: IMemoryRecord<unknown>) => boolean;
63
99
  }
@@ -152,6 +188,40 @@ export function recencyCompare(a: IMemoryRecord<unknown>, b: IMemoryRecord<unkno
152
188
  return byUpdated !== 0 ? byUpdated : b.envelope.seq - a.envelope.seq;
153
189
  }
154
190
 
191
+ /**
192
+ * Rank comparator: store-computed {@link IMemoryEnvelope.rank} descending, with
193
+ * {@link recencyCompare} as the tiebreak. Records with an absent `rank` sort LAST
194
+ * (after every ranked record), then by recency among themselves. Mirrors the
195
+ * index's rank-view ordering.
196
+ * @public
197
+ */
198
+ export function rankCompare(a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>): number {
199
+ const ra: number | undefined = a.envelope.rank;
200
+ const rb: number | undefined = b.envelope.rank;
201
+ if (ra === undefined && rb !== undefined) {
202
+ return 1;
203
+ }
204
+ if (rb === undefined && ra !== undefined) {
205
+ return -1;
206
+ }
207
+ if (ra !== undefined && rb !== undefined && ra !== rb) {
208
+ return rb - ra;
209
+ }
210
+ return recencyCompare(a, b);
211
+ }
212
+
213
+ /**
214
+ * Select the record comparator for a query's {@link IMemoryQuery.orderBy | orderBy}
215
+ * axis: {@link rankCompare} for `'rank'`, {@link recencyCompare} otherwise (the
216
+ * default, byte-identical to the pre-`orderBy` behavior).
217
+ * @public
218
+ */
219
+ export function orderingCompare(
220
+ orderBy?: IMemoryQuery['orderBy']
221
+ ): (a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>) => number {
222
+ return orderBy === 'rank' ? rankCompare : recencyCompare;
223
+ }
224
+
155
225
  /**
156
226
  * Whether an indexed entry satisfies a query's scope / kind / tag / predicate
157
227
  * pre-filter (the axes shared by every v1 retriever). The `semantic` / `asOf` /
@@ -165,6 +235,9 @@ export function indexedRecordMatchesQuery(entry: IIndexedMemoryRecord, query: IM
165
235
  if (query.kind !== undefined && entry.record.envelope.kind !== query.kind) {
166
236
  return false;
167
237
  }
238
+ if (query.kinds !== undefined && !query.kinds.includes(entry.record.envelope.kind)) {
239
+ return false;
240
+ }
168
241
  if (query.tag !== undefined && !entry.record.envelope.tags.includes(query.tag)) {
169
242
  return false;
170
243
  }
@@ -187,21 +260,32 @@ export function selectByQuery(
187
260
  }
188
261
 
189
262
  /**
190
- * Truncate to `query.limit` records (a no-op when `limit` is absent). Applied
191
- * last, after ordering, so it always takes the top-N of the ordered result. A
192
- * non-positive `limit` is public query input and means "no records" it returns
193
- * an empty array rather than letting a negative value slip into `slice`.
263
+ * Apply the `{ offset, limit }` page window to an ordered record set. Applied
264
+ * last, after ordering, so it always takes a stable window of the ordered
265
+ * result. `offset` is applied first (records to skip), then `limit` (top-N of
266
+ * the remainder).
267
+ *
268
+ * @remarks
269
+ * Both bounds are public query input and are guarded against non-positive
270
+ * values slipping into `slice`:
271
+ * - `offset` absent or non-positive → no skip (today's behavior). An offset past
272
+ * the end yields an empty page rather than a throw.
273
+ * - `limit` absent → no truncation; a non-positive `limit` means "no records"
274
+ * and returns an empty array.
194
275
  * @public
195
276
  */
196
277
  export function limitRecords(
197
278
  records: ReadonlyArray<IMemoryRecord<unknown>>,
198
- limit?: number
279
+ limit?: number,
280
+ offset?: number
199
281
  ): ReadonlyArray<IMemoryRecord<unknown>> {
282
+ const skip: number = offset !== undefined && offset > 0 ? offset : 0;
283
+ const windowed: ReadonlyArray<IMemoryRecord<unknown>> = skip > 0 ? records.slice(skip) : records;
200
284
  if (limit === undefined) {
201
- return records;
285
+ return windowed;
202
286
  }
203
287
  if (limit <= 0) {
204
288
  return [];
205
289
  }
206
- return records.length > limit ? records.slice(0, limit) : records;
290
+ return windowed.length > limit ? windowed.slice(0, limit) : windowed;
207
291
  }
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { Result, fail, succeed } from '@fgv/ts-utils';
7
- import { IMemoryRecord, MemoryId } from '../types';
7
+ import { IMemoryRecord, edgeTargetKey } from '../types';
8
8
  import { IIndexedMemoryRecord, IMemoryIndex } from '../index';
9
9
  import { IVectorIndex, IVectorQueryHit } from '../vector';
10
10
  import {
@@ -118,17 +118,22 @@ export class SemanticRetriever implements IMemoryRetriever {
118
118
  if (hits.isFailure()) {
119
119
  return fail(hits.message);
120
120
  }
121
- const byId: Map<MemoryId, IIndexedMemoryRecord> = new Map(
122
- this._index.entries().map((entry) => [entry.record.envelope.id, entry])
121
+ // Key by the canonical scope-qualified target so a hit re-resolves to the
122
+ // exact record it scored against — a bare id would alias two records that
123
+ // share a filename stem across scopes.
124
+ const byKey: Map<string, IIndexedMemoryRecord> = new Map(
125
+ this._index
126
+ .entries()
127
+ .map((entry) => [edgeTargetKey({ scope: entry.scope, id: entry.record.envelope.id }), entry])
123
128
  );
124
129
  const records: IMemoryRecord<unknown>[] = [];
125
130
  for (const hit of hits.value) {
126
- const entry: IIndexedMemoryRecord | undefined = byId.get(hit.id);
131
+ const entry: IIndexedMemoryRecord | undefined = byKey.get(edgeTargetKey(hit.target));
127
132
  if (entry !== undefined && indexedRecordMatchesQuery(entry, query)) {
128
133
  records.push(entry.record);
129
134
  }
130
135
  }
131
- return succeed(limitRecords(records, query.limit));
136
+ return succeed(limitRecords(records, query.limit, query.offset));
132
137
  }
133
138
 
134
139
  /**
@@ -13,7 +13,7 @@ import {
13
13
  NON_SEMANTIC_CAPABILITIES,
14
14
  guardRetrieverCapabilities,
15
15
  limitRecords,
16
- recencyCompare,
16
+ orderingCompare,
17
17
  selectByQuery
18
18
  } from './retriever';
19
19
 
@@ -49,9 +49,9 @@ export class StructuredFilterRetriever implements IMemoryRetriever {
49
49
  return succeed([]);
50
50
  }
51
51
  const ordered: IMemoryRecord<unknown>[] = selectByQuery(this._index.entries(), query).sort(
52
- recencyCompare
52
+ orderingCompare(query.orderBy)
53
53
  );
54
- return succeed(limitRecords(ordered, query.limit));
54
+ return succeed(limitRecords(ordered, query.limit, query.offset));
55
55
  })
56
56
  );
57
57
  }
@@ -13,7 +13,7 @@ import {
13
13
  NON_SEMANTIC_CAPABILITIES,
14
14
  guardRetrieverCapabilities,
15
15
  limitRecords,
16
- recencyCompare,
16
+ orderingCompare,
17
17
  selectByQuery
18
18
  } from './retriever';
19
19
 
@@ -49,9 +49,9 @@ export class TagRetriever implements IMemoryRetriever {
49
49
  return succeed([]);
50
50
  }
51
51
  const ordered: IMemoryRecord<unknown>[] = selectByQuery(this._index.entries(), query).sort(
52
- recencyCompare
52
+ orderingCompare(query.orderBy)
53
53
  );
54
- return succeed(limitRecords(ordered, query.limit));
54
+ return succeed(limitRecords(ordered, query.limit, query.offset));
55
55
  })
56
56
  );
57
57
  }
@@ -90,7 +90,7 @@ export class CurrentValidRetriever implements IMemoryRetriever {
90
90
  }
91
91
  }
92
92
  selected.sort(recencyCompare);
93
- return succeed(limitRecords(selected, query.limit));
93
+ return succeed(limitRecords(selected, query.limit, query.offset));
94
94
  })
95
95
  );
96
96
  }
@@ -137,7 +137,7 @@ export class AsOfRetriever implements IMemoryRetriever {
137
137
  }
138
138
  }
139
139
  selected.sort(recencyCompare);
140
- return succeed(limitRecords(selected, query.limit));
140
+ return succeed(limitRecords(selected, query.limit, query.offset));
141
141
  })
142
142
  );
143
143
  }
@@ -182,7 +182,7 @@ export class HistoryRetriever implements IMemoryRetriever {
182
182
  history.push(...versions);
183
183
  }
184
184
  history.sort(HistoryRetriever._byValidAtAscending);
185
- return succeed(limitRecords(history, query.limit));
185
+ return succeed(limitRecords(history, query.limit, query.offset));
186
186
  })
187
187
  );
188
188
  }
@@ -10,6 +10,7 @@ import {
10
10
  DEFAULT_DEDUP_SCOPE,
11
11
  DedupScope,
12
12
  EntityId,
13
+ IEdgeTarget,
13
14
  IIdentityCodec,
14
15
  IMemoryEnvelope,
15
16
  IMemoryRecord,
@@ -20,6 +21,7 @@ import {
20
21
  KnowledgeLwwPolicy,
21
22
  MemoryId,
22
23
  MemoryScopeKey,
24
+ RankProjector,
23
25
  Tag,
24
26
  isTemporalIdentityCodec,
25
27
  isTemporalRecord,
@@ -35,7 +37,7 @@ import {
35
37
  MemoryObservationOutcome,
36
38
  MemoryObservationPhase
37
39
  } from '../observe';
38
- import { IVectorIndex, MemoryEmbedder } from '../vector';
40
+ import { IMemoryRecordSource, IScopedMemoryRecord, IVectorIndex, MemoryEmbedder } from '../vector';
39
41
  import { defaultMemoryScopeEncoding } from './scopeEncoding';
40
42
 
41
43
  /** The on-disk extension for a memory record file. */
@@ -84,6 +86,26 @@ export interface IMemoryStore {
84
86
  */
85
87
  list(filter?: IMemoryStoreListFilter): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>>;
86
88
 
89
+ /**
90
+ * List EVERY record in the vault, each paired with its scope-qualified
91
+ * `(scope, id)` address — the projection {@link IMemoryRecordSource} requires.
92
+ * Unlike {@link IMemoryStore.list | list}, it takes no filter (whole-vault) and
93
+ * returns {@link IScopedMemoryRecord}s so a re-index keys each entry on the same
94
+ * scoped target the incremental embed-on-write path uses. Two records that share
95
+ * a filename stem across scopes appear as distinct entries.
96
+ */
97
+ listScoped(): Promise<Result<ReadonlyArray<IScopedMemoryRecord>>>;
98
+
99
+ /**
100
+ * Adapt this store to the {@link IMemoryRecordSource} seam so it can drive
101
+ * {@link IVectorIndex} rebuilds (e.g. `InMemoryCosineIndex.rebuild`). The
102
+ * returned source's `list()` delegates to {@link IMemoryStore.listScoped}. The
103
+ * store cannot implement {@link IMemoryRecordSource} directly because its
104
+ * `list(filter?)` returns bare records (the ergonomic query surface) while the
105
+ * seam's `list()` returns scope-qualified records.
106
+ */
107
+ asRecordSource(): IMemoryRecordSource;
108
+
87
109
  /**
88
110
  * Write a record. Validates the body, computes a content hash, deduplicates
89
111
  * (scope-wide, before policy), applies the kind's {@link IWritePolicy}, stamps
@@ -116,6 +138,19 @@ export interface IFileTreeMemoryStoreCreateParams {
116
138
  readonly writePolicies?: ReadonlyMap<Kind, IWritePolicy>;
117
139
  /** Per-kind identity codecs. */
118
140
  readonly codecs?: ReadonlyMap<Kind, IIdentityCodec>;
141
+ /**
142
+ * Optional per-kind host projector map. When a kind has an entry, the store
143
+ * runs the projector on the fully-resolved (post-merge) record on every put
144
+ * AND every update — in the same pass that recomputes `contentHash` — and
145
+ * stamps the numeric result into {@link IMemoryEnvelope.rank}, which the index's
146
+ * rank view and `orderBy: 'rank'` retrieval sort by (descending, absent last).
147
+ * Absent for a kind → that kind's records carry no `rank`. Purely additive and
148
+ * zero-overhead when unwired: an absent map leaves every write byte-identical.
149
+ * The projector is a host callback — a throw is logged at `warn` and the record
150
+ * is stamped with no `rank`, never failing the write (mirrors the store's other
151
+ * guard-host-callbacks conventions).
152
+ */
153
+ readonly rankProjectors?: ReadonlyMap<Kind, RankProjector>;
119
154
  /** Default codec for kinds without an explicit entry. */
120
155
  readonly defaultCodec?: IIdentityCodec;
121
156
  /** Scope encoding. Defaults to {@link defaultMemoryScopeEncoding}. */
@@ -182,6 +217,7 @@ interface IInternalParams {
182
217
  readonly registry: IRegistry;
183
218
  readonly writePolicies: ReadonlyMap<Kind, IWritePolicy>;
184
219
  readonly codecs: ReadonlyMap<Kind, IIdentityCodec>;
220
+ readonly rankProjectors: ReadonlyMap<Kind, RankProjector>;
185
221
  readonly defaultCodec?: IIdentityCodec;
186
222
  readonly defaultPolicy: IWritePolicy;
187
223
  readonly scopeEncoding: (scope: MemoryScopeKey) => Result<string>;
@@ -212,6 +248,7 @@ export class FileTreeMemoryStore implements IMemoryStore {
212
248
  private readonly _registry: IRegistry;
213
249
  private readonly _writePolicies: ReadonlyMap<Kind, IWritePolicy>;
214
250
  private readonly _codecs: ReadonlyMap<Kind, IIdentityCodec>;
251
+ private readonly _rankProjectors: ReadonlyMap<Kind, RankProjector>;
215
252
  private readonly _defaultCodec: IIdentityCodec | undefined;
216
253
  private readonly _defaultPolicy: IWritePolicy;
217
254
  private readonly _scopeEncoding: (scope: MemoryScopeKey) => Result<string>;
@@ -258,6 +295,7 @@ export class FileTreeMemoryStore implements IMemoryStore {
258
295
  this._registry = params.registry;
259
296
  this._writePolicies = params.writePolicies;
260
297
  this._codecs = params.codecs;
298
+ this._rankProjectors = params.rankProjectors;
261
299
  this._defaultCodec = params.defaultCodec;
262
300
  this._defaultPolicy = params.defaultPolicy;
263
301
  this._scopeEncoding = params.scopeEncoding;
@@ -286,6 +324,7 @@ export class FileTreeMemoryStore implements IMemoryStore {
286
324
  registry: params.registry,
287
325
  writePolicies: params.writePolicies ?? new Map<Kind, IWritePolicy>(),
288
326
  codecs: params.codecs ?? new Map<Kind, IIdentityCodec>(),
327
+ rankProjectors: params.rankProjectors ?? new Map<Kind, RankProjector>(),
289
328
  defaultCodec: params.defaultCodec,
290
329
  defaultPolicy,
291
330
  scopeEncoding: params.scopeEncoding ?? defaultMemoryScopeEncoding,
@@ -361,6 +400,25 @@ export class FileTreeMemoryStore implements IMemoryStore {
361
400
  return succeed(FileTreeMemoryStore._projectAsOf(matches, filter.asOf));
362
401
  }
363
402
 
403
+ /** {@inheritDoc IMemoryStore.listScoped} */
404
+ public async listScoped(): Promise<Result<ReadonlyArray<IScopedMemoryRecord>>> {
405
+ // The derived index already carries each record's scope
406
+ // ({@link IIndexedMemoryRecord.scope}), so the scoped projection is a direct
407
+ // map — the record's `(scope, id)` is exactly the address the vector index
408
+ // keys on. No filter/temporal projection: the seam re-embeds the whole vault.
409
+ return succeed(
410
+ this._index.entries().map((entry) => ({
411
+ target: { scope: entry.scope, id: entry.record.envelope.id },
412
+ record: entry.record
413
+ }))
414
+ );
415
+ }
416
+
417
+ /** {@inheritDoc IMemoryStore.asRecordSource} */
418
+ public asRecordSource(): IMemoryRecordSource {
419
+ return { list: (): Promise<Result<ReadonlyArray<IScopedMemoryRecord>>> => this.listScoped() };
420
+ }
421
+
364
422
  /**
365
423
  * Collapse temporal records to the single version valid at `asOf` per entity;
366
424
  * non-temporal records are timeless and pass through unchanged (valid-time
@@ -669,7 +727,8 @@ export class FileTreeMemoryStore implements IMemoryStore {
669
727
  return fail(`memory put: rejected by policy: ${decision.reason}`);
670
728
  }
671
729
  return this._buildRecord(record, body, existing, policy, hash)
672
- .thenOnSuccess((built) => this._embedOnWrite(built))
730
+ .onSuccess((built) => succeed(this._stampRank(built)))
731
+ .thenOnSuccess((built) => this._embedOnWrite(built, scope))
673
732
  .onSuccess((embeddedBuilt) => this._persist(embeddedBuilt, scope, idStem))
674
733
  .thenOnSuccess(async (persisted) => {
675
734
  // Everything after the authoritative `_persist` commit is best-effort and
@@ -679,7 +738,7 @@ export class FileTreeMemoryStore implements IMemoryStore {
679
738
  // it), and only the successfully-evicted ids flow onward;
680
739
  // - vector pruning of the evicted cohort is likewise best-effort.
681
740
  const evicted: ReadonlyArray<MemoryId> = this._applyEvictions(decision, scope);
682
- await this._removeEvictedVectors(evicted);
741
+ await this._removeEvictedVectors(evicted, scope);
683
742
  return succeed({ record: persisted, evicted });
684
743
  });
685
744
  }
@@ -696,12 +755,16 @@ export class FileTreeMemoryStore implements IMemoryStore {
696
755
  * Always succeeds (`Result` is the chain's shape, never a vector-induced
697
756
  * failure).
698
757
  */
699
- private async _embedOnWrite(built: IMemoryRecord<string>): Promise<Result<IMemoryRecord<string>>> {
758
+ private async _embedOnWrite(
759
+ built: IMemoryRecord<string>,
760
+ scope: MemoryScopeKey
761
+ ): Promise<Result<IMemoryRecord<string>>> {
700
762
  if (this._vectorIndex === undefined || this._embed === undefined) {
701
763
  return succeed(built);
702
764
  }
703
765
  const vectorIndex: IVectorIndex = this._vectorIndex;
704
766
  const embed: MemoryEmbedder = this._embed;
767
+ const target: IEdgeTarget = { scope, id: built.envelope.id };
705
768
  const embedded: Result<Float32Array> = await this._tryVectorOp(
706
769
  () => embed(built),
707
770
  `embedding '${built.envelope.id}'`
@@ -710,7 +773,7 @@ export class FileTreeMemoryStore implements IMemoryStore {
710
773
  return succeed(built);
711
774
  }
712
775
  const added: Result<string> = await this._tryVectorOp(
713
- () => vectorIndex.add(built.envelope.id, embedded.value),
776
+ () => vectorIndex.add(target, embedded.value),
714
777
  `vector add for '${built.envelope.id}'`
715
778
  );
716
779
  if (added.isFailure()) {
@@ -744,10 +807,18 @@ export class FileTreeMemoryStore implements IMemoryStore {
744
807
  return evicted;
745
808
  }
746
809
 
747
- /** Best-effort vector removal for each evicted record (never fails the put). */
748
- private async _removeEvictedVectors(evicted: ReadonlyArray<MemoryId>): Promise<void> {
810
+ /**
811
+ * Best-effort vector removal for each evicted record (never fails the put).
812
+ * Every evicted record is in the same `scope` as the incoming write (the
813
+ * cull-oldest cohort is the incoming record's `(scope, kind)` cohort), so that
814
+ * scope qualifies each removal target.
815
+ */
816
+ private async _removeEvictedVectors(
817
+ evicted: ReadonlyArray<MemoryId>,
818
+ scope: MemoryScopeKey
819
+ ): Promise<void> {
749
820
  for (const id of evicted) {
750
- await this._removeVectorBestEffort(id);
821
+ await this._removeVectorBestEffort({ scope, id });
751
822
  }
752
823
  }
753
824
 
@@ -777,12 +848,12 @@ export class FileTreeMemoryStore implements IMemoryStore {
777
848
  * behaves byte-identically. Failures are logged, never surfaced — a committed
778
849
  * delete/eviction must not fail because a derived index could not be pruned.
779
850
  */
780
- private async _removeVectorBestEffort(id: MemoryId): Promise<void> {
851
+ private async _removeVectorBestEffort(target: IEdgeTarget): Promise<void> {
781
852
  if (this._vectorIndex === undefined || this._embed === undefined) {
782
853
  return;
783
854
  }
784
855
  const vectorIndex: IVectorIndex = this._vectorIndex;
785
- await this._tryVectorOp(() => vectorIndex.remove(id), `vector removal for '${id}'`);
856
+ await this._tryVectorOp(() => vectorIndex.remove(target), `vector removal for '${target.id}'`);
786
857
  }
787
858
 
788
859
  /**
@@ -888,7 +959,7 @@ export class FileTreeMemoryStore implements IMemoryStore {
888
959
  return this._deleteFile(scope, idStem)
889
960
  .onSuccess(() => this._index.patch('delete', { scope, record: existing }))
890
961
  .thenOnSuccess(async () => {
891
- await this._removeVectorBestEffort(existing.envelope.id);
962
+ await this._removeVectorBestEffort({ scope, id: existing.envelope.id });
892
963
  return succeed(existing.envelope.id);
893
964
  });
894
965
  });
@@ -982,7 +1053,8 @@ export class FileTreeMemoryStore implements IMemoryStore {
982
1053
  .withErrorFormat((msg) => `memory put '${entityId}': ${msg}`)
983
1054
  .thenOnSuccess((versionStem) =>
984
1055
  this._buildVersionedRecord(record, body, current, policy, hash, versionStem, validAt, now, seq)
985
- .thenOnSuccess((built) => this._embedOnWrite(built))
1056
+ .onSuccess((built) => succeed(this._stampRank(built)))
1057
+ .thenOnSuccess((built) => this._embedOnWrite(built, scope))
986
1058
  .onSuccess((embeddedBuilt) => this._persist(embeddedBuilt, scope, versionStem))
987
1059
  .onSuccess((persisted) =>
988
1060
  this._invalidateCurrents(scope, priorCurrents, validAt, now).onSuccess(() =>
@@ -1241,6 +1313,39 @@ export class FileTreeMemoryStore implements IMemoryStore {
1241
1313
  return this._hasher.computeHash({ kind, body, links });
1242
1314
  }
1243
1315
 
1316
+ /**
1317
+ * Stamp the store-computed {@link IMemoryEnvelope.rank} onto a fully-built,
1318
+ * fully-stamped record by running the kind's registered {@link RankProjector}.
1319
+ * Runs on the SAME resolved (post-merge) record whose `contentHash` was just
1320
+ * computed, so `rank` is always consistent with the current body — no separate
1321
+ * write path and no consumer write-discipline rule. A no-op pass-through
1322
+ * (byte-identical record) when the kind has no projector — the additive,
1323
+ * zero-overhead-when-unwired default. The projector is a host callback: a throw
1324
+ * is logged at `warn` and the record is stamped with NO `rank` (the field is
1325
+ * explicitly cleared, so a throw on an update drops a now-stale prior rank rather
1326
+ * than preserving it), so a ranking bug never loses an authoritative write.
1327
+ */
1328
+ private _stampRank(record: IMemoryRecord<string>): IMemoryRecord<string> {
1329
+ const projector: RankProjector | undefined = this._rankProjectors.get(record.envelope.kind);
1330
+ if (projector === undefined) {
1331
+ return record;
1332
+ }
1333
+ try {
1334
+ const rank: number = projector(record);
1335
+ return { envelope: { ...record.envelope, rank }, body: record.body };
1336
+ } catch (err) {
1337
+ this._warnSwallowed(
1338
+ `memory put '${record.envelope.id}': rank projector threw (swallowed; rank cleared): ${String(err)}`
1339
+ );
1340
+ // Explicitly CLEAR `rank` (not `return record`): on an update the merged
1341
+ // envelope carries the prior version's `rank` (the write-policy merge does
1342
+ // not touch `rank`), so returning it verbatim would keep a stale value
1343
+ // inconsistent with the revised body. Clearing honors the staleness
1344
+ // contract — an uncomputable rank means the record sorts last as "unranked".
1345
+ return { envelope: { ...record.envelope, rank: undefined }, body: record.body };
1346
+ }
1347
+ }
1348
+
1244
1349
  private _codecFor(kind: Kind): Result<IIdentityCodec> {
1245
1350
  const codec: IIdentityCodec | undefined = this._codecs.get(kind) ?? this._defaultCodec;
1246
1351
  if (codec === undefined) {