@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.
- package/.rush/temp/{b82cf6bdece20481260e6bab946179eeec9d7b46.tar.log → cbbdbe09515171b4eba8f2592be2dace1e4e8142.tar.log} +14 -2
- package/.rush/temp/chunked-rush-logs/ts-agent-memory.build.chunks.jsonl +2 -2
- package/.rush/temp/operation/build/all.log +2 -2
- package/.rush/temp/operation/build/log-chunks.jsonl +2 -2
- package/.rush/temp/operation/build/state.json +1 -1
- package/dist/packlets/converters/envelopeConverter.js +17 -3
- package/dist/packlets/converters/envelopeConverter.js.map +1 -1
- package/dist/packlets/index/memoryIndex.js +58 -10
- package/dist/packlets/index/memoryIndex.js.map +1 -1
- package/dist/packlets/ingest/cycleGuard.js +13 -6
- package/dist/packlets/ingest/cycleGuard.js.map +1 -1
- package/dist/packlets/ingest/hostStages.js.map +1 -1
- package/dist/packlets/ingest/model.js.map +1 -1
- package/dist/packlets/ingest/orchestrator.js +94 -49
- package/dist/packlets/ingest/orchestrator.js.map +1 -1
- package/dist/packlets/retrieve/hybridRetriever.js +13 -2
- package/dist/packlets/retrieve/hybridRetriever.js.map +1 -1
- package/dist/packlets/retrieve/linkTraversalRetriever.js +46 -57
- package/dist/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
- package/dist/packlets/retrieve/recencyRetriever.js +3 -3
- package/dist/packlets/retrieve/recencyRetriever.js.map +1 -1
- package/dist/packlets/retrieve/retriever.js +50 -7
- package/dist/packlets/retrieve/retriever.js.map +1 -1
- package/dist/packlets/retrieve/semanticRetriever.js +9 -3
- package/dist/packlets/retrieve/semanticRetriever.js.map +1 -1
- package/dist/packlets/retrieve/structuredFilterRetriever.js +3 -3
- package/dist/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
- package/dist/packlets/retrieve/tagRetriever.js +3 -3
- package/dist/packlets/retrieve/tagRetriever.js.map +1 -1
- package/dist/packlets/retrieve/temporalRetrievers.js +3 -3
- package/dist/packlets/retrieve/temporalRetrievers.js.map +1 -1
- package/dist/packlets/store/fileTreeMemoryStore.js +72 -16
- package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
- package/dist/packlets/tools/memoryTools.js +104 -21
- package/dist/packlets/tools/memoryTools.js.map +1 -1
- package/dist/packlets/types/envelope.js +13 -1
- package/dist/packlets/types/envelope.js.map +1 -1
- package/dist/packlets/vector/inMemoryCosineIndex.js +22 -17
- package/dist/packlets/vector/inMemoryCosineIndex.js.map +1 -1
- package/dist/packlets/vector/vectorIndex.js.map +1 -1
- package/dist/test/unit/converters/antagonistRoundTrip.test.js +3 -3
- package/dist/test/unit/converters/antagonistRoundTrip.test.js.map +1 -1
- package/dist/test/unit/converters/envelopeConverter.test.js +125 -8
- package/dist/test/unit/converters/envelopeConverter.test.js.map +1 -1
- package/dist/test/unit/index/memoryIndex.test.js +87 -25
- package/dist/test/unit/index/memoryIndex.test.js.map +1 -1
- package/dist/test/unit/ingest/antagonistCycleAndParity.test.js +20 -16
- package/dist/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -1
- package/dist/test/unit/ingest/cycleGuard.test.js +28 -1
- package/dist/test/unit/ingest/cycleGuard.test.js.map +1 -1
- package/dist/test/unit/ingest/orchestrator.test.js +187 -45
- package/dist/test/unit/ingest/orchestrator.test.js.map +1 -1
- package/dist/test/unit/retrieve/linkTraversalRetriever.test.js +106 -31
- package/dist/test/unit/retrieve/linkTraversalRetriever.test.js.map +1 -1
- package/dist/test/unit/retrieve/retrievers.test.js +299 -37
- package/dist/test/unit/retrieve/retrievers.test.js.map +1 -1
- package/dist/test/unit/store/embedOnWrite.test.js +69 -12
- package/dist/test/unit/store/embedOnWrite.test.js.map +1 -1
- package/dist/test/unit/store/listScoped.test.js +109 -0
- package/dist/test/unit/store/listScoped.test.js.map +1 -0
- package/dist/test/unit/store/rankAxis.test.js +254 -0
- package/dist/test/unit/store/rankAxis.test.js.map +1 -0
- package/dist/test/unit/tools/memoryTools.test.js +280 -11
- package/dist/test/unit/tools/memoryTools.test.js.map +1 -1
- package/dist/test/unit/types/writePolicy.test.js +9 -2
- package/dist/test/unit/types/writePolicy.test.js.map +1 -1
- package/dist/test/unit/vector/inMemoryCosineIndex.test.js +95 -35
- package/dist/test/unit/vector/inMemoryCosineIndex.test.js.map +1 -1
- package/dist/test/unit/vector/vectorIndex.test.js +24 -15
- package/dist/test/unit/vector/vectorIndex.test.js.map +1 -1
- package/dist/ts-agent-memory.d.ts +384 -97
- package/etc/ts-agent-memory.api.md +64 -22
- package/lib/packlets/converters/envelopeConverter.d.ts +8 -1
- package/lib/packlets/converters/envelopeConverter.d.ts.map +1 -1
- package/lib/packlets/converters/envelopeConverter.js +18 -4
- package/lib/packlets/converters/envelopeConverter.js.map +1 -1
- package/lib/packlets/index/memoryIndex.d.ts +42 -10
- package/lib/packlets/index/memoryIndex.d.ts.map +1 -1
- package/lib/packlets/index/memoryIndex.js +58 -10
- package/lib/packlets/index/memoryIndex.js.map +1 -1
- package/lib/packlets/ingest/cycleGuard.d.ts +5 -5
- package/lib/packlets/ingest/cycleGuard.d.ts.map +1 -1
- package/lib/packlets/ingest/cycleGuard.js +13 -6
- package/lib/packlets/ingest/cycleGuard.js.map +1 -1
- package/lib/packlets/ingest/hostStages.d.ts +3 -3
- package/lib/packlets/ingest/hostStages.d.ts.map +1 -1
- package/lib/packlets/ingest/hostStages.js.map +1 -1
- package/lib/packlets/ingest/model.d.ts +25 -14
- package/lib/packlets/ingest/model.d.ts.map +1 -1
- package/lib/packlets/ingest/model.js.map +1 -1
- package/lib/packlets/ingest/orchestrator.d.ts +18 -4
- package/lib/packlets/ingest/orchestrator.d.ts.map +1 -1
- package/lib/packlets/ingest/orchestrator.js +93 -48
- package/lib/packlets/ingest/orchestrator.js.map +1 -1
- package/lib/packlets/retrieve/hybridRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/hybridRetriever.js +12 -1
- package/lib/packlets/retrieve/hybridRetriever.js.map +1 -1
- package/lib/packlets/retrieve/linkTraversalRetriever.d.ts +18 -23
- package/lib/packlets/retrieve/linkTraversalRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/linkTraversalRetriever.js +45 -56
- package/lib/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
- package/lib/packlets/retrieve/recencyRetriever.js +2 -2
- package/lib/packlets/retrieve/recencyRetriever.js.map +1 -1
- package/lib/packlets/retrieve/retriever.d.ts +70 -11
- package/lib/packlets/retrieve/retriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/retriever.js +52 -7
- package/lib/packlets/retrieve/retriever.js.map +1 -1
- package/lib/packlets/retrieve/semanticRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/semanticRetriever.js +9 -3
- package/lib/packlets/retrieve/semanticRetriever.js.map +1 -1
- package/lib/packlets/retrieve/structuredFilterRetriever.js +2 -2
- package/lib/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
- package/lib/packlets/retrieve/tagRetriever.js +2 -2
- package/lib/packlets/retrieve/tagRetriever.js.map +1 -1
- package/lib/packlets/retrieve/temporalRetrievers.js +3 -3
- package/lib/packlets/retrieve/temporalRetrievers.js.map +1 -1
- package/lib/packlets/store/fileTreeMemoryStore.d.ts +57 -3
- package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
- package/lib/packlets/store/fileTreeMemoryStore.js +72 -16
- package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
- package/lib/packlets/tools/memoryTools.d.ts +24 -0
- package/lib/packlets/tools/memoryTools.d.ts.map +1 -1
- package/lib/packlets/tools/memoryTools.js +104 -21
- package/lib/packlets/tools/memoryTools.js.map +1 -1
- package/lib/packlets/types/envelope.d.ts +61 -8
- package/lib/packlets/types/envelope.d.ts.map +1 -1
- package/lib/packlets/types/envelope.js +14 -0
- package/lib/packlets/types/envelope.js.map +1 -1
- package/lib/packlets/vector/inMemoryCosineIndex.d.ts +9 -5
- package/lib/packlets/vector/inMemoryCosineIndex.d.ts.map +1 -1
- package/lib/packlets/vector/inMemoryCosineIndex.js +22 -17
- package/lib/packlets/vector/inMemoryCosineIndex.js.map +1 -1
- package/lib/packlets/vector/vectorIndex.d.ts +46 -20
- package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
- package/lib/packlets/vector/vectorIndex.js.map +1 -1
- package/lib/test/unit/converters/antagonistRoundTrip.test.js +3 -3
- package/lib/test/unit/converters/antagonistRoundTrip.test.js.map +1 -1
- package/lib/test/unit/converters/envelopeConverter.test.js +124 -7
- package/lib/test/unit/converters/envelopeConverter.test.js.map +1 -1
- package/lib/test/unit/index/memoryIndex.test.js +86 -24
- package/lib/test/unit/index/memoryIndex.test.js.map +1 -1
- package/lib/test/unit/ingest/antagonistCycleAndParity.test.js +20 -16
- package/lib/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -1
- package/lib/test/unit/ingest/cycleGuard.test.js +28 -1
- package/lib/test/unit/ingest/cycleGuard.test.js.map +1 -1
- package/lib/test/unit/ingest/orchestrator.test.js +186 -44
- package/lib/test/unit/ingest/orchestrator.test.js.map +1 -1
- package/lib/test/unit/retrieve/linkTraversalRetriever.test.js +106 -31
- package/lib/test/unit/retrieve/linkTraversalRetriever.test.js.map +1 -1
- package/lib/test/unit/retrieve/retrievers.test.js +298 -36
- package/lib/test/unit/retrieve/retrievers.test.js.map +1 -1
- package/lib/test/unit/store/embedOnWrite.test.js +68 -11
- package/lib/test/unit/store/embedOnWrite.test.js.map +1 -1
- package/lib/test/unit/store/listScoped.test.d.ts +2 -0
- package/lib/test/unit/store/listScoped.test.d.ts.map +1 -0
- package/lib/test/unit/store/listScoped.test.js +111 -0
- package/lib/test/unit/store/listScoped.test.js.map +1 -0
- package/lib/test/unit/store/rankAxis.test.d.ts +2 -0
- package/lib/test/unit/store/rankAxis.test.d.ts.map +1 -0
- package/lib/test/unit/store/rankAxis.test.js +256 -0
- package/lib/test/unit/store/rankAxis.test.js.map +1 -0
- package/lib/test/unit/tools/memoryTools.test.js +280 -11
- package/lib/test/unit/tools/memoryTools.test.js.map +1 -1
- package/lib/test/unit/types/writePolicy.test.js +9 -2
- package/lib/test/unit/types/writePolicy.test.js.map +1 -1
- package/lib/test/unit/vector/inMemoryCosineIndex.test.js +95 -35
- package/lib/test/unit/vector/inMemoryCosineIndex.test.js.map +1 -1
- package/lib/test/unit/vector/vectorIndex.test.js +24 -15
- package/lib/test/unit/vector/vectorIndex.test.js.map +1 -1
- package/package.json +7 -7
- package/rush-logs/ts-agent-memory.build.cache.log +1 -1
- package/rush-logs/ts-agent-memory.build.log +2 -2
- package/src/packlets/converters/envelopeConverter.ts +27 -4
- package/src/packlets/index/memoryIndex.ts +86 -22
- package/src/packlets/ingest/cycleGuard.ts +22 -11
- package/src/packlets/ingest/hostStages.ts +3 -3
- package/src/packlets/ingest/model.ts +25 -14
- package/src/packlets/ingest/orchestrator.ts +143 -67
- package/src/packlets/retrieve/hybridRetriever.ts +14 -1
- package/src/packlets/retrieve/linkTraversalRetriever.ts +51 -62
- package/src/packlets/retrieve/recencyRetriever.ts +3 -3
- package/src/packlets/retrieve/retriever.ts +97 -13
- package/src/packlets/retrieve/semanticRetriever.ts +10 -5
- package/src/packlets/retrieve/structuredFilterRetriever.ts +3 -3
- package/src/packlets/retrieve/tagRetriever.ts +3 -3
- package/src/packlets/retrieve/temporalRetrievers.ts +3 -3
- package/src/packlets/store/fileTreeMemoryStore.ts +117 -12
- package/src/packlets/tools/memoryTools.ts +152 -25
- package/src/packlets/types/envelope.ts +66 -8
- package/src/packlets/vector/inMemoryCosineIndex.ts +45 -22
- package/src/packlets/vector/vectorIndex.ts +47 -20
- package/src/test/unit/converters/antagonistRoundTrip.test.ts +3 -3
- package/src/test/unit/converters/envelopeConverter.test.ts +168 -11
- package/src/test/unit/index/memoryIndex.test.ts +99 -14
- package/src/test/unit/ingest/antagonistCycleAndParity.test.ts +23 -18
- package/src/test/unit/ingest/cycleGuard.test.ts +44 -2
- package/src/test/unit/ingest/orchestrator.test.ts +234 -41
- package/src/test/unit/retrieve/linkTraversalRetriever.test.ts +134 -35
- package/src/test/unit/retrieve/retrievers.test.ts +381 -25
- package/src/test/unit/store/embedOnWrite.test.ts +83 -11
- package/src/test/unit/store/listScoped.test.ts +138 -0
- package/src/test/unit/store/rankAxis.test.ts +349 -0
- package/src/test/unit/tools/memoryTools.test.ts +362 -13
- package/src/test/unit/types/writePolicy.test.ts +11 -2
- package/src/test/unit/vector/inMemoryCosineIndex.test.ts +115 -39
- package/src/test/unit/vector/vectorIndex.test.ts +33 -17
- package/temp/build/lint/_eslint-5eVG3S6w.json +41 -33
- package/temp/build/typescript/ts_8nwakTlr.json +1 -1
- package/temp/ts-agent-memory.api.json +1080 -136
- 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,
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
*
|
|
191
|
-
* last, after ordering, so it always takes
|
|
192
|
-
*
|
|
193
|
-
*
|
|
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
|
|
285
|
+
return windowed;
|
|
202
286
|
}
|
|
203
287
|
if (limit <= 0) {
|
|
204
288
|
return [];
|
|
205
289
|
}
|
|
206
|
-
return
|
|
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,
|
|
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
|
-
|
|
122
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
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(
|
|
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(
|
|
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
|
-
/**
|
|
748
|
-
|
|
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(
|
|
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(
|
|
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
|
-
.
|
|
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) {
|