@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
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
Convert,
|
|
9
9
|
EntityId,
|
|
10
10
|
IEdge,
|
|
11
|
+
IEdgeTarget,
|
|
11
12
|
IIdentityCodec,
|
|
12
13
|
IIdentityCodecResult,
|
|
13
14
|
IMemoryEnvelope,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
MemoryId,
|
|
19
20
|
MemoryScopeKey,
|
|
20
21
|
Tag,
|
|
22
|
+
edgeTargetKey,
|
|
21
23
|
isTemporalRecord,
|
|
22
24
|
isVersionCurrent
|
|
23
25
|
} from '../types';
|
|
@@ -156,6 +158,12 @@ interface ISimilarityWiring {
|
|
|
156
158
|
readonly embed: MemoryEmbedder;
|
|
157
159
|
}
|
|
158
160
|
|
|
161
|
+
/** A snapshot record paired with its resolved scope-qualified `(scope, id)` address. */
|
|
162
|
+
interface IScopedRecord {
|
|
163
|
+
readonly address: IEdgeTarget;
|
|
164
|
+
readonly record: IMemoryRecord<unknown>;
|
|
165
|
+
}
|
|
166
|
+
|
|
159
167
|
/** Internal per-candidate plan threaded through the pipeline. */
|
|
160
168
|
interface ICandidatePlan {
|
|
161
169
|
readonly candidate: ICandidateRecord;
|
|
@@ -165,6 +173,12 @@ interface ICandidatePlan {
|
|
|
165
173
|
readonly writeEntityId: EntityId;
|
|
166
174
|
/** The reference id used in stage-5 edges (the write target's `idStem`). */
|
|
167
175
|
readonly refId: MemoryId;
|
|
176
|
+
/**
|
|
177
|
+
* The scope-qualified reference used in stage-5 edges: the write target's
|
|
178
|
+
* `(scope, idStem)`. Stage-5 sources / dedup all key on this scoped address so
|
|
179
|
+
* two candidates that share a stem across scopes never collide.
|
|
180
|
+
*/
|
|
181
|
+
readonly refTarget: IEdgeTarget;
|
|
168
182
|
/** The resolution verdict; carries the target id on its target-bearing arms. */
|
|
169
183
|
readonly verdict: ResolutionVerdict;
|
|
170
184
|
/**
|
|
@@ -268,12 +282,23 @@ export class MemoryIngestOrchestrator implements IMemoryIngestOrchestrator {
|
|
|
268
282
|
return fail(`ingest '${item.id}': failed to snapshot store: ${snapshotResult.message}`);
|
|
269
283
|
}
|
|
270
284
|
const snapshot: ReadonlyArray<IMemoryRecord<unknown>> = snapshotResult.value;
|
|
271
|
-
|
|
285
|
+
// Resolve every snapshot record's scope-qualified address ONCE, then index it
|
|
286
|
+
// by the canonical scoped key. The single `byKey` view drives BOTH the edge
|
|
287
|
+
// path (validation + cycle guard) AND the verdict/similarity target lookups —
|
|
288
|
+
// so a filename stem reused across scopes never aliases on any path.
|
|
289
|
+
const scopedResult: Result<ReadonlyArray<IScopedRecord>> = this._scopeRecords(snapshot);
|
|
290
|
+
if (scopedResult.isFailure()) {
|
|
291
|
+
return fail(`ingest '${item.id}': ${scopedResult.message}`);
|
|
292
|
+
}
|
|
293
|
+
const scoped: ReadonlyArray<IScopedRecord> = scopedResult.value;
|
|
294
|
+
const byKey: ReadonlyMap<string, IMemoryRecord<unknown>> = new Map<string, IMemoryRecord<unknown>>(
|
|
295
|
+
scoped.map((s) => [edgeTargetKey(s.address), s.record])
|
|
296
|
+
);
|
|
272
297
|
|
|
273
298
|
// Stage 3b + 4: validate each body and resolve a verdict/plan.
|
|
274
299
|
const plans: ICandidatePlan[] = [];
|
|
275
300
|
for (const candidate of candidates) {
|
|
276
|
-
const planResult: Result<ICandidatePlan> = await this._planCandidate(item, candidate, snapshot,
|
|
301
|
+
const planResult: Result<ICandidatePlan> = await this._planCandidate(item, candidate, snapshot, byKey);
|
|
277
302
|
if (planResult.isFailure()) {
|
|
278
303
|
return fail(planResult.message);
|
|
279
304
|
}
|
|
@@ -285,8 +310,8 @@ export class MemoryIngestOrchestrator implements IMemoryIngestOrchestrator {
|
|
|
285
310
|
const edgesResult: Result<ReadonlyArray<ICandidateEdge>> = await this._relate(
|
|
286
311
|
item,
|
|
287
312
|
writablePlans,
|
|
288
|
-
|
|
289
|
-
|
|
313
|
+
scoped,
|
|
314
|
+
byKey
|
|
290
315
|
);
|
|
291
316
|
if (edgesResult.isFailure()) {
|
|
292
317
|
return fail(edgesResult.message);
|
|
@@ -310,7 +335,7 @@ export class MemoryIngestOrchestrator implements IMemoryIngestOrchestrator {
|
|
|
310
335
|
item: IIngestItem,
|
|
311
336
|
candidate: ICandidateRecord,
|
|
312
337
|
snapshot: ReadonlyArray<IMemoryRecord<unknown>>,
|
|
313
|
-
|
|
338
|
+
byKey: ReadonlyMap<string, IMemoryRecord<unknown>>
|
|
314
339
|
): Promise<Result<ICandidatePlan>> {
|
|
315
340
|
const kind: Kind = candidate.envelope.kind;
|
|
316
341
|
// Stage 3b: the typed validation boundary — no unchecked host body reaches the store.
|
|
@@ -329,8 +354,8 @@ export class MemoryIngestOrchestrator implements IMemoryIngestOrchestrator {
|
|
|
329
354
|
}
|
|
330
355
|
const addr: IIdentityCodecResult = addrResult.value;
|
|
331
356
|
|
|
332
|
-
return (await this._resolveVerdict(candidate, kind, body, addr, snapshot,
|
|
333
|
-
this._planFromVerdict(item, candidate, addr, verdict,
|
|
357
|
+
return (await this._resolveVerdict(candidate, kind, body, addr, snapshot, byKey)).onSuccess((verdict) =>
|
|
358
|
+
this._planFromVerdict(item, candidate, addr, verdict, byKey)
|
|
334
359
|
);
|
|
335
360
|
}
|
|
336
361
|
|
|
@@ -348,7 +373,7 @@ export class MemoryIngestOrchestrator implements IMemoryIngestOrchestrator {
|
|
|
348
373
|
candidate: ICandidateRecord,
|
|
349
374
|
addr: IIdentityCodecResult,
|
|
350
375
|
verdict: ResolutionVerdict,
|
|
351
|
-
|
|
376
|
+
byKey: ReadonlyMap<string, IMemoryRecord<unknown>>
|
|
352
377
|
): Result<ICandidatePlan> {
|
|
353
378
|
if (verdict.verdict === 'new') {
|
|
354
379
|
return Convert.memoryId.convert(addr.idStem).onSuccess((refId) =>
|
|
@@ -357,41 +382,51 @@ export class MemoryIngestOrchestrator implements IMemoryIngestOrchestrator {
|
|
|
357
382
|
writeAddress: addr,
|
|
358
383
|
writeEntityId: candidate.envelope.entityId,
|
|
359
384
|
refId,
|
|
385
|
+
refTarget: { scope: addr.scope, id: refId },
|
|
360
386
|
verdict
|
|
361
387
|
})
|
|
362
388
|
);
|
|
363
389
|
}
|
|
364
390
|
// duplicate-of | supersede | merge-into: the target must exist and share the
|
|
365
|
-
// candidate's kind.
|
|
366
|
-
|
|
391
|
+
// candidate's kind. Lookup is on the canonical scoped key, so a stem reused
|
|
392
|
+
// across scopes binds only the record the verdict actually named.
|
|
393
|
+
const target: IMemoryRecord<unknown> | undefined = byKey.get(edgeTargetKey(verdict.target));
|
|
367
394
|
if (target === undefined) {
|
|
368
395
|
return fail(
|
|
369
|
-
`ingest '${item.id}': ${verdict.verdict} target '${
|
|
396
|
+
`ingest '${item.id}': ${verdict.verdict} target '${MemoryIngestOrchestrator._formatTarget(
|
|
397
|
+
verdict.target
|
|
398
|
+
)}' does not exist in the store`
|
|
370
399
|
);
|
|
371
400
|
}
|
|
372
401
|
if (target.envelope.kind !== candidate.envelope.kind) {
|
|
373
402
|
return fail(
|
|
374
|
-
`ingest '${item.id}': ${verdict.verdict} target '${
|
|
403
|
+
`ingest '${item.id}': ${verdict.verdict} target '${MemoryIngestOrchestrator._formatTarget(
|
|
404
|
+
verdict.target
|
|
405
|
+
)}' is kind '${target.envelope.kind}' but the candidate is kind '${candidate.envelope.kind}'`
|
|
375
406
|
);
|
|
376
407
|
}
|
|
377
408
|
if (verdict.verdict === 'merge-into') {
|
|
378
409
|
// Re-address the write to the target's entity, carrying the target record so
|
|
379
410
|
// stage 6 can UNION its existing tags/links (never overwrite them).
|
|
380
411
|
const targetEntityId: EntityId = target.envelope.entityId;
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
412
|
+
// The merge target is a snapshot record, and `_scopeRecords` already proved
|
|
413
|
+
// every snapshot record's address resolves before planning runs — so this
|
|
414
|
+
// re-resolve (needed for the full codec result: scope + idStem + isVersioned)
|
|
415
|
+
// cannot fail. No error-context wrap is warranted; a failure here would be a
|
|
416
|
+
// logic contradiction, not a user-facing condition.
|
|
417
|
+
return this._resolveAddress(targetEntityId, target.envelope.kind).onSuccess((targetAddr) =>
|
|
418
|
+
Convert.memoryId.convert(targetAddr.idStem).onSuccess((refId) =>
|
|
419
|
+
succeed({
|
|
420
|
+
candidate,
|
|
421
|
+
writeAddress: targetAddr,
|
|
422
|
+
writeEntityId: targetEntityId,
|
|
423
|
+
refId,
|
|
424
|
+
refTarget: { scope: targetAddr.scope, id: refId },
|
|
425
|
+
verdict,
|
|
426
|
+
mergeTarget: target
|
|
427
|
+
})
|
|
428
|
+
)
|
|
429
|
+
);
|
|
395
430
|
}
|
|
396
431
|
// duplicate-of | supersede: write under the candidate's own address.
|
|
397
432
|
return Convert.memoryId.convert(addr.idStem).onSuccess((refId) =>
|
|
@@ -400,6 +435,7 @@ export class MemoryIngestOrchestrator implements IMemoryIngestOrchestrator {
|
|
|
400
435
|
writeAddress: addr,
|
|
401
436
|
writeEntityId: candidate.envelope.entityId,
|
|
402
437
|
refId,
|
|
438
|
+
refTarget: { scope: addr.scope, id: refId },
|
|
403
439
|
verdict
|
|
404
440
|
})
|
|
405
441
|
);
|
|
@@ -418,18 +454,20 @@ export class MemoryIngestOrchestrator implements IMemoryIngestOrchestrator {
|
|
|
418
454
|
body: string,
|
|
419
455
|
addr: IIdentityCodecResult,
|
|
420
456
|
snapshot: ReadonlyArray<IMemoryRecord<unknown>>,
|
|
421
|
-
|
|
457
|
+
byKey: ReadonlyMap<string, IMemoryRecord<unknown>>
|
|
422
458
|
): Promise<Result<ResolutionVerdict>> {
|
|
423
459
|
return this._findExactMatch(kind, body, addr.scope, snapshot).thenOnSuccess(async (matchId) => {
|
|
424
460
|
if (matchId !== undefined) {
|
|
425
|
-
|
|
461
|
+
// The exact-match cohort is filtered to `addr.scope`, so the match lives
|
|
462
|
+
// under that scope — its scope-qualified target is `(addr.scope, matchId)`.
|
|
463
|
+
return succeed({ verdict: 'duplicate-of', target: { scope: addr.scope, id: matchId } });
|
|
426
464
|
}
|
|
427
465
|
const layer2: ISimilarityWiring | undefined = this._similarity;
|
|
428
466
|
if (layer2 === undefined) {
|
|
429
467
|
// No layer-2: exact-only fall-back (the deterministic-identity host path).
|
|
430
468
|
return succeed({ verdict: 'new' });
|
|
431
469
|
}
|
|
432
|
-
return this._resolveViaSimilarity(candidate, addr, body, layer2,
|
|
470
|
+
return this._resolveViaSimilarity(candidate, addr, body, layer2, byKey);
|
|
433
471
|
});
|
|
434
472
|
}
|
|
435
473
|
|
|
@@ -439,10 +477,10 @@ export class MemoryIngestOrchestrator implements IMemoryIngestOrchestrator {
|
|
|
439
477
|
addr: IIdentityCodecResult,
|
|
440
478
|
body: string,
|
|
441
479
|
wiring: ISimilarityWiring,
|
|
442
|
-
|
|
480
|
+
byKey: ReadonlyMap<string, IMemoryRecord<unknown>>
|
|
443
481
|
): Promise<Result<ResolutionVerdict>> {
|
|
444
482
|
return MemoryIngestOrchestrator._provisionalRecord(candidate, addr.idStem, body).thenOnSuccess(
|
|
445
|
-
(provisional) => this._resolveViaSimilarityEmbedded(candidate, addr, wiring,
|
|
483
|
+
(provisional) => this._resolveViaSimilarityEmbedded(candidate, addr, wiring, byKey, provisional)
|
|
446
484
|
);
|
|
447
485
|
}
|
|
448
486
|
|
|
@@ -451,7 +489,7 @@ export class MemoryIngestOrchestrator implements IMemoryIngestOrchestrator {
|
|
|
451
489
|
candidate: ICandidateRecord,
|
|
452
490
|
addr: IIdentityCodecResult,
|
|
453
491
|
wiring: ISimilarityWiring,
|
|
454
|
-
|
|
492
|
+
byKey: ReadonlyMap<string, IMemoryRecord<unknown>>,
|
|
455
493
|
provisional: IMemoryRecord<unknown>
|
|
456
494
|
): Promise<Result<ResolutionVerdict>> {
|
|
457
495
|
const embedded: Result<Float32Array> = await this._capture(
|
|
@@ -470,12 +508,20 @@ export class MemoryIngestOrchestrator implements IMemoryIngestOrchestrator {
|
|
|
470
508
|
}
|
|
471
509
|
const similar: IEntityResolutionCandidate[] = [];
|
|
472
510
|
for (const hit of queried.value) {
|
|
473
|
-
|
|
511
|
+
// Skip below-threshold hits and the candidate's OWN scope-qualified address
|
|
512
|
+
// (a re-ingest of the same entity must not dedup against its prior version).
|
|
513
|
+
// Both the scope AND the id must match to be "self" — a same-stem record in
|
|
514
|
+
// another scope is a legitimate distinct neighbor. This field-equality is
|
|
515
|
+
// exactly equivalent to comparing `edgeTargetKey(hit.target)` against the
|
|
516
|
+
// candidate's own key (edgeTargetKey is pure and injective on `(scope, id)`),
|
|
517
|
+
// so this one site reads the components directly rather than re-keying.
|
|
518
|
+
const isSelf: boolean = hit.target.scope === addr.scope && hit.target.id === addr.idStem;
|
|
519
|
+
if (hit.score < this._similarityThreshold || isSelf) {
|
|
474
520
|
continue;
|
|
475
521
|
}
|
|
476
|
-
const record: IMemoryRecord<unknown> | undefined =
|
|
522
|
+
const record: IMemoryRecord<unknown> | undefined = byKey.get(edgeTargetKey(hit.target));
|
|
477
523
|
if (record !== undefined) {
|
|
478
|
-
similar.push({
|
|
524
|
+
similar.push({ target: hit.target, record, score: hit.score });
|
|
479
525
|
}
|
|
480
526
|
}
|
|
481
527
|
if (similar.length === 0) {
|
|
@@ -546,12 +592,12 @@ export class MemoryIngestOrchestrator implements IMemoryIngestOrchestrator {
|
|
|
546
592
|
private async _relate(
|
|
547
593
|
item: IIngestItem,
|
|
548
594
|
writablePlans: ReadonlyArray<ICandidatePlan>,
|
|
549
|
-
|
|
550
|
-
|
|
595
|
+
scoped: ReadonlyArray<IScopedRecord>,
|
|
596
|
+
byKey: ReadonlyMap<string, IMemoryRecord<unknown>>
|
|
551
597
|
): Promise<Result<ReadonlyArray<ICandidateEdge>>> {
|
|
552
598
|
const relationCandidates: IRelationCandidate[] = writablePlans.map((plan) => ({
|
|
553
599
|
candidate: plan.candidate,
|
|
554
|
-
id: plan.
|
|
600
|
+
id: plan.refTarget
|
|
555
601
|
}));
|
|
556
602
|
const proposed: Result<ReadonlyArray<ICandidateEdge>> = await this._capture(
|
|
557
603
|
() => this._relationExtractor.relate({ item, candidates: relationCandidates }),
|
|
@@ -560,14 +606,19 @@ export class MemoryIngestOrchestrator implements IMemoryIngestOrchestrator {
|
|
|
560
606
|
if (proposed.isFailure()) {
|
|
561
607
|
return proposed;
|
|
562
608
|
}
|
|
563
|
-
|
|
564
|
-
|
|
609
|
+
// refIds and the existing-record view (`byKey`, shared with the verdict path)
|
|
610
|
+
// both key on the canonical scoped address, so a stem reused across scopes
|
|
611
|
+
// never aliases.
|
|
612
|
+
const refIds: ReadonlySet<string> = new Set<string>(
|
|
613
|
+
writablePlans.map((plan) => edgeTargetKey(plan.refTarget))
|
|
614
|
+
);
|
|
615
|
+
const validation: Result<true> = this._validateEdges(item, proposed.value, refIds, byKey);
|
|
565
616
|
if (validation.isFailure()) {
|
|
566
617
|
return fail(validation.message);
|
|
567
618
|
}
|
|
568
619
|
if (this._cycleGuard === 'reject') {
|
|
569
620
|
const guard: Result<true> = assertNoCycles(
|
|
570
|
-
MemoryIngestOrchestrator._existingEdges(
|
|
621
|
+
MemoryIngestOrchestrator._existingEdges(scoped),
|
|
571
622
|
proposed.value.map((e) => ({ source: e.source, target: e.edge.target, type: e.edge.type }))
|
|
572
623
|
);
|
|
573
624
|
if (guard.isFailure()) {
|
|
@@ -579,22 +630,30 @@ export class MemoryIngestOrchestrator implements IMemoryIngestOrchestrator {
|
|
|
579
630
|
|
|
580
631
|
/**
|
|
581
632
|
* Validate stage-5 edges: each `source` must be a candidate being written; each
|
|
582
|
-
* `target` must resolve to a sibling candidate or an existing store record.
|
|
633
|
+
* `target` must resolve to a sibling candidate or an existing store record. All
|
|
634
|
+
* matching is on the canonical scope-qualified address.
|
|
583
635
|
*/
|
|
584
636
|
private _validateEdges(
|
|
585
637
|
item: IIngestItem,
|
|
586
638
|
edges: ReadonlyArray<ICandidateEdge>,
|
|
587
639
|
refIds: ReadonlySet<string>,
|
|
588
|
-
|
|
640
|
+
byKey: ReadonlyMap<string, IMemoryRecord<unknown>>
|
|
589
641
|
): Result<true> {
|
|
590
642
|
return mapResults(
|
|
591
643
|
edges.map((edge) => {
|
|
592
|
-
if (!refIds.has(edge.source)) {
|
|
593
|
-
return fail(
|
|
644
|
+
if (!refIds.has(edgeTargetKey(edge.source))) {
|
|
645
|
+
return fail(
|
|
646
|
+
`ingest '${item.id}': edge source '${MemoryIngestOrchestrator._formatTarget(
|
|
647
|
+
edge.source
|
|
648
|
+
)}' is not a candidate being written`
|
|
649
|
+
);
|
|
594
650
|
}
|
|
595
|
-
|
|
651
|
+
const targetKey: string = edgeTargetKey(edge.edge.target);
|
|
652
|
+
if (!refIds.has(targetKey) && byKey.get(targetKey) === undefined) {
|
|
596
653
|
return fail(
|
|
597
|
-
`ingest '${item.id}': edge target '${
|
|
654
|
+
`ingest '${item.id}': edge target '${MemoryIngestOrchestrator._formatTarget(
|
|
655
|
+
edge.edge.target
|
|
656
|
+
)}' resolves to neither a sibling candidate nor an existing record`
|
|
598
657
|
);
|
|
599
658
|
}
|
|
600
659
|
return succeed(true);
|
|
@@ -608,7 +667,8 @@ export class MemoryIngestOrchestrator implements IMemoryIngestOrchestrator {
|
|
|
608
667
|
plan: ICandidatePlan,
|
|
609
668
|
allEdges: ReadonlyArray<ICandidateEdge>
|
|
610
669
|
): Promise<Result<IIngestedRecordResult>> {
|
|
611
|
-
const
|
|
670
|
+
const refKey: string = edgeTargetKey(plan.refTarget);
|
|
671
|
+
const myEdges: ReadonlyArray<ICandidateEdge> = allEdges.filter((e) => edgeTargetKey(e.source) === refKey);
|
|
612
672
|
if (plan.verdict.verdict === 'duplicate-of') {
|
|
613
673
|
// No write: the existing target satisfied the candidate. The target id is a
|
|
614
674
|
// typed field of the narrowed `duplicate-of` verdict — no cast, no guard.
|
|
@@ -616,7 +676,7 @@ export class MemoryIngestOrchestrator implements IMemoryIngestOrchestrator {
|
|
|
616
676
|
candidate: plan.candidate,
|
|
617
677
|
resolution: plan.verdict,
|
|
618
678
|
disposition: 'deduped',
|
|
619
|
-
id: plan.verdict.target,
|
|
679
|
+
id: plan.verdict.target.id,
|
|
620
680
|
edges: myEdges
|
|
621
681
|
});
|
|
622
682
|
}
|
|
@@ -740,30 +800,46 @@ export class MemoryIngestOrchestrator implements IMemoryIngestOrchestrator {
|
|
|
740
800
|
}
|
|
741
801
|
}
|
|
742
802
|
|
|
743
|
-
/**
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
return byId;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
/** Every existing outbound edge in the snapshot, as cycle-guard edges. */
|
|
755
|
-
private static _existingEdges(
|
|
756
|
-
records: ReadonlyArray<IMemoryRecord<unknown>>
|
|
757
|
-
): ReadonlyArray<ICycleGuardEdge> {
|
|
803
|
+
/**
|
|
804
|
+
* Every existing outbound edge in the snapshot, as cycle-guard edges. The
|
|
805
|
+
* source is the record's own scope-qualified address (already resolved by
|
|
806
|
+
* {@link MemoryIngestOrchestrator._scopeRecords}); the target is the edge's
|
|
807
|
+
* own scoped target. Both ends are scoped so the guard never conflates a stem
|
|
808
|
+
* shared across scopes into one graph node.
|
|
809
|
+
*/
|
|
810
|
+
private static _existingEdges(scoped: ReadonlyArray<IScopedRecord>): ReadonlyArray<ICycleGuardEdge> {
|
|
758
811
|
const edges: ICycleGuardEdge[] = [];
|
|
759
|
-
for (const
|
|
760
|
-
for (const edge of record.envelope.links) {
|
|
761
|
-
edges.push({ source:
|
|
812
|
+
for (const s of scoped) {
|
|
813
|
+
for (const edge of s.record.envelope.links) {
|
|
814
|
+
edges.push({ source: s.address, target: edge.target, type: edge.type });
|
|
762
815
|
}
|
|
763
816
|
}
|
|
764
817
|
return edges;
|
|
765
818
|
}
|
|
766
819
|
|
|
820
|
+
/**
|
|
821
|
+
* Resolve every snapshot record's scope-qualified `(scope, id)` address via its
|
|
822
|
+
* registered codec. Fails loudly if a record's kind has no resolvable codec —
|
|
823
|
+
* the edge path cannot place an un-scopeable record in the graph, and a missing
|
|
824
|
+
* codec for a stored kind is a real misconfiguration, not something to paper over.
|
|
825
|
+
*/
|
|
826
|
+
private _scopeRecords(
|
|
827
|
+
records: ReadonlyArray<IMemoryRecord<unknown>>
|
|
828
|
+
): Result<ReadonlyArray<IScopedRecord>> {
|
|
829
|
+
return mapResults(
|
|
830
|
+
records.map((record) =>
|
|
831
|
+
this._resolveAddress(record.envelope.entityId, record.envelope.kind)
|
|
832
|
+
.withErrorFormat((msg) => `cannot resolve scope for stored record '${record.envelope.id}': ${msg}`)
|
|
833
|
+
.onSuccess((addr) => succeed({ address: { scope: addr.scope, id: record.envelope.id }, record }))
|
|
834
|
+
)
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/** Human-readable `scope/id` rendering of a scoped target for edge-validation diagnostics. */
|
|
839
|
+
private static _formatTarget(target: IEdgeTarget): string {
|
|
840
|
+
return `${target.scope}/${target.id}`;
|
|
841
|
+
}
|
|
842
|
+
|
|
767
843
|
/** A provisional record for embedding a candidate (placeholder txn-time fields). */
|
|
768
844
|
private static _provisionalRecord(
|
|
769
845
|
candidate: ICandidateRecord,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
IMemoryRetrieverCapabilities,
|
|
12
12
|
guardRetrieverCapabilities,
|
|
13
13
|
limitRecords,
|
|
14
|
+
rankCompare,
|
|
14
15
|
recencyCompare
|
|
15
16
|
} from './retriever';
|
|
16
17
|
|
|
@@ -145,7 +146,15 @@ export class HybridRetriever implements IMemoryRetriever {
|
|
|
145
146
|
);
|
|
146
147
|
return mapResults(perRetriever)
|
|
147
148
|
.onSuccess((resultSets) => this._mergeStrategy.merge(resultSets))
|
|
148
|
-
.onSuccess((merged) =>
|
|
149
|
+
.onSuccess((merged) => {
|
|
150
|
+
// `orderBy: 'rank'` re-orders the merged set by rank (descending, absent
|
|
151
|
+
// last) before the page window, so a rank-ordered hybrid query yields a
|
|
152
|
+
// rank-ordered page. Absent / `'recency'` preserves the merge strategy's
|
|
153
|
+
// own ordering (byte-identical to the pre-`orderBy` behavior).
|
|
154
|
+
const ordered: ReadonlyArray<IMemoryRecord<unknown>> =
|
|
155
|
+
query.orderBy === 'rank' ? [...merged].sort(rankCompare) : merged;
|
|
156
|
+
return succeed(limitRecords(ordered, query.limit, query.offset));
|
|
157
|
+
});
|
|
149
158
|
});
|
|
150
159
|
}
|
|
151
160
|
|
|
@@ -164,6 +173,10 @@ export class HybridRetriever implements IMemoryRetriever {
|
|
|
164
173
|
private _projectQuery(query: IMemoryQuery, retriever: IMemoryRetriever): IMemoryQuery {
|
|
165
174
|
const projected: { -readonly [K in keyof IMemoryQuery]: IMemoryQuery[K] } = { ...query };
|
|
166
175
|
delete projected.limit;
|
|
176
|
+
// Offset, like limit, is a post-merge concern: a child that pre-skipped its
|
|
177
|
+
// own ordered set would drop candidates the merge needs to score correctly.
|
|
178
|
+
// The hybrid applies the `{ offset, limit }` window once, after merge.
|
|
179
|
+
delete projected.offset;
|
|
167
180
|
if (!retriever.capabilities.supportsSemanticRecall) {
|
|
168
181
|
delete projected.semantic;
|
|
169
182
|
delete projected.topK;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { Result, fail, succeed } from '@fgv/ts-utils';
|
|
7
|
-
import { IMemoryRecord,
|
|
7
|
+
import { IEdgeTarget, IMemoryRecord, edgeTargetKey } from '../types';
|
|
8
8
|
import { IIndexedMemoryRecord, IMemoryIndex } from '../index';
|
|
9
9
|
import {
|
|
10
10
|
IMemoryQuery,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
guardRetrieverCapabilities,
|
|
14
14
|
indexedRecordMatchesQuery,
|
|
15
15
|
limitRecords,
|
|
16
|
-
|
|
16
|
+
orderingCompare
|
|
17
17
|
} from './retriever';
|
|
18
18
|
|
|
19
19
|
/** The capabilities a link-traversal retriever exposes (link traversal only). */
|
|
@@ -35,20 +35,23 @@ export const LINK_TRAVERSAL_NO_SEED_MESSAGE: string =
|
|
|
35
35
|
'link traversal requires a seed id (linkedFrom or linkedTo)';
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* Breadth-first link-traversal retriever. From a
|
|
39
|
-
* the link graph up to `query.hops` levels and
|
|
40
|
-
* (excluding the seed), recency-ordered and limited.
|
|
38
|
+
* Breadth-first link-traversal retriever. From a scope-qualified
|
|
39
|
+
* {@link IEdgeTarget} seed it walks the link graph up to `query.hops` levels and
|
|
40
|
+
* returns the records reached (excluding the seed), recency-ordered and limited.
|
|
41
41
|
*
|
|
42
42
|
* @remarks
|
|
43
43
|
* - **Direction.** `linkedFrom` walks OUTBOUND edges (each record's
|
|
44
44
|
* `envelope.links[].target`); `linkedTo` walks INBOUND edges (the index's
|
|
45
45
|
* `backlinks`). Exactly one is the seed; `linkedFrom` wins if both are set.
|
|
46
|
+
* - **Scope-qualified nodes.** Every graph node is an {@link IEdgeTarget}
|
|
47
|
+
* `(scope, id)` pair, so following an edge to `turn-3` reaches ONLY the record
|
|
48
|
+
* in the edge's own scope — never a same-stem record in another scope.
|
|
46
49
|
* - **Bound + cycle safety.** Traversal is bounded by `hops` (default `1` — a
|
|
47
|
-
* single hop) and a visited-set guard.
|
|
48
|
-
* string {@link
|
|
49
|
-
* collision-free cycle key — no structural hashing (e.g.
|
|
50
|
-
* needed. A self-loop or any multi-hop cycle terminates
|
|
51
|
-
* is never re-expanded.
|
|
50
|
+
* single hop) and a visited-set guard. Nodes are canonicalized to their
|
|
51
|
+
* `(scope, id)` string via {@link edgeTargetKey}, so a `Set<string>` visited-set
|
|
52
|
+
* is the exact, collision-free cycle key — no structural hashing (e.g.
|
|
53
|
+
* `Crc32Normalizer`) is needed. A self-loop or any multi-hop cycle terminates
|
|
54
|
+
* because a revisited node is never re-expanded.
|
|
52
55
|
* - **Post-filter.** The scope / kind / tag / predicate axes of the query are
|
|
53
56
|
* applied to the reached records (the link axes are the traversal itself).
|
|
54
57
|
* @public
|
|
@@ -80,24 +83,26 @@ export class LinkTraversalRetriever implements IMemoryRetriever {
|
|
|
80
83
|
/** Run the bounded, cycle-safe BFS and post-filter the reached records. */
|
|
81
84
|
private _traverse(query: IMemoryQuery): Result<ReadonlyArray<IMemoryRecord<unknown>>> {
|
|
82
85
|
const outbound: boolean = query.linkedFrom !== undefined;
|
|
83
|
-
const seed:
|
|
86
|
+
const seed: IEdgeTarget | undefined = query.linkedFrom ?? query.linkedTo;
|
|
84
87
|
if (seed === undefined) {
|
|
85
88
|
return fail(LINK_TRAVERSAL_NO_SEED_MESSAGE);
|
|
86
89
|
}
|
|
87
90
|
const hops: number = query.hops ?? DEFAULT_HOPS;
|
|
88
|
-
const
|
|
91
|
+
const byKey: ReadonlyMap<string, IIndexedMemoryRecord> = this._indexByKey();
|
|
89
92
|
|
|
90
|
-
// The visited-set IS the cycle guard:
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
93
|
+
// The visited-set IS the cycle guard: nodes are canonicalized to their
|
|
94
|
+
// `(scope, id)` string, so set membership is an exact identity check. The
|
|
95
|
+
// seed is pre-marked so it is never re-added.
|
|
96
|
+
const visited: Set<string> = new Set<string>([edgeTargetKey(seed)]);
|
|
97
|
+
const reached: IEdgeTarget[] = [];
|
|
98
|
+
let frontier: IEdgeTarget[] = [seed];
|
|
95
99
|
for (let hop = 0; hop < hops && frontier.length > 0; hop++) {
|
|
96
|
-
const next:
|
|
97
|
-
for (const
|
|
98
|
-
for (const neighbor of outbound ? this._outbound(
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
const next: IEdgeTarget[] = [];
|
|
101
|
+
for (const node of frontier) {
|
|
102
|
+
for (const neighbor of outbound ? this._outbound(node, byKey) : this._inbound(node)) {
|
|
103
|
+
const neighborKey: string = edgeTargetKey(neighbor);
|
|
104
|
+
if (!visited.has(neighborKey)) {
|
|
105
|
+
visited.add(neighborKey);
|
|
101
106
|
reached.push(neighbor);
|
|
102
107
|
next.push(neighbor);
|
|
103
108
|
}
|
|
@@ -107,63 +112,47 @@ export class LinkTraversalRetriever implements IMemoryRetriever {
|
|
|
107
112
|
}
|
|
108
113
|
|
|
109
114
|
const entries: IIndexedMemoryRecord[] = [];
|
|
110
|
-
for (const
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
113
|
-
entries.push(
|
|
115
|
+
for (const node of reached) {
|
|
116
|
+
const match: IIndexedMemoryRecord | undefined = byKey.get(edgeTargetKey(node));
|
|
117
|
+
if (match !== undefined) {
|
|
118
|
+
entries.push(match);
|
|
114
119
|
}
|
|
115
120
|
}
|
|
116
121
|
const ordered: IMemoryRecord<unknown>[] = entries
|
|
117
122
|
.filter((entry) => indexedRecordMatchesQuery(entry, query))
|
|
118
123
|
.map((entry) => entry.record)
|
|
119
|
-
.sort(
|
|
120
|
-
return succeed(limitRecords(ordered, query.limit));
|
|
124
|
+
.sort(orderingCompare(query.orderBy));
|
|
125
|
+
return succeed(limitRecords(ordered, query.limit, query.offset));
|
|
121
126
|
}
|
|
122
127
|
|
|
123
128
|
/**
|
|
124
|
-
* Group the index's entries by
|
|
125
|
-
*
|
|
126
|
-
* two
|
|
127
|
-
*
|
|
128
|
-
* @remarks
|
|
129
|
-
* **Design note (links are globally-scoped identifiers in this phase).** An
|
|
130
|
-
* {@link IEdge.target} is a bare `MemoryId`, not a `(scope, id)` pair, so
|
|
131
|
-
* traversal resolves a target across ALL scopes that hold that id. When two
|
|
132
|
-
* scopes reuse a stem, following an edge to it reaches every match. This
|
|
133
|
-
* mirrors the `backlinks` index, which is also keyed by bare id. Scope-
|
|
134
|
-
* qualified link resolution is intentionally out of scope for Phase C and
|
|
135
|
-
* would be an additive change here (and to {@link IEdge} / the index).
|
|
129
|
+
* Group the index's entries by their scope-qualified {@link edgeTargetKey}
|
|
130
|
+
* `(scope, id)` composite. Each composite is the index's primary key, so it maps
|
|
131
|
+
* to exactly one entry — two records that reuse a filename stem across scopes
|
|
132
|
+
* (e.g. `turn-0` in two conversations) get distinct keys and never collide.
|
|
136
133
|
*/
|
|
137
|
-
private
|
|
138
|
-
const
|
|
134
|
+
private _indexByKey(): ReadonlyMap<string, IIndexedMemoryRecord> {
|
|
135
|
+
const byKey: Map<string, IIndexedMemoryRecord> = new Map<string, IIndexedMemoryRecord>();
|
|
139
136
|
for (const entry of this._index.entries()) {
|
|
140
|
-
|
|
141
|
-
const existing: IIndexedMemoryRecord[] | undefined = byId.get(id);
|
|
142
|
-
if (existing === undefined) {
|
|
143
|
-
byId.set(id, [entry]);
|
|
144
|
-
} else {
|
|
145
|
-
existing.push(entry);
|
|
146
|
-
}
|
|
137
|
+
byKey.set(edgeTargetKey({ scope: entry.scope, id: entry.record.envelope.id }), entry);
|
|
147
138
|
}
|
|
148
|
-
return
|
|
139
|
+
return byKey;
|
|
149
140
|
}
|
|
150
141
|
|
|
151
|
-
/** Outbound neighbors: the targets of every edge on the
|
|
152
|
-
private _outbound(
|
|
153
|
-
const targets:
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
156
|
-
for (const
|
|
157
|
-
|
|
158
|
-
targets.push(edge.target);
|
|
159
|
-
}
|
|
142
|
+
/** Outbound neighbors: the scope-qualified targets of every edge on the record at `node`. */
|
|
143
|
+
private _outbound(node: IEdgeTarget, byKey: ReadonlyMap<string, IIndexedMemoryRecord>): IEdgeTarget[] {
|
|
144
|
+
const targets: IEdgeTarget[] = [];
|
|
145
|
+
const match: IIndexedMemoryRecord | undefined = byKey.get(edgeTargetKey(node));
|
|
146
|
+
if (match !== undefined) {
|
|
147
|
+
for (const edge of match.record.envelope.links) {
|
|
148
|
+
targets.push(edge.target);
|
|
160
149
|
}
|
|
161
150
|
}
|
|
162
151
|
return targets;
|
|
163
152
|
}
|
|
164
153
|
|
|
165
|
-
/** Inbound neighbors: the
|
|
166
|
-
private _inbound(
|
|
167
|
-
return this._index.backlinks(
|
|
154
|
+
/** Inbound neighbors: the scope-qualified sources whose edges point AT `node` (the backlinks). */
|
|
155
|
+
private _inbound(node: IEdgeTarget): ReadonlyArray<IEdgeTarget> {
|
|
156
|
+
return this._index.backlinks(node);
|
|
168
157
|
}
|
|
169
158
|
}
|
|
@@ -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
|
|
|
@@ -45,9 +45,9 @@ export class RecencyRetriever implements IMemoryRetriever {
|
|
|
45
45
|
return Promise.resolve(
|
|
46
46
|
guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => {
|
|
47
47
|
const ordered: IMemoryRecord<unknown>[] = selectByQuery(this._index.entries(), query).sort(
|
|
48
|
-
|
|
48
|
+
orderingCompare(query.orderBy)
|
|
49
49
|
);
|
|
50
|
-
return succeed(limitRecords(ordered, query.limit));
|
|
50
|
+
return succeed(limitRecords(ordered, query.limit, query.offset));
|
|
51
51
|
})
|
|
52
52
|
);
|
|
53
53
|
}
|