@fgv/ts-agent-memory 5.1.0-45 → 5.1.0-47

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 (120) hide show
  1. package/.rush/temp/{bc5b0f2098f7df2a5c498ea77b0de13316349b96.tar.log → 5667e36f1c0cc503a5c20a3ebff76e7b9a702d41.tar.log} +26 -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/README.md +134 -4
  7. package/dist/packlets/ingest/orchestrator.js +104 -19
  8. package/dist/packlets/ingest/orchestrator.js.map +1 -1
  9. package/dist/packlets/retrieve/fragmentSemanticRetriever.js +10 -7
  10. package/dist/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -1
  11. package/dist/packlets/store/fileTreeMemoryStore.js +31 -8
  12. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  13. package/dist/packlets/types/writePolicy.js +10 -2
  14. package/dist/packlets/types/writePolicy.js.map +1 -1
  15. package/dist/packlets/vector/fragmentConverters.js +41 -0
  16. package/dist/packlets/vector/fragmentConverters.js.map +1 -0
  17. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js +17 -9
  18. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  19. package/dist/packlets/vector/index.js +1 -0
  20. package/dist/packlets/vector/index.js.map +1 -1
  21. package/dist/packlets/vector/vectorIndex.js.map +1 -1
  22. package/dist/test/unit/ingest/dedupScope.test.js +434 -0
  23. package/dist/test/unit/ingest/dedupScope.test.js.map +1 -0
  24. package/dist/test/unit/ingest/orchestrator.test.js +7 -4
  25. package/dist/test/unit/ingest/orchestrator.test.js.map +1 -1
  26. package/dist/test/unit/store/fileTreeMemoryStore.test.js +64 -0
  27. package/dist/test/unit/store/fileTreeMemoryStore.test.js.map +1 -1
  28. package/dist/test/unit/store/indexInjection.test.js +432 -0
  29. package/dist/test/unit/store/indexInjection.test.js.map +1 -0
  30. package/dist/test/unit/tools/memoryTools.test.js +1 -0
  31. package/dist/test/unit/tools/memoryTools.test.js.map +1 -1
  32. package/dist/test/unit/types/temporalPolicy.test.js +21 -0
  33. package/dist/test/unit/types/temporalPolicy.test.js.map +1 -1
  34. package/dist/test/unit/types/writePolicy.test.js +83 -0
  35. package/dist/test/unit/types/writePolicy.test.js.map +1 -1
  36. package/dist/test/unit/vector/fragmentConverters.test.js +58 -0
  37. package/dist/test/unit/vector/fragmentConverters.test.js.map +1 -0
  38. package/dist/test/unit/vector/inMemoryFragmentCosineIndex.test.js +65 -0
  39. package/dist/test/unit/vector/inMemoryFragmentCosineIndex.test.js.map +1 -1
  40. package/dist/ts-agent-memory.d.ts +332 -42
  41. package/etc/ts-agent-memory.api.md +12 -1
  42. package/lib/packlets/ingest/orchestrator.d.ts +62 -5
  43. package/lib/packlets/ingest/orchestrator.d.ts.map +1 -1
  44. package/lib/packlets/ingest/orchestrator.js +104 -19
  45. package/lib/packlets/ingest/orchestrator.js.map +1 -1
  46. package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts +10 -7
  47. package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts.map +1 -1
  48. package/lib/packlets/retrieve/fragmentSemanticRetriever.js +10 -7
  49. package/lib/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -1
  50. package/lib/packlets/store/fileTreeMemoryStore.d.ts +96 -4
  51. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  52. package/lib/packlets/store/fileTreeMemoryStore.js +31 -8
  53. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  54. package/lib/packlets/types/writePolicy.d.ts +40 -2
  55. package/lib/packlets/types/writePolicy.d.ts.map +1 -1
  56. package/lib/packlets/types/writePolicy.js +10 -2
  57. package/lib/packlets/types/writePolicy.js.map +1 -1
  58. package/lib/packlets/vector/fragmentConverters.d.ts +25 -0
  59. package/lib/packlets/vector/fragmentConverters.d.ts.map +1 -0
  60. package/lib/packlets/vector/fragmentConverters.js +44 -0
  61. package/lib/packlets/vector/fragmentConverters.js.map +1 -0
  62. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts +5 -3
  63. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts.map +1 -1
  64. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js +17 -9
  65. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  66. package/lib/packlets/vector/index.d.ts +1 -0
  67. package/lib/packlets/vector/index.d.ts.map +1 -1
  68. package/lib/packlets/vector/index.js +1 -0
  69. package/lib/packlets/vector/index.js.map +1 -1
  70. package/lib/packlets/vector/vectorIndex.d.ts +97 -22
  71. package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
  72. package/lib/packlets/vector/vectorIndex.js.map +1 -1
  73. package/lib/test/unit/ingest/dedupScope.test.d.ts +2 -0
  74. package/lib/test/unit/ingest/dedupScope.test.d.ts.map +1 -0
  75. package/lib/test/unit/ingest/dedupScope.test.js +436 -0
  76. package/lib/test/unit/ingest/dedupScope.test.js.map +1 -0
  77. package/lib/test/unit/ingest/orchestrator.test.js +7 -4
  78. package/lib/test/unit/ingest/orchestrator.test.js.map +1 -1
  79. package/lib/test/unit/store/fileTreeMemoryStore.test.js +64 -0
  80. package/lib/test/unit/store/fileTreeMemoryStore.test.js.map +1 -1
  81. package/lib/test/unit/store/indexInjection.test.d.ts +2 -0
  82. package/lib/test/unit/store/indexInjection.test.d.ts.map +1 -0
  83. package/lib/test/unit/store/indexInjection.test.js +434 -0
  84. package/lib/test/unit/store/indexInjection.test.js.map +1 -0
  85. package/lib/test/unit/tools/memoryTools.test.js +1 -0
  86. package/lib/test/unit/tools/memoryTools.test.js.map +1 -1
  87. package/lib/test/unit/types/temporalPolicy.test.js +21 -0
  88. package/lib/test/unit/types/temporalPolicy.test.js.map +1 -1
  89. package/lib/test/unit/types/writePolicy.test.js +83 -0
  90. package/lib/test/unit/types/writePolicy.test.js.map +1 -1
  91. package/lib/test/unit/vector/fragmentConverters.test.d.ts +2 -0
  92. package/lib/test/unit/vector/fragmentConverters.test.d.ts.map +1 -0
  93. package/lib/test/unit/vector/fragmentConverters.test.js +60 -0
  94. package/lib/test/unit/vector/fragmentConverters.test.js.map +1 -0
  95. package/lib/test/unit/vector/inMemoryFragmentCosineIndex.test.js +65 -0
  96. package/lib/test/unit/vector/inMemoryFragmentCosineIndex.test.js.map +1 -1
  97. package/package.json +7 -7
  98. package/rush-logs/ts-agent-memory.build.cache.log +1 -1
  99. package/rush-logs/ts-agent-memory.build.log +2 -2
  100. package/src/packlets/ingest/orchestrator.ts +116 -21
  101. package/src/packlets/retrieve/fragmentSemanticRetriever.ts +10 -7
  102. package/src/packlets/store/fileTreeMemoryStore.ts +114 -6
  103. package/src/packlets/types/writePolicy.ts +40 -2
  104. package/src/packlets/vector/fragmentConverters.ts +55 -0
  105. package/src/packlets/vector/inMemoryFragmentCosineIndex.ts +35 -8
  106. package/src/packlets/vector/index.ts +1 -0
  107. package/src/packlets/vector/vectorIndex.ts +97 -22
  108. package/src/test/unit/ingest/dedupScope.test.ts +542 -0
  109. package/src/test/unit/ingest/orchestrator.test.ts +4 -0
  110. package/src/test/unit/store/fileTreeMemoryStore.test.ts +75 -0
  111. package/src/test/unit/store/indexInjection.test.ts +550 -0
  112. package/src/test/unit/tools/memoryTools.test.ts +1 -0
  113. package/src/test/unit/types/temporalPolicy.test.ts +28 -0
  114. package/src/test/unit/types/writePolicy.test.ts +112 -0
  115. package/src/test/unit/vector/fragmentConverters.test.ts +80 -0
  116. package/src/test/unit/vector/inMemoryFragmentCosineIndex.test.ts +88 -0
  117. package/temp/build/lint/_eslint-5eVG3S6w.json +30 -14
  118. package/temp/build/typescript/ts_8nwakTlr.json +1 -1
  119. package/temp/ts-agent-memory.api.json +274 -21
  120. package/temp/ts-agent-memory.api.md +12 -1
@@ -271,6 +271,22 @@ export declare const edgeTargetConverter: Converter<IEdgeTarget>;
271
271
  */
272
272
  export declare function edgeTargetKey(target: IEdgeTarget): string;
273
273
 
274
+ /**
275
+ * Converter for an {@link IEmbeddedFragment}, and the boundary that enforces the
276
+ * fragment-identifiability invariant: **at least one of `locator` / `fragmentId`
277
+ * must be present.**
278
+ *
279
+ * @remarks
280
+ * The invariant lives here rather than in the type. A conditional-required union
281
+ * (`{ locator; fragmentId? } | { locator?; fragmentId }`) was considered and
282
+ * declined: it costs at every construction site and buys nothing at the read site,
283
+ * where each field reads as `… | undefined` either way. What it must not cost is
284
+ * identifiability — a fragment carrying neither identity cannot be resolved back to
285
+ * anything by a consumer holding the query hit, so it is rejected here.
286
+ * @public
287
+ */
288
+ export declare const embeddedFragmentConverter: Converter<IEmbeddedFragment>;
289
+
274
290
  /**
275
291
  * Consumer-supplied domain key. The stable entity identity across versions;
276
292
  * the package never mints identity. Equals {@link MemoryId} for non-temporal
@@ -360,11 +376,19 @@ export declare class FileTreeMemoryStore implements IMemoryStore {
360
376
  */
361
377
  get skippedRecords(): ReadonlyArray<ISkippedRecord>;
362
378
  /**
363
- * Family-convention factory. Builds the derived index and a default LWW
364
- * policy, then performs an initial FileTree walk so an existing vault is
365
- * indexed (and the `seq` counter resumes past the highest persisted `seq`).
379
+ * Family-convention factory. Resolves the derived index (the caller's
380
+ * {@link IFileTreeMemoryStoreCreateParams.index | index} when supplied, a fresh
381
+ * {@link MemoryIndex} otherwise) and a default LWW policy, then performs an
382
+ * initial FileTree walk so an existing vault is indexed (and the `seq` counter
383
+ * resumes past the highest persisted `seq`).
366
384
  */
367
385
  static create(params: IFileTreeMemoryStoreCreateParams): Result<FileTreeMemoryStore>;
386
+ /**
387
+ * Resolve the derived index for a `create()`: the caller's injected
388
+ * {@link IMemoryIndex} verbatim, or a fresh {@link MemoryIndex} when none was
389
+ * supplied (the default that keeps an omitting caller byte-identical).
390
+ */
391
+ private static _resolveIndex;
368
392
  /** {@inheritDoc IMemoryStore.get} */
369
393
  get(kind: Kind, entityId: EntityId): Promise<Result<IMemoryRecord<unknown> | undefined>>;
370
394
  /** {@inheritDoc IMemoryStore.getById} */
@@ -625,6 +649,8 @@ export declare class FileTreeMemoryStore implements IMemoryStore {
625
649
  private _stampRank;
626
650
  private _codecFor;
627
651
  private _policyFor;
652
+ /** {@inheritDoc IMemoryStore.dedupScopeFor} */
653
+ dedupScopeFor(kind: Kind): DedupScope;
628
654
  /**
629
655
  * Read and validate the record at `<scope>/<idStem>.md`, returning `undefined`
630
656
  * when the scope directory or file is absent. Verifies the on-disk id ↔
@@ -697,20 +723,31 @@ export declare const FRAGMENT_SEMANTIC_UNWIRED_MESSAGE: string;
697
723
  */
698
724
  export declare type FragmentEmbedder = (record: IMemoryRecord<unknown>) => Promise<Result<ReadonlyArray<IEmbeddedFragment>>>;
699
725
 
726
+ /**
727
+ * Converter for an {@link IFragmentLocator}. Validates the two offsets only — the
728
+ * span's *meaning* (character / byte / token unit) is the consumer's, and the index
729
+ * never interprets it. Note the span is advisory: see {@link IFragmentLocator}.
730
+ * @public
731
+ */
732
+ export declare const fragmentLocatorConverter: Converter<IFragmentLocator>;
733
+
700
734
  /**
701
735
  * The sub-document semantic-search retriever — the "discovery" half of a
702
736
  * search-then-read contract. It embeds a fragment query, queries the
703
737
  * {@link IFragmentVectorIndex}, and returns the raw per-fragment
704
- * {@link IVectorQueryHit | hits} (each carrying a record `target` AND the matched
705
- * `locator`), NOT resolved records: the consumer re-reads each record and slices it
706
- * by the locator on its own read side.
738
+ * {@link IVectorQueryHit | hits} (each carrying a record `target` AND whichever of
739
+ * `locator` / `fragmentId` the stored fragment was indexed with), NOT resolved
740
+ * records: the consumer re-reads each record and resolves the fragment on its own
741
+ * read side. Note the `locator` span is advisory — see {@link IFragmentLocator}; it
742
+ * is not a slice guaranteed to reproduce the fragment's text.
707
743
  *
708
744
  * @remarks
709
745
  * Deliberately NOT an {@link IMemoryRetriever}: memory recall is record-granular and
710
- * returns records; fragment discovery is span-granular and returns locators. Keeping
711
- * it a distinct surface matches the consumer contract (memory stays record-granular;
712
- * sub-document knowledge uses a separate fragment index) and avoids overloading the
713
- * record retriever's return type with a locator that only makes sense here.
746
+ * returns records; fragment discovery is fragment-granular and returns fragment
747
+ * identities. Keeping it a distinct surface matches the consumer contract (memory
748
+ * stays record-granular; sub-document knowledge uses a separate fragment index) and
749
+ * avoids overloading the record retriever's return type with identity fields that
750
+ * only make sense here.
714
751
  *
715
752
  * When no backend is wired, `supportsFragmentRecall` is `false` and any fragment
716
753
  * query degrades loudly ({@link FRAGMENT_SEMANTIC_UNWIRED_MESSAGE}) — it NEVER
@@ -1016,15 +1053,44 @@ export declare interface IEdgeTarget {
1016
1053
  }
1017
1054
 
1018
1055
  /**
1019
- * One embedded fragment of a record: its in-record {@link IFragmentLocator | span}
1020
- * and the vector for that span. Produced by a {@link FragmentEmbedder} and stored
1021
- * via {@link IFragmentVectorIndex.addFragments}.
1056
+ * One embedded fragment of a record: the fragment's vector, plus at least one of the
1057
+ * two ways to identify it its advisory in-record {@link IFragmentLocator | span}
1058
+ * and/or an opaque consumer-minted {@link IEmbeddedFragment.fragmentId | fragmentId}.
1059
+ * Produced by a {@link FragmentEmbedder} and stored via
1060
+ * {@link IFragmentVectorIndex.addFragments}.
1061
+ *
1062
+ * @remarks
1063
+ * Both identity fields are optional **in the type**, but the "at least one"
1064
+ * requirement is real — a fragment carrying neither is unidentifiable at the read
1065
+ * side. It is enforced by {@link embeddedFragmentConverter} (and re-checked by the
1066
+ * in-package index implementations) rather than by a conditional-required union
1067
+ * (`{ locator; fragmentId? } | { locator?; fragmentId }`), which was considered and
1068
+ * declined: the union costs at every construction site and buys nothing at the read
1069
+ * site, where each field reads as `… | undefined` either way.
1022
1070
  * @public
1023
1071
  */
1024
1072
  export declare interface IEmbeddedFragment {
1025
- /** The fragment's in-record span. */
1026
- readonly locator: IFragmentLocator;
1027
- /** The embedding vector for that span. */
1073
+ /**
1074
+ * The region of the record body this fragment was derived from, when one can be
1075
+ * stated honestly. Advisory see {@link IFragmentLocator}; it is NOT a slice that
1076
+ * reproduces the fragment text. Omit it for a fragment with no honest body span (a
1077
+ * rewriting segmenter), in which case `fragmentId` must be supplied.
1078
+ */
1079
+ readonly locator?: IFragmentLocator;
1080
+ /**
1081
+ * An opaque, consumer-minted identity for this fragment, carried verbatim through
1082
+ * the index and returned on the corresponding {@link IVectorQueryHit}. The index
1083
+ * **never parses it, never filters on it, and never assigns meaning to it** — it is
1084
+ * a bytestring, not part of the query path. It exists so a fragment stays
1085
+ * identifiable when its text is not re-derivable from the record body.
1086
+ *
1087
+ * The guarantee is "we never parse it", NOT "we keep it stable". Because
1088
+ * `addFragments` is whole-record-replace, an updated record re-emits its entire
1089
+ * fragment set, so **any stability of a fragment id across re-embeds is the
1090
+ * consumer's responsibility**, not the index's.
1091
+ */
1092
+ readonly fragmentId?: string;
1093
+ /** The embedding vector for this fragment. */
1028
1094
  readonly vector: Float32Array;
1029
1095
  }
1030
1096
 
@@ -1110,6 +1176,59 @@ export declare interface IFileTreeMemoryStoreCreateParams {
1110
1176
  readonly rankProjectors?: ReadonlyMap<Kind, RankProjector>;
1111
1177
  /** Default codec for kinds without an explicit entry. */
1112
1178
  readonly defaultCodec?: IIdentityCodec;
1179
+ /**
1180
+ * Optional derived-index implementation. Defaults to a fresh {@link MemoryIndex} —
1181
+ * omitting this parameter is byte-identical to the store's behavior before the
1182
+ * parameter existed. When supplied, the store uses it for EVERY index operation
1183
+ * it performs and never holds a second index, so an injected index is the store's
1184
+ * only view of its own records:
1185
+ *
1186
+ * - `rebuild` — once, from the initial vault walk in `create()`.
1187
+ * - `patch` — on every persisted write, delete, version invalidation, and
1188
+ * cap-cull eviction.
1189
+ * - `entries` — behind {@link IMemoryStore.list | list} /
1190
+ * {@link IMemoryStore.listScoped | listScoped}, the keyed temporal reads, the
1191
+ * write path's content-hash dedup and write-policy admission cohort, AND the
1192
+ * temporal (versioned) write and delete paths, which resolve an entity's
1193
+ * version history entirely from the index.
1194
+ *
1195
+ * That last group is the one to weigh before injecting anything other than a
1196
+ * pass-through decorator: an index that filters, reorders, or otherwise reshapes
1197
+ * `entries()` changes WRITE semantics, not just what reads return. Concretely,
1198
+ * on a versioned kind the store derives an entity's whole version history from
1199
+ * `entries()` filtered by scope, and that derivation decides which version a
1200
+ * `put` treats as current (so what it dedups against and what it merges its
1201
+ * patch over), which prior versions it stamps `invalid_at` on, what the
1202
+ * admission cohort is, and which versions a `delete` tombstones. An index that
1203
+ * hides a version makes it invisible to all of those — the FileTree still holds
1204
+ * it, but the store will not supersede, invalidate, or tombstone it. On flat
1205
+ * kinds the same reshaping changes what dedups and what a cap-cull policy
1206
+ * evicts. A faithful delegating decorator — the intended use below — has no such
1207
+ * effect. Note the store's keyed reads ({@link IMemoryStore.get | get} on a flat
1208
+ * kind, and {@link IMemoryStore.getById | getById}) go to the FileTree, not the
1209
+ * index; the FileTree remains the source of truth and the index stays a derived
1210
+ * view.
1211
+ *
1212
+ * @remarks
1213
+ * **This is an instrumentation seam, NOT a resident-memory fix.** The intended
1214
+ * use is wrapping the shipped {@link MemoryIndex} in a decorator that counts and
1215
+ * times the calls the store makes — resident bytes by kind, open cost against
1216
+ * vault size, where the curve actually bends — so a decision about a partial-read
1217
+ * redesign can be driven by measurements instead of estimates.
1218
+ *
1219
+ * It does NOT lower the store's resident-memory ceiling, and injecting a
1220
+ * "persisted" or "lazy" index will not change that. {@link IMemoryIndex}'s read
1221
+ * surface returns whole records by construction: `entries()` yields
1222
+ * {@link IIndexedMemoryRecord}s and `byKind` / `byTag` / `byRecency` / `byRank`
1223
+ * yield `IMemoryRecord<unknown>` — `{ envelope, body }` pairs with the body
1224
+ * materialized. Any implementation satisfying the current contract must therefore
1225
+ * be able to produce every body on demand. An injected index changes WHERE records
1226
+ * come from; it does not change WHETHER bodies are held. Lowering the ceiling
1227
+ * requires a partial-read (id-or-envelope-only) redesign of `IMemoryIndex` itself,
1228
+ * which is separate, breaking, design-first work and is deliberately not part of
1229
+ * this seam.
1230
+ */
1231
+ readonly index?: IMemoryIndex;
1113
1232
  /** Scope encoding. Defaults to {@link defaultMemoryScopeEncoding}. */
1114
1233
  readonly scopeEncoding?: (scope: MemoryScopeKey) => Result<string>;
1115
1234
  /**
@@ -1192,10 +1311,24 @@ export declare interface IFileTreeMemoryStoreCreateParams {
1192
1311
 
1193
1312
  /**
1194
1313
  * A half-open `[start, end)` span into a record's body — the in-record locator a
1195
- * {@link IFragmentVectorIndex} carries on each fragment hit. `start` is inclusive,
1314
+ * {@link IFragmentVectorIndex} may carry on a fragment. `start` is inclusive,
1196
1315
  * `end` exclusive. The unit (character / byte / token offsets) is the consumer's
1197
1316
  * choice: the index stores the two integers opaquely and never interprets them,
1198
1317
  * so they line up with whatever locator the consumer's own read side uses.
1318
+ *
1319
+ * @remarks
1320
+ * **The span is advisory.** It names the region of the body a fragment was
1321
+ * *derived from*; it is NOT a slice guaranteed to reproduce the fragment's text.
1322
+ * `body.slice(start, end)` round-trips only under a segmenter that merely chooses
1323
+ * boundaries. Under a **rewriting** segmenter — one that turns a span into a
1324
+ * curated block, an increasingly common ingestion shape when a model both selects
1325
+ * and rewrites — the fragment text is not a substring of the body at all, and the
1326
+ * fragmentation is not re-derivable from the body. Treat the span as a pointer for
1327
+ * locating context, never as an extraction recipe.
1328
+ *
1329
+ * A fragment whose provenance cannot honestly be expressed as a body span should
1330
+ * omit the locator entirely and carry an {@link IEmbeddedFragment.fragmentId}
1331
+ * instead.
1199
1332
  * @public
1200
1333
  */
1201
1334
  export declare interface IFragmentLocator {
@@ -1247,18 +1380,21 @@ export declare interface IFragmentSemanticBackend {
1247
1380
 
1248
1381
  /**
1249
1382
  * The fragment-granular sibling of {@link IVectorIndex}: instead of one vector per
1250
- * record it holds many vectors per record, each tagged with an in-record
1251
- * {@link IFragmentLocator}, and its `query` returns per-fragment hits carrying that
1252
- * locator. This is the seam behind sub-document semantic search — the "discovery"
1253
- * half of a search-then-read contract, where a hit's `(target, locator)` tells the
1254
- * consumer which record AND which span to read.
1383
+ * record it holds many vectors per record, each tagged with the identity its
1384
+ * {@link IEmbeddedFragment} carried, and its `query` returns per-fragment hits
1385
+ * carrying that identity back. This is the seam behind sub-document semantic search
1386
+ * — the "discovery" half of a search-then-read contract, where a hit tells the
1387
+ * consumer which record AND which fragment of it to read.
1255
1388
  *
1256
1389
  * @remarks
1257
- * Deliberately NOT `extends IVectorIndex`: an index keyed by `(target, locator)`
1258
- * has no well-defined single-vector `add(target, vector)`. It is a parallel
1259
- * contract with three operations — `addFragments`, `remove`, `query` — reusing
1260
- * {@link IVectorQueryHit} (whose `locator` is always populated here). Kept distinct
1261
- * from the record-granular index per the consumer contract: memory recall stays
1390
+ * Deliberately NOT `extends IVectorIndex`: an index holding many vectors per record
1391
+ * has no well-defined single-vector `add(target, vector)`. It is a parallel contract
1392
+ * with three operations — `addFragments`, `remove`, `query` — reusing
1393
+ * {@link IVectorQueryHit}, on which both `locator` and `fragmentId` are optional. A
1394
+ * fragment hit populates whichever of the two its stored fragment carried; see
1395
+ * {@link IVectorQueryHit} for why that is not a discriminator and why fragment-ness
1396
+ * is determined by the index queried, not by field presence. Kept distinct from the
1397
+ * record-granular index per the consumer contract: memory recall stays
1262
1398
  * record-granular; sub-document knowledge uses a separate fragment index.
1263
1399
  * @public
1264
1400
  */
@@ -1277,7 +1413,8 @@ export declare interface IFragmentVectorIndex {
1277
1413
  remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>>;
1278
1414
  /**
1279
1415
  * Return the `topK` nearest fragments to `vector`, in descending score order,
1280
- * each hit carrying its record `target` and fragment `locator`. When
1416
+ * each hit carrying its record `target` plus whichever of `locator` /
1417
+ * `fragmentId` the stored fragment was added with. When
1281
1418
  * `maxPerRecord` is supplied, no more than that many fragments of any single
1282
1419
  * record appear in the result — the cap is applied during selection (before the
1283
1420
  * `topK` cut) so one long document cannot crowd out others.
@@ -1441,6 +1578,15 @@ export declare interface IMemoryCapCullPolicyParams {
1441
1578
  * The fields a merge-patch update may touch (drawn from the record-level
1442
1579
  * mutable vocabulary: `body` / `tags` / `links` / `provenance` /
1443
1580
  * `embeddingRef`). Fields outside this list are immutable.
1581
+ *
1582
+ * @remarks
1583
+ * This list — unlike {@link KnowledgeLwwPolicy}'s pinned surface — is
1584
+ * caller-supplied, so it is what decides which fields get the RFC-7386
1585
+ * guarantees documented on {@link IWritePolicy.applyUpdate}. Declare
1586
+ * `provenance` here to get per-key provenance merging and `null` sub-key
1587
+ * clearing; omit it and every provenance patch key is inert (dropped before
1588
+ * the merge, so the existing value is preserved verbatim and even a
1589
+ * whole-block `null` is a silent no-op rather than an error).
1444
1590
  */
1445
1591
  readonly mutableFields: ReadonlyArray<string>;
1446
1592
  }
@@ -1943,6 +2089,34 @@ export declare interface IMemoryStore {
1943
2089
  * seam's `list()` returns scope-qualified records.
1944
2090
  */
1945
2091
  asRecordSource(): IMemoryRecordSource;
2092
+ /**
2093
+ * The EFFECTIVE {@link DedupScope} for `kind` — the granularity at which a
2094
+ * write for this kind deduplicates against the existing vault.
2095
+ *
2096
+ * @remarks
2097
+ * This is a **read accessor over the store's already-injected write policies**,
2098
+ * and it is the single place any caller — the store's own write path included —
2099
+ * asks what a kind's dedup granularity is. It resolves the full chain the store
2100
+ * applies on write: the kind's registered {@link IWritePolicy}, falling back to
2101
+ * the store's default policy, then that policy's
2102
+ * {@link IWritePolicy.dedupScope | dedupScope}, falling back to
2103
+ * {@link DEFAULT_DEDUP_SCOPE}. Note the store's default policy is a
2104
+ * {@link KnowledgeLwwPolicy}, which declares `'content'` — so a kind with NO
2105
+ * registered policy resolves to `'content'`, not to `DEFAULT_DEDUP_SCOPE`.
2106
+ *
2107
+ * It exists so a caller that must agree with the store about dedup granularity
2108
+ * — notably the ingest orchestrator's stage-4 layer-1 exact match — can read the
2109
+ * declaration through this seam instead of being handed a second copy of the
2110
+ * policy map. A second declaration site is precisely the defect this accessor
2111
+ * was added to remove.
2112
+ *
2113
+ * Deliberately synchronous, total, and NOT `Result`-returning: it reads
2114
+ * constructor-injected configuration, touches no I/O, and cannot fail (every
2115
+ * link in the fallback chain has a total default). It exposes only the scope,
2116
+ * never the {@link IWritePolicy} itself, so it can never become a back door for
2117
+ * invoking admission or merge logic out of band.
2118
+ */
2119
+ dedupScopeFor(kind: Kind): DedupScope;
1946
2120
  /**
1947
2121
  * Write a record. Validates the body, computes a content hash, deduplicates
1948
2122
  * (scope-wide, before policy), applies the kind's {@link IWritePolicy}, stamps
@@ -2122,9 +2296,11 @@ export declare class InMemoryCosineIndex implements IVectorIndex {
2122
2296
  /**
2123
2297
  * The brute-force, in-memory cosine {@link IFragmentVectorIndex} — the
2124
2298
  * fragment-granular sibling of {@link InMemoryCosineIndex}. Stores many
2125
- * `Float32Array`s per record (one per in-record {@link IFragmentLocator | span})
2126
- * and answers a query by computing cosine similarity against every stored
2127
- * fragment, returning the top-k fragment hits by descending score.
2299
+ * `Float32Array`s per record (one per {@link IEmbeddedFragment | fragment}) and
2300
+ * answers a query by computing cosine similarity against every stored fragment,
2301
+ * returning the top-k fragment hits by descending score. Each hit carries back
2302
+ * whichever of {@link IFragmentLocator | locator} / `fragmentId` its fragment was
2303
+ * added with; a fragment must carry at least one of the two.
2128
2304
  *
2129
2305
  * @remarks
2130
2306
  * Same regime and same non-goals as {@link InMemoryCosineIndex}: no external
@@ -2452,9 +2628,27 @@ export declare interface IVectorIndex {
2452
2628
  * that share a stem. The caller re-resolves the hit against the record index by
2453
2629
  * the same scoped address.
2454
2630
  *
2455
- * `locator` is present only on hits from a {@link IFragmentVectorIndex} — it
2456
- * identifies WHICH fragment of the record matched. Record-granular
2457
- * {@link IVectorIndex} hits omit it.
2631
+ * **No single field discriminates a fragment hit from a record-granular hit.** A
2632
+ * record hit carries neither `locator` nor `fragmentId`; a fragment hit carries at
2633
+ * least one of the two, but not necessarily any particular one — a fragment with a
2634
+ * body span but no consumer-minted id, and a fragment with an id but no honest span,
2635
+ * are both legal. Testing one field for presence therefore cannot tell you which
2636
+ * kind of hit you hold.
2637
+ *
2638
+ * That "at least one" requirement is enforced on the upsert side by
2639
+ * {@link embeddedFragmentConverter} — a different boundary from this type — and is
2640
+ * deliberately NOT offered here as a discriminator either. A caller keyed off it
2641
+ * would be coupled to an invariant this type does not own, and would fail silently
2642
+ * if the invariant were ever relaxed.
2643
+ *
2644
+ * **The robust rule is that fragment-ness is determined by which index produced the
2645
+ * hit**: {@link IFragmentVectorIndex.query} returns fragment hits and
2646
+ * {@link IVectorIndex.query} returns record hits. The caller chose the index it
2647
+ * queried, so it already knows which kind it is holding.
2648
+ *
2649
+ * Note in particular that an absent `locator` now carries **two** distinct meanings
2650
+ * — a record-granular hit, or a fragment with no honest body span (see
2651
+ * {@link IFragmentLocator}) — which is precisely why presence-branching is unsafe.
2458
2652
  * @public
2459
2653
  */
2460
2654
  export declare interface IVectorQueryHit {
@@ -2462,8 +2656,18 @@ export declare interface IVectorQueryHit {
2462
2656
  readonly target: IEdgeTarget;
2463
2657
  /** Backend similarity score; higher is more similar. */
2464
2658
  readonly score: number;
2465
- /** The matched fragment's in-record span; present only for fragment-index hits. */
2659
+ /**
2660
+ * The advisory in-record span the matched fragment was derived from, when the
2661
+ * producing fragment carried one. Absent on record-granular hits AND on fragment
2662
+ * hits with no honest span — see the remarks above; do not branch on its presence.
2663
+ */
2466
2664
  readonly locator?: IFragmentLocator;
2665
+ /**
2666
+ * The opaque identity the producing fragment was stored with, carried back
2667
+ * verbatim. Absent on record-granular hits AND on fragment hits stored without
2668
+ * one — see the remarks above; do not branch on its presence.
2669
+ */
2670
+ readonly fragmentId?: string;
2467
2671
  }
2468
2672
 
2469
2673
  /**
@@ -2504,6 +2708,27 @@ export declare interface IWritePolicy {
2504
2708
  * record. Called when admission is `accept` AND a record with the same
2505
2709
  * `entityId` already exists (an update, not a first write).
2506
2710
  *
2711
+ * @remarks
2712
+ * **RFC-7386 semantics are the contract here, not an artifact of the shipped
2713
+ * policies' merge configuration.** Implementations are expected to honor them,
2714
+ * and consumers may rely on them:
2715
+ *
2716
+ * - **Objects merge per key.** A supplied key overwrites; an omitted key is
2717
+ * PRESERVED, not dropped. Patching one key of a nested object (e.g. one
2718
+ * field of `provenance`) leaves its siblings intact.
2719
+ * - **An explicit `null` on a sub-key clears exactly that sub-key.** This is
2720
+ * the sanctioned way to remove a single key from a nested object.
2721
+ * - **Arrays replace wholesale** — `tags` / `links` are never element-merged.
2722
+ * - **A whole-block `null` that would delete a REQUIRED field is rejected
2723
+ * loudly** (`Result.fail`), never silently accepted. `body` / `tags` /
2724
+ * `links` / `provenance` are required; `embeddingRef` is optional and a
2725
+ * `null` restores it to absent (NOT to `null`), so it stays hash-stable.
2726
+ *
2727
+ * Which fields these guarantees cover is **policy-dependent**: they apply to
2728
+ * the fields the policy declares in {@link IWritePolicy.mutableFields}, and a
2729
+ * field outside that list is inert — its patch key is dropped before the merge,
2730
+ * so a `null` on it neither clears the value nor raises an error.
2731
+ *
2507
2732
  * @param existing - The current persisted record.
2508
2733
  * @param patch - A partial JSON object in Merge Patch format. `null`
2509
2734
  * deletes the corresponding key; arrays replace wholesale; nested objects
@@ -2564,13 +2789,21 @@ export declare class KnowledgeIdentityCodec implements IIdentityCodec {
2564
2789
  * view, runs the merge over that view, then rebuilds a coherent record. The
2565
2790
  * identity and transaction-time envelope fields (`id`, `entityId`, `kind`,
2566
2791
  * `created`, `updated`, `seq`, `contentHash`) are NOT mutable and are
2567
- * preserved verbatim; the store stamps `updated` / `seq` on write.
2792
+ * preserved verbatim; the store stamps `updated` / `seq` / `contentHash` on write.
2793
+ *
2794
+ * Because the surface is pinned rather than caller-supplied, the RFC-7386
2795
+ * guarantees documented on {@link IWritePolicy.applyUpdate} apply to every field
2796
+ * listed above — in particular to `provenance`, whose keys merge individually, a
2797
+ * `null` on any one of which clears that key alone, and a `null` on the whole
2798
+ * block of which is rejected loudly (it is a required field). Consumers may
2799
+ * depend on this; it is covered by tests.
2568
2800
  * @public
2569
2801
  */
2570
2802
  export declare class KnowledgeLwwPolicy implements IWritePolicy {
2571
2803
  /**
2572
2804
  * The knowledge mutable surface: the body plus the envelope metadata a
2573
- * consumer may revise without minting a new entity.
2805
+ * consumer may revise without minting a new entity. Pinned, not
2806
+ * caller-supplied — see the class remarks for what that guarantees.
2574
2807
  */
2575
2808
  readonly mutableFields: ReadonlyArray<string>;
2576
2809
  /**
@@ -2940,7 +3173,9 @@ export declare class MemoryIngestOrchestrator implements IMemoryIngestOrchestrat
2940
3173
  private _planFromVerdict;
2941
3174
  /**
2942
3175
  * Stage 4 — resolve a dedup verdict. Layer 1: an exact `{ kind, body }` match in
2943
- * the candidate's scope is a `duplicate-of` (design note §1). Layer 2 (only when
3176
+ * the candidate's scope is a `duplicate-of`, at the granularity the kind's
3177
+ * {@link DedupScope} declares — scope-wide for `'content'`, restricted to the
3178
+ * candidate's own entity for `'entity'` (design note §1). Layer 2 (only when
2944
3179
  * a resolver + vector index + embedder are all wired): embed the candidate,
2945
3180
  * surface over-threshold neighbors, and dispatch to the {@link IEntityResolver}.
2946
3181
  * Otherwise the verdict is `new` (the exact-only fall-back path).
@@ -2951,9 +3186,26 @@ export declare class MemoryIngestOrchestrator implements IMemoryIngestOrchestrat
2951
3186
  /** Layer-2 continuation once the candidate has a provisional record to embed. */
2952
3187
  private _resolveViaSimilarityEmbedded;
2953
3188
  /**
2954
- * Find an existing record in `scope` whose `{ kind, body }` hash matches the
2955
- * candidate's (layer-1 exact dedup). Invalidated temporal versions are excluded
2956
- * — only a live (non-temporal or current) record deduplicates a candidate.
3189
+ * Find an existing record whose `{ kind, body }` hash matches the candidate's
3190
+ * (layer-1 exact dedup, design note §1). Invalidated temporal versions are
3191
+ * excluded — only a live (non-temporal or current) record deduplicates a
3192
+ * candidate.
3193
+ *
3194
+ * @remarks
3195
+ * The cohort's granularity is the kind's DECLARED {@link DedupScope}, read
3196
+ * through {@link IMemoryStore.dedupScopeFor} so this layer and the store's own
3197
+ * write path can never disagree:
3198
+ *
3199
+ * - `'content'` — every live same-kind record in the candidate's scope,
3200
+ * regardless of entity. A cross-id body collision IS a duplicate (the
3201
+ * knowledge family; unchanged behavior).
3202
+ * - `'entity'` — only records at the candidate's OWN entity address. Two
3203
+ * distinct entities with byte-identical bodies never collapse into one
3204
+ * another, which is what the declaration has always meant on the direct-put
3205
+ * path and now means here too.
3206
+ *
3207
+ * The same-id collapse itself remains the store's job — this layer only decides
3208
+ * whether a cross-id body collision is eligible to be a `duplicate-of` at all.
2957
3209
  */
2958
3210
  private _findExactMatch;
2959
3211
  /**
@@ -2965,8 +3217,46 @@ export declare class MemoryIngestOrchestrator implements IMemoryIngestOrchestrat
2965
3217
  private static _recordBodyString;
2966
3218
  /** The stage-4 exact-dedup key over `{ kind, body }` (design note §1). */
2967
3219
  private _exactKey;
2968
- /** Stage 5 — relate (host), validate edges, and run the write-time cycle guard. */
3220
+ /** Stage 5 — relate (host), redirect collapsed targets, validate edges, and run the write-time cycle guard. */
2969
3221
  private _relate;
3222
+ /**
3223
+ * The stage-5 edge-target redirect map for this pass: every `duplicate-of`
3224
+ * candidate's OWN scoped address, mapped to the address of the record its
3225
+ * verdict collapsed it into (design note §3).
3226
+ *
3227
+ * @remarks
3228
+ * A `duplicate-of` verdict means "this candidate IS that record". The candidate
3229
+ * is therefore not written, and its address never becomes a live reference — so
3230
+ * a sibling edge built against it in the same pass would resolve to neither a
3231
+ * written candidate nor an existing record and would fail the WHOLE ingest item.
3232
+ * That is a second-order hazard of collapsing, not a defect in the edge: the
3233
+ * host related two candidates it was correctly told about, and one of them
3234
+ * turned out to already exist.
3235
+ *
3236
+ * Redirecting is the honest repair, and it is independent of {@link DedupScope}
3237
+ * — it applies equally to a `'content'` kind, where the collapse is exactly
3238
+ * right and the ingest still must not fail.
3239
+ *
3240
+ * The map cannot chain: {@link MemoryIngestOrchestrator._planFromVerdict}
3241
+ * requires every target-bearing verdict's target to already exist in the store
3242
+ * snapshot, so a redirect destination is always a persisted record and never
3243
+ * another collapsed candidate. One pass is sufficient by construction.
3244
+ */
3245
+ private static _collapseRedirects;
3246
+ /**
3247
+ * Rewrite each edge whose TARGET names a collapsed candidate to point at the
3248
+ * record that candidate collapsed into.
3249
+ *
3250
+ * @remarks
3251
+ * Only the target is redirected. An edge whose SOURCE is a collapsed candidate
3252
+ * stays untouched and is still rejected by {@link
3253
+ * MemoryIngestOrchestrator._validateEdges}: sources are the records an edge is
3254
+ * written ONTO, a collapsed candidate is never written, and the relation
3255
+ * extractor is only ever offered writable candidates as edge sources in the
3256
+ * first place. Silently relocating such an edge onto an existing record would
3257
+ * attribute a link the host never asked for — the loud failure is correct there.
3258
+ */
3259
+ private static _redirectEdges;
2970
3260
  /**
2971
3261
  * Validate stage-5 edges: each `source` must be a candidate being written; each
2972
3262
  * `target` must resolve to a sibling candidate or an existing store record. All
@@ -102,6 +102,9 @@ export const edgeTargetConverter: Converter<IEdgeTarget>;
102
102
  // @public
103
103
  export function edgeTargetKey(target: IEdgeTarget): string;
104
104
 
105
+ // @public
106
+ export const embeddedFragmentConverter: Converter<IEmbeddedFragment>;
107
+
105
108
  // @public
106
109
  export type EntityId = Brand<string, 'EntityId'>;
107
110
 
@@ -115,6 +118,7 @@ export const envelopeYamlConverter: Converter<IMemoryEnvelope>;
115
118
  export class FileTreeMemoryStore implements IMemoryStore {
116
119
  asRecordSource(): IMemoryRecordSource;
117
120
  static create(params: IFileTreeMemoryStoreCreateParams): Result<FileTreeMemoryStore>;
121
+ dedupScopeFor(kind: Kind): DedupScope;
118
122
  delete(kind: Kind, entityId: EntityId): Promise<Result<MemoryId>>;
119
123
  get(kind: Kind, entityId: EntityId): Promise<Result<IMemoryRecord<unknown> | undefined>>;
120
124
  getById(scope: MemoryScopeKey, id: MemoryId): Promise<Result<IMemoryRecord<unknown> | undefined>>;
@@ -130,6 +134,9 @@ export const FRAGMENT_SEMANTIC_UNWIRED_MESSAGE: string;
130
134
  // @public
131
135
  export type FragmentEmbedder = (record: IMemoryRecord<unknown>) => Promise<Result<ReadonlyArray<IEmbeddedFragment>>>;
132
136
 
137
+ // @public
138
+ export const fragmentLocatorConverter: Converter<IFragmentLocator>;
139
+
133
140
  // @public
134
141
  export class FragmentSemanticRetriever {
135
142
  get capabilities(): IFragmentRetrieverCapabilities;
@@ -221,7 +228,8 @@ export interface IEdgeTarget {
221
228
 
222
229
  // @public
223
230
  export interface IEmbeddedFragment {
224
- readonly locator: IFragmentLocator;
231
+ readonly fragmentId?: string;
232
+ readonly locator?: IFragmentLocator;
225
233
  readonly vector: Float32Array;
226
234
  }
227
235
 
@@ -250,6 +258,7 @@ export interface IFileTreeMemoryStoreCreateParams {
250
258
  readonly embed?: MemoryEmbedder;
251
259
  readonly fragmentEmbedder?: FragmentEmbedder;
252
260
  readonly fragmentIndex?: IFragmentVectorIndex;
261
+ readonly index?: IMemoryIndex;
253
262
  readonly logger?: Logging.ILogger;
254
263
  readonly observers?: ReadonlyArray<IMemoryObserver>;
255
264
  readonly onRecordError?: MemoryRecordErrorMode;
@@ -497,6 +506,7 @@ export interface IMemoryRetrieverCapabilities {
497
506
  // @public
498
507
  export interface IMemoryStore {
499
508
  asRecordSource(): IMemoryRecordSource;
509
+ dedupScopeFor(kind: Kind): DedupScope;
500
510
  delete(kind: Kind, entityId: EntityId): Promise<Result<MemoryId>>;
501
511
  get(kind: Kind, entityId: EntityId): Promise<Result<IMemoryRecord<unknown> | undefined>>;
502
512
  getById(scope: MemoryScopeKey, id: MemoryId): Promise<Result<IMemoryRecord<unknown> | undefined>>;
@@ -653,6 +663,7 @@ export interface IVectorIndex {
653
663
 
654
664
  // @public
655
665
  export interface IVectorQueryHit {
666
+ readonly fragmentId?: string;
656
667
  readonly locator?: IFragmentLocator;
657
668
  readonly score: number;
658
669
  readonly target: IEdgeTarget;