@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.
- package/.rush/temp/{bc5b0f2098f7df2a5c498ea77b0de13316349b96.tar.log → 5667e36f1c0cc503a5c20a3ebff76e7b9a702d41.tar.log} +26 -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/README.md +134 -4
- package/dist/packlets/ingest/orchestrator.js +104 -19
- package/dist/packlets/ingest/orchestrator.js.map +1 -1
- package/dist/packlets/retrieve/fragmentSemanticRetriever.js +10 -7
- package/dist/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -1
- package/dist/packlets/store/fileTreeMemoryStore.js +31 -8
- package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
- package/dist/packlets/types/writePolicy.js +10 -2
- package/dist/packlets/types/writePolicy.js.map +1 -1
- package/dist/packlets/vector/fragmentConverters.js +41 -0
- package/dist/packlets/vector/fragmentConverters.js.map +1 -0
- package/dist/packlets/vector/inMemoryFragmentCosineIndex.js +17 -9
- package/dist/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
- package/dist/packlets/vector/index.js +1 -0
- package/dist/packlets/vector/index.js.map +1 -1
- package/dist/packlets/vector/vectorIndex.js.map +1 -1
- package/dist/test/unit/ingest/dedupScope.test.js +434 -0
- package/dist/test/unit/ingest/dedupScope.test.js.map +1 -0
- package/dist/test/unit/ingest/orchestrator.test.js +7 -4
- package/dist/test/unit/ingest/orchestrator.test.js.map +1 -1
- package/dist/test/unit/store/fileTreeMemoryStore.test.js +64 -0
- package/dist/test/unit/store/fileTreeMemoryStore.test.js.map +1 -1
- package/dist/test/unit/store/indexInjection.test.js +432 -0
- package/dist/test/unit/store/indexInjection.test.js.map +1 -0
- package/dist/test/unit/tools/memoryTools.test.js +1 -0
- package/dist/test/unit/tools/memoryTools.test.js.map +1 -1
- package/dist/test/unit/types/temporalPolicy.test.js +21 -0
- package/dist/test/unit/types/temporalPolicy.test.js.map +1 -1
- package/dist/test/unit/types/writePolicy.test.js +83 -0
- package/dist/test/unit/types/writePolicy.test.js.map +1 -1
- package/dist/test/unit/vector/fragmentConverters.test.js +58 -0
- package/dist/test/unit/vector/fragmentConverters.test.js.map +1 -0
- package/dist/test/unit/vector/inMemoryFragmentCosineIndex.test.js +65 -0
- package/dist/test/unit/vector/inMemoryFragmentCosineIndex.test.js.map +1 -1
- package/dist/ts-agent-memory.d.ts +332 -42
- package/etc/ts-agent-memory.api.md +12 -1
- package/lib/packlets/ingest/orchestrator.d.ts +62 -5
- package/lib/packlets/ingest/orchestrator.d.ts.map +1 -1
- package/lib/packlets/ingest/orchestrator.js +104 -19
- package/lib/packlets/ingest/orchestrator.js.map +1 -1
- package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts +10 -7
- package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/fragmentSemanticRetriever.js +10 -7
- package/lib/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -1
- package/lib/packlets/store/fileTreeMemoryStore.d.ts +96 -4
- package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
- package/lib/packlets/store/fileTreeMemoryStore.js +31 -8
- package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
- package/lib/packlets/types/writePolicy.d.ts +40 -2
- package/lib/packlets/types/writePolicy.d.ts.map +1 -1
- package/lib/packlets/types/writePolicy.js +10 -2
- package/lib/packlets/types/writePolicy.js.map +1 -1
- package/lib/packlets/vector/fragmentConverters.d.ts +25 -0
- package/lib/packlets/vector/fragmentConverters.d.ts.map +1 -0
- package/lib/packlets/vector/fragmentConverters.js +44 -0
- package/lib/packlets/vector/fragmentConverters.js.map +1 -0
- package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts +5 -3
- package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts.map +1 -1
- package/lib/packlets/vector/inMemoryFragmentCosineIndex.js +17 -9
- package/lib/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
- package/lib/packlets/vector/index.d.ts +1 -0
- package/lib/packlets/vector/index.d.ts.map +1 -1
- package/lib/packlets/vector/index.js +1 -0
- package/lib/packlets/vector/index.js.map +1 -1
- package/lib/packlets/vector/vectorIndex.d.ts +97 -22
- package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
- package/lib/packlets/vector/vectorIndex.js.map +1 -1
- package/lib/test/unit/ingest/dedupScope.test.d.ts +2 -0
- package/lib/test/unit/ingest/dedupScope.test.d.ts.map +1 -0
- package/lib/test/unit/ingest/dedupScope.test.js +436 -0
- package/lib/test/unit/ingest/dedupScope.test.js.map +1 -0
- package/lib/test/unit/ingest/orchestrator.test.js +7 -4
- package/lib/test/unit/ingest/orchestrator.test.js.map +1 -1
- package/lib/test/unit/store/fileTreeMemoryStore.test.js +64 -0
- package/lib/test/unit/store/fileTreeMemoryStore.test.js.map +1 -1
- package/lib/test/unit/store/indexInjection.test.d.ts +2 -0
- package/lib/test/unit/store/indexInjection.test.d.ts.map +1 -0
- package/lib/test/unit/store/indexInjection.test.js +434 -0
- package/lib/test/unit/store/indexInjection.test.js.map +1 -0
- package/lib/test/unit/tools/memoryTools.test.js +1 -0
- package/lib/test/unit/tools/memoryTools.test.js.map +1 -1
- package/lib/test/unit/types/temporalPolicy.test.js +21 -0
- package/lib/test/unit/types/temporalPolicy.test.js.map +1 -1
- package/lib/test/unit/types/writePolicy.test.js +83 -0
- package/lib/test/unit/types/writePolicy.test.js.map +1 -1
- package/lib/test/unit/vector/fragmentConverters.test.d.ts +2 -0
- package/lib/test/unit/vector/fragmentConverters.test.d.ts.map +1 -0
- package/lib/test/unit/vector/fragmentConverters.test.js +60 -0
- package/lib/test/unit/vector/fragmentConverters.test.js.map +1 -0
- package/lib/test/unit/vector/inMemoryFragmentCosineIndex.test.js +65 -0
- package/lib/test/unit/vector/inMemoryFragmentCosineIndex.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/ingest/orchestrator.ts +116 -21
- package/src/packlets/retrieve/fragmentSemanticRetriever.ts +10 -7
- package/src/packlets/store/fileTreeMemoryStore.ts +114 -6
- package/src/packlets/types/writePolicy.ts +40 -2
- package/src/packlets/vector/fragmentConverters.ts +55 -0
- package/src/packlets/vector/inMemoryFragmentCosineIndex.ts +35 -8
- package/src/packlets/vector/index.ts +1 -0
- package/src/packlets/vector/vectorIndex.ts +97 -22
- package/src/test/unit/ingest/dedupScope.test.ts +542 -0
- package/src/test/unit/ingest/orchestrator.test.ts +4 -0
- package/src/test/unit/store/fileTreeMemoryStore.test.ts +75 -0
- package/src/test/unit/store/indexInjection.test.ts +550 -0
- package/src/test/unit/tools/memoryTools.test.ts +1 -0
- package/src/test/unit/types/temporalPolicy.test.ts +28 -0
- package/src/test/unit/types/writePolicy.test.ts +112 -0
- package/src/test/unit/vector/fragmentConverters.test.ts +80 -0
- package/src/test/unit/vector/inMemoryFragmentCosineIndex.test.ts +88 -0
- package/temp/build/lint/_eslint-5eVG3S6w.json +30 -14
- package/temp/build/typescript/ts_8nwakTlr.json +1 -1
- package/temp/ts-agent-memory.api.json +274 -21
- package/temp/ts-agent-memory.api.md +12 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
Start time:
|
|
2
|
-
Invoking "/usr/bin/tar -c -f /home/runner/work/fgv/fgv/common/temp/build-cache/
|
|
1
|
+
Start time: Fri Aug 07 2026 01:16:23 GMT+0000 (Coordinated Universal Time)
|
|
2
|
+
Invoking "/usr/bin/tar -c -f /home/runner/work/fgv/fgv/common/temp/build-cache/5667e36f1c0cc503a5c20a3ebff76e7b9a702d41-f9816615898c604e.temp -z --files-from=-"
|
|
3
3
|
|
|
4
4
|
======= BEGIN PROCESS INPUT ======
|
|
5
5
|
.rush/temp/operation/build/all.log
|
|
@@ -77,6 +77,8 @@ dist/packlets/types/temporal.js
|
|
|
77
77
|
dist/packlets/types/temporal.js.map
|
|
78
78
|
dist/packlets/types/writePolicy.js
|
|
79
79
|
dist/packlets/types/writePolicy.js.map
|
|
80
|
+
dist/packlets/vector/fragmentConverters.js
|
|
81
|
+
dist/packlets/vector/fragmentConverters.js.map
|
|
80
82
|
dist/packlets/vector/inMemoryCosineIndex.js
|
|
81
83
|
dist/packlets/vector/inMemoryCosineIndex.js.map
|
|
82
84
|
dist/packlets/vector/inMemoryFragmentCosineIndex.js
|
|
@@ -97,6 +99,8 @@ dist/test/unit/ingest/antagonistCycleAndParity.test.js
|
|
|
97
99
|
dist/test/unit/ingest/antagonistCycleAndParity.test.js.map
|
|
98
100
|
dist/test/unit/ingest/cycleGuard.test.js
|
|
99
101
|
dist/test/unit/ingest/cycleGuard.test.js.map
|
|
102
|
+
dist/test/unit/ingest/dedupScope.test.js
|
|
103
|
+
dist/test/unit/ingest/dedupScope.test.js.map
|
|
100
104
|
dist/test/unit/ingest/orchestrator.test.js
|
|
101
105
|
dist/test/unit/ingest/orchestrator.test.js.map
|
|
102
106
|
dist/test/unit/observe/memoryObservationStore.test.js
|
|
@@ -117,6 +121,8 @@ dist/test/unit/store/fileTreeMemoryStore.test.js
|
|
|
117
121
|
dist/test/unit/store/fileTreeMemoryStore.test.js.map
|
|
118
122
|
dist/test/unit/store/fragmentEmbedOnWrite.test.js
|
|
119
123
|
dist/test/unit/store/fragmentEmbedOnWrite.test.js.map
|
|
124
|
+
dist/test/unit/store/indexInjection.test.js
|
|
125
|
+
dist/test/unit/store/indexInjection.test.js.map
|
|
120
126
|
dist/test/unit/store/lenientOpen.test.js
|
|
121
127
|
dist/test/unit/store/lenientOpen.test.js.map
|
|
122
128
|
dist/test/unit/store/listScoped.test.js
|
|
@@ -141,6 +147,8 @@ dist/test/unit/types/temporalPolicy.test.js
|
|
|
141
147
|
dist/test/unit/types/temporalPolicy.test.js.map
|
|
142
148
|
dist/test/unit/types/writePolicy.test.js
|
|
143
149
|
dist/test/unit/types/writePolicy.test.js.map
|
|
150
|
+
dist/test/unit/vector/fragmentConverters.test.js
|
|
151
|
+
dist/test/unit/vector/fragmentConverters.test.js.map
|
|
144
152
|
dist/test/unit/vector/inMemoryCosineIndex.test.js
|
|
145
153
|
dist/test/unit/vector/inMemoryCosineIndex.test.js.map
|
|
146
154
|
dist/test/unit/vector/inMemoryFragmentCosineIndex.test.js
|
|
@@ -293,6 +301,10 @@ lib/packlets/types/writePolicy.d.ts
|
|
|
293
301
|
lib/packlets/types/writePolicy.d.ts.map
|
|
294
302
|
lib/packlets/types/writePolicy.js
|
|
295
303
|
lib/packlets/types/writePolicy.js.map
|
|
304
|
+
lib/packlets/vector/fragmentConverters.d.ts
|
|
305
|
+
lib/packlets/vector/fragmentConverters.d.ts.map
|
|
306
|
+
lib/packlets/vector/fragmentConverters.js
|
|
307
|
+
lib/packlets/vector/fragmentConverters.js.map
|
|
296
308
|
lib/packlets/vector/inMemoryCosineIndex.d.ts
|
|
297
309
|
lib/packlets/vector/inMemoryCosineIndex.d.ts.map
|
|
298
310
|
lib/packlets/vector/inMemoryCosineIndex.js
|
|
@@ -333,6 +345,10 @@ lib/test/unit/ingest/cycleGuard.test.d.ts
|
|
|
333
345
|
lib/test/unit/ingest/cycleGuard.test.d.ts.map
|
|
334
346
|
lib/test/unit/ingest/cycleGuard.test.js
|
|
335
347
|
lib/test/unit/ingest/cycleGuard.test.js.map
|
|
348
|
+
lib/test/unit/ingest/dedupScope.test.d.ts
|
|
349
|
+
lib/test/unit/ingest/dedupScope.test.d.ts.map
|
|
350
|
+
lib/test/unit/ingest/dedupScope.test.js
|
|
351
|
+
lib/test/unit/ingest/dedupScope.test.js.map
|
|
336
352
|
lib/test/unit/ingest/orchestrator.test.d.ts
|
|
337
353
|
lib/test/unit/ingest/orchestrator.test.d.ts.map
|
|
338
354
|
lib/test/unit/ingest/orchestrator.test.js
|
|
@@ -373,6 +389,10 @@ lib/test/unit/store/fragmentEmbedOnWrite.test.d.ts
|
|
|
373
389
|
lib/test/unit/store/fragmentEmbedOnWrite.test.d.ts.map
|
|
374
390
|
lib/test/unit/store/fragmentEmbedOnWrite.test.js
|
|
375
391
|
lib/test/unit/store/fragmentEmbedOnWrite.test.js.map
|
|
392
|
+
lib/test/unit/store/indexInjection.test.d.ts
|
|
393
|
+
lib/test/unit/store/indexInjection.test.d.ts.map
|
|
394
|
+
lib/test/unit/store/indexInjection.test.js
|
|
395
|
+
lib/test/unit/store/indexInjection.test.js.map
|
|
376
396
|
lib/test/unit/store/lenientOpen.test.d.ts
|
|
377
397
|
lib/test/unit/store/lenientOpen.test.d.ts.map
|
|
378
398
|
lib/test/unit/store/lenientOpen.test.js
|
|
@@ -421,6 +441,10 @@ lib/test/unit/types/writePolicy.test.d.ts
|
|
|
421
441
|
lib/test/unit/types/writePolicy.test.d.ts.map
|
|
422
442
|
lib/test/unit/types/writePolicy.test.js
|
|
423
443
|
lib/test/unit/types/writePolicy.test.js.map
|
|
444
|
+
lib/test/unit/vector/fragmentConverters.test.d.ts
|
|
445
|
+
lib/test/unit/vector/fragmentConverters.test.d.ts.map
|
|
446
|
+
lib/test/unit/vector/fragmentConverters.test.js
|
|
447
|
+
lib/test/unit/vector/fragmentConverters.test.js.map
|
|
424
448
|
lib/test/unit/vector/inMemoryCosineIndex.test.d.ts
|
|
425
449
|
lib/test/unit/vector/inMemoryCosineIndex.test.d.ts.map
|
|
426
450
|
lib/test/unit/vector/inMemoryCosineIndex.test.js
|
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
{"kind":"O","text":"[build:lint] Using ESLint version 9.39.5\n"}
|
|
6
6
|
{"kind":"O","text":"[build:api-extractor] Using API Extractor version 7.58.9\n"}
|
|
7
7
|
{"kind":"O","text":"[build:api-extractor] Analysis will use the bundled TypeScript version 5.9.3\n"}
|
|
8
|
-
{"kind":"O","text":" ---- build finished (
|
|
9
|
-
{"kind":"O","text":"-------------------- Finished (
|
|
8
|
+
{"kind":"O","text":" ---- build finished (26.047s) ---- \n"}
|
|
9
|
+
{"kind":"O","text":"-------------------- Finished (26.053s) --------------------\n"}
|
|
@@ -5,5 +5,5 @@ Invoking: heft build --clean
|
|
|
5
5
|
[build:lint] Using ESLint version 9.39.5
|
|
6
6
|
[build:api-extractor] Using API Extractor version 7.58.9
|
|
7
7
|
[build:api-extractor] Analysis will use the bundled TypeScript version 5.9.3
|
|
8
|
-
---- build finished (
|
|
9
|
-
-------------------- Finished (
|
|
8
|
+
---- build finished (26.047s) ----
|
|
9
|
+
-------------------- Finished (26.053s) --------------------
|
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
{"kind":"O","text":"[build:lint] Using ESLint version 9.39.5\n"}
|
|
6
6
|
{"kind":"O","text":"[build:api-extractor] Using API Extractor version 7.58.9\n"}
|
|
7
7
|
{"kind":"O","text":"[build:api-extractor] Analysis will use the bundled TypeScript version 5.9.3\n"}
|
|
8
|
-
{"kind":"O","text":" ---- build finished (
|
|
9
|
-
{"kind":"O","text":"-------------------- Finished (
|
|
8
|
+
{"kind":"O","text":" ---- build finished (26.047s) ---- \n"}
|
|
9
|
+
{"kind":"O","text":"-------------------- Finished (26.053s) --------------------\n"}
|
package/README.md
CHANGED
|
@@ -6,11 +6,12 @@ edges, content-hash dedup, and the optional-layer seams (vector / temporal /
|
|
|
6
6
|
observe). It owns none of a consumer's processing, transformation, or
|
|
7
7
|
composition logic.
|
|
8
8
|
|
|
9
|
-
> **Status:**
|
|
10
|
-
> knowledge-first.
|
|
11
|
-
> converters
|
|
9
|
+
> **Status:** under active development. The package is being built
|
|
10
|
+
> knowledge-first. The surface listing below covers the foundational type model
|
|
11
|
+
> and converters; the store, index, retrieval, observe, and vector layers ship
|
|
12
|
+
> alongside them and are not itemized here.
|
|
12
13
|
|
|
13
|
-
##
|
|
14
|
+
## Foundational surface
|
|
14
15
|
|
|
15
16
|
### `types` packlet
|
|
16
17
|
|
|
@@ -34,6 +35,135 @@ composition logic.
|
|
|
34
35
|
`joinFrontmatter` / `parseMemoryFile` / `serializeMemoryFile` helpers for
|
|
35
36
|
the `---\n<yaml>\n---\n<body>` memory-file format.
|
|
36
37
|
|
|
38
|
+
## Record updates — the merge contract
|
|
39
|
+
|
|
40
|
+
When a `put` targets an `entityId` that already exists, the write is an
|
|
41
|
+
**update**, not a replace. The store projects the incoming record's mutable
|
|
42
|
+
fields into a patch and hands it to the kind's `IWritePolicy.applyUpdate`, which
|
|
43
|
+
applies it as an **RFC-7386 JSON Merge Patch** over the policy's declared
|
|
44
|
+
mutable surface. This is a **pinned contract**, not an artifact of the current
|
|
45
|
+
merge configuration — the shipped policies compose `@fgv/ts-json`'s `JsonEditor`
|
|
46
|
+
with `{ nullAsDelete: true, arrayMergeBehavior: 'replace' }` specifically to get
|
|
47
|
+
RFC-7386 semantics, and `applyUpdate` documents them as the interface contract.
|
|
48
|
+
|
|
49
|
+
Within the policy's declared mutable surface:
|
|
50
|
+
|
|
51
|
+
- **Objects merge per key.** Keys you supply overwrite; keys you omit are
|
|
52
|
+
**preserved**, not dropped. An update that sets one provenance field leaves
|
|
53
|
+
every sibling field intact.
|
|
54
|
+
- **An explicit `null` on a sub-key clears that sub-key.** This is the
|
|
55
|
+
sanctioned way to remove a single key from a nested object such as
|
|
56
|
+
`provenance`. The cleared key is gone from the persisted record and does not
|
|
57
|
+
reappear on reload.
|
|
58
|
+
- **Arrays replace wholesale.** `tags` and `links` are not element-merged.
|
|
59
|
+
- **A whole-block `null` on a required field is rejected loudly**, never
|
|
60
|
+
silently accepted. `body`, `tags`, `links`, and `provenance` are required; a
|
|
61
|
+
patch that deletes one fails with
|
|
62
|
+
`... merge patch may not delete required field(s): <names>`. (`embeddingRef`
|
|
63
|
+
is optional and *may* be cleared by a `null`, which restores it to absent
|
|
64
|
+
rather than to `null`.)
|
|
65
|
+
- **Keys outside the mutable surface are ignored.** Identity and
|
|
66
|
+
transaction-time envelope fields (`id`, `entityId`, `kind`, `created`,
|
|
67
|
+
`updated`, `seq`, `contentHash`) are preserved verbatim; the store stamps
|
|
68
|
+
`updated` / `seq` / `contentHash` on write.
|
|
69
|
+
|
|
70
|
+
### `provenance` is on the pinned surface
|
|
71
|
+
|
|
72
|
+
`KnowledgeLwwPolicy` and `TemporalVersionedPolicy` both hard-code their mutable
|
|
73
|
+
surface as `['body', 'tags', 'links', 'provenance', 'embeddingRef']`. For those
|
|
74
|
+
policies the guarantees above apply to `provenance` specifically: per-key merge,
|
|
75
|
+
sub-key clearing via `null`, and loud rejection of a whole-block delete are all
|
|
76
|
+
part of the contract and are pinned by tests.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// existing.envelope.provenance: { source: 'agent', confidence: 0.9, note: 'stale' }
|
|
80
|
+
|
|
81
|
+
// clear one sub-key; siblings survive
|
|
82
|
+
policy.applyUpdate(existing, { provenance: { note: null } });
|
|
83
|
+
// => provenance: { source: 'agent', confidence: 0.9 }
|
|
84
|
+
|
|
85
|
+
// revise one sub-key; siblings survive
|
|
86
|
+
policy.applyUpdate(existing, { provenance: { confidence: 0.5 } });
|
|
87
|
+
// => provenance: { source: 'agent', confidence: 0.5, note: 'stale' }
|
|
88
|
+
|
|
89
|
+
// delete the whole block — rejected, not silently accepted
|
|
90
|
+
policy.applyUpdate(existing, { provenance: null });
|
|
91
|
+
// => Result.fail('knowledge LWW: merge patch may not delete required field(s): provenance')
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Going through the store rather than calling a policy directly, the same three
|
|
95
|
+
cases are expressed as the `provenance` value on the record you `put`: include a
|
|
96
|
+
key to set it, set a key to `null` to clear it, and omit a key to leave it
|
|
97
|
+
alone. A whole-block delete is not expressible there at all —
|
|
98
|
+
`IMemoryEnvelope.provenance` is non-nullable and `envelopeConverter` rejects
|
|
99
|
+
`null` — so that case fails at the converter instead of at the policy. Either
|
|
100
|
+
way it fails loudly.
|
|
101
|
+
|
|
102
|
+
### The surface is policy-dependent
|
|
103
|
+
|
|
104
|
+
`MemoryCapCullPolicy` takes its `mutableFields` from the caller, so **what it
|
|
105
|
+
guarantees depends on what you declared**. A field you did not declare mutable
|
|
106
|
+
is not merged at all: patch keys naming it are dropped before the merge runs, so
|
|
107
|
+
a `null` on it neither clears anything nor raises an error — it is simply inert,
|
|
108
|
+
and the existing value is preserved verbatim. If you want the guarantees above
|
|
109
|
+
for `provenance` under a cap-cull policy, `provenance` must appear in the
|
|
110
|
+
`mutableFields` you pass to `MemoryCapCullPolicy.create`.
|
|
111
|
+
|
|
112
|
+
## Dedup granularity — `dedupScope`
|
|
113
|
+
|
|
114
|
+
Each kind's `IWritePolicy` declares the granularity at which a write
|
|
115
|
+
deduplicates against the existing vault:
|
|
116
|
+
|
|
117
|
+
- **`'content'`** — scope-wide, cross-id. An identical body anywhere in the
|
|
118
|
+
scope, even under a different entity, is the same record. The knowledge
|
|
119
|
+
family (`KnowledgeLwwPolicy`) uses this.
|
|
120
|
+
- **`'entity'`** — same-entity only. An identical re-put of one entity is a
|
|
121
|
+
no-op, but two *distinct* entities with coincidentally-identical bodies both
|
|
122
|
+
persist. `MemoryCapCullPolicy` and `TemporalVersionedPolicy` declare this, so
|
|
123
|
+
every experience and versioned kind gets it.
|
|
124
|
+
|
|
125
|
+
Read the effective value for a kind through **`IMemoryStore.dedupScopeFor(kind)`**.
|
|
126
|
+
That accessor is the single owner: it resolves the registered policy, the store's
|
|
127
|
+
default policy, the policy's declaration, and finally `DEFAULT_DEDUP_SCOPE` — and
|
|
128
|
+
the store's own write path reads through it too, so nothing can drift out of
|
|
129
|
+
agreement with it.
|
|
130
|
+
|
|
131
|
+
> **Careful:** `DEFAULT_DEDUP_SCOPE` is `'entity'`, but it is only reached when a
|
|
132
|
+
> policy declares no `dedupScope`. A kind with **no registered policy** falls back
|
|
133
|
+
> to the store's default policy — a `KnowledgeLwwPolicy`, which declares
|
|
134
|
+
> `'content'` explicitly. So an unpoliced kind gets `'content'`, not `'entity'`.
|
|
135
|
+
|
|
136
|
+
### ⚠️ Behavior change: ingest now honors `dedupScope`
|
|
137
|
+
|
|
138
|
+
**This changes results for existing hosts, deliberately, with no opt-in flag.**
|
|
139
|
+
|
|
140
|
+
The ingest orchestrator's stage-4 layer-1 exact match previously ignored
|
|
141
|
+
`dedupScope` entirely and always behaved as `'content'` — the declaration was
|
|
142
|
+
dead code on that path. It now honors the declaration, so:
|
|
143
|
+
|
|
144
|
+
- A kind declaring **`'entity'`** (i.e. anything using `MemoryCapCullPolicy` or
|
|
145
|
+
`TemporalVersionedPolicy`) **no longer collapses two distinct entities that
|
|
146
|
+
share a byte-identical body** during `ingestItem` / `ingestBatch`. Candidates
|
|
147
|
+
that used to come back `deduped` now come back `written`, and the records
|
|
148
|
+
genuinely persist. If your vault silently lost turns whose summaries happened
|
|
149
|
+
to match, it will stop doing that.
|
|
150
|
+
- A kind declaring **`'content'`** is unchanged.
|
|
151
|
+
- A kind with **no registered policy** is unchanged (it resolves to `'content'`,
|
|
152
|
+
as above).
|
|
153
|
+
|
|
154
|
+
This is the intended fix, not a regression: the same declaration already governed
|
|
155
|
+
the direct `put` path, and the two paths disagreeing was the bug. It is not
|
|
156
|
+
flagged, because a flag would preserve the disagreement.
|
|
157
|
+
|
|
158
|
+
Independently of `dedupScope`, a `duplicate-of` collapse no longer orphans
|
|
159
|
+
sibling edges. An edge built against a collapsed candidate is **redirected** to
|
|
160
|
+
the record that candidate collapsed into, instead of failing the entire ingest
|
|
161
|
+
item. This also fixes `'content'` kinds, where the collapse is correct but the
|
|
162
|
+
ingest still should not fail.
|
|
163
|
+
|
|
164
|
+
See `.claude/project/agent-memory-ingest-design.md` §1 and §3 for the full
|
|
165
|
+
treatment.
|
|
166
|
+
|
|
37
167
|
## Conventions
|
|
38
168
|
|
|
39
169
|
`Result<T>` on every fallible operation; no `any`; Converters/Validators for
|
|
@@ -115,7 +115,7 @@ export class MemoryIngestOrchestrator {
|
|
|
115
115
|
}
|
|
116
116
|
// Stage 5: relate over the writable candidates, validate + cycle-guard.
|
|
117
117
|
const writablePlans = plans.filter((plan) => plan.verdict.verdict !== 'duplicate-of');
|
|
118
|
-
const edgesResult = await this._relate(item, writablePlans, scoped, byKey);
|
|
118
|
+
const edgesResult = await this._relate(item, writablePlans, MemoryIngestOrchestrator._collapseRedirects(plans), scoped, byKey);
|
|
119
119
|
if (edgesResult.isFailure()) {
|
|
120
120
|
return fail(edgesResult.message);
|
|
121
121
|
}
|
|
@@ -211,13 +211,15 @@ export class MemoryIngestOrchestrator {
|
|
|
211
211
|
}
|
|
212
212
|
/**
|
|
213
213
|
* Stage 4 — resolve a dedup verdict. Layer 1: an exact `{ kind, body }` match in
|
|
214
|
-
* the candidate's scope is a `duplicate-of
|
|
214
|
+
* the candidate's scope is a `duplicate-of`, at the granularity the kind's
|
|
215
|
+
* {@link DedupScope} declares — scope-wide for `'content'`, restricted to the
|
|
216
|
+
* candidate's own entity for `'entity'` (design note §1). Layer 2 (only when
|
|
215
217
|
* a resolver + vector index + embedder are all wired): embed the candidate,
|
|
216
218
|
* surface over-threshold neighbors, and dispatch to the {@link IEntityResolver}.
|
|
217
219
|
* Otherwise the verdict is `new` (the exact-only fall-back path).
|
|
218
220
|
*/
|
|
219
221
|
async _resolveVerdict(candidate, kind, body, addr, snapshot, byKey) {
|
|
220
|
-
return this._findExactMatch(kind, body, addr
|
|
222
|
+
return this._findExactMatch(kind, body, addr, snapshot).thenOnSuccess(async (matchId) => {
|
|
221
223
|
if (matchId !== undefined) {
|
|
222
224
|
// The exact-match cohort is filtered to `addr.scope`, so the match lives
|
|
223
225
|
// under that scope — its scope-qualified target is `(addr.scope, matchId)`.
|
|
@@ -269,19 +271,44 @@ export class MemoryIngestOrchestrator {
|
|
|
269
271
|
return this._capture(() => wiring.resolver.resolve(candidate, similar), `ingest '${candidate.envelope.entityId}': resolve`);
|
|
270
272
|
}
|
|
271
273
|
/**
|
|
272
|
-
* Find an existing record
|
|
273
|
-
*
|
|
274
|
-
* — only a live (non-temporal or current) record deduplicates a
|
|
274
|
+
* Find an existing record whose `{ kind, body }` hash matches the candidate's
|
|
275
|
+
* (layer-1 exact dedup, design note §1). Invalidated temporal versions are
|
|
276
|
+
* excluded — only a live (non-temporal or current) record deduplicates a
|
|
277
|
+
* candidate.
|
|
278
|
+
*
|
|
279
|
+
* @remarks
|
|
280
|
+
* The cohort's granularity is the kind's DECLARED {@link DedupScope}, read
|
|
281
|
+
* through {@link IMemoryStore.dedupScopeFor} so this layer and the store's own
|
|
282
|
+
* write path can never disagree:
|
|
283
|
+
*
|
|
284
|
+
* - `'content'` — every live same-kind record in the candidate's scope,
|
|
285
|
+
* regardless of entity. A cross-id body collision IS a duplicate (the
|
|
286
|
+
* knowledge family; unchanged behavior).
|
|
287
|
+
* - `'entity'` — only records at the candidate's OWN entity address. Two
|
|
288
|
+
* distinct entities with byte-identical bodies never collapse into one
|
|
289
|
+
* another, which is what the declaration has always meant on the direct-put
|
|
290
|
+
* path and now means here too.
|
|
291
|
+
*
|
|
292
|
+
* The same-id collapse itself remains the store's job — this layer only decides
|
|
293
|
+
* whether a cross-id body collision is eligible to be a `duplicate-of` at all.
|
|
275
294
|
*/
|
|
276
|
-
_findExactMatch(kind, body,
|
|
295
|
+
_findExactMatch(kind, body, addr, snapshot) {
|
|
296
|
+
const dedupScope = this._store.dedupScopeFor(kind);
|
|
277
297
|
// Same-kind, same-scope, LIVE (non-temporal or current) records are the exact
|
|
278
|
-
// cohort
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
298
|
+
// cohort, narrowed to the candidate's own entity address under `'entity'`
|
|
299
|
+
// granularity. `_resolveAddress(...).orDefault()` collapses an unresolved
|
|
300
|
+
// codec to `undefined` — a non-matching address — with no explicit failure
|
|
301
|
+
// branch, exactly as the scope-only filter did before.
|
|
302
|
+
const cohort = snapshot.filter((record) => {
|
|
303
|
+
if (record.envelope.kind !== kind || (isTemporalRecord(record) && !isVersionCurrent(record))) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
const recordAddr = this._resolveAddress(record.envelope.entityId, record.envelope.kind).orDefault();
|
|
307
|
+
if (recordAddr === undefined || recordAddr.scope !== addr.scope) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
return dedupScope === 'content' || recordAddr.idStem === addr.idStem;
|
|
311
|
+
});
|
|
285
312
|
return this._exactKey(kind, body).onSuccess((key) => mapResults(cohort.map((record) => MemoryIngestOrchestrator._recordBodyString(record).onSuccess((recordBody) => this._exactKey(record.envelope.kind, recordBody).onSuccess((recordKey) => succeed({ id: record.envelope.id, key: recordKey }))))).onSuccess((keyed) => { var _a; return succeed((_a = keyed.find((entry) => entry.key === key)) === null || _a === void 0 ? void 0 : _a.id); }));
|
|
286
313
|
}
|
|
287
314
|
/**
|
|
@@ -300,8 +327,8 @@ export class MemoryIngestOrchestrator {
|
|
|
300
327
|
_exactKey(kind, body) {
|
|
301
328
|
return this._hasher.computeHash({ kind, body });
|
|
302
329
|
}
|
|
303
|
-
/** Stage 5 — relate (host), validate edges, and run the write-time cycle guard. */
|
|
304
|
-
async _relate(item, writablePlans, scoped, byKey) {
|
|
330
|
+
/** Stage 5 — relate (host), redirect collapsed targets, validate edges, and run the write-time cycle guard. */
|
|
331
|
+
async _relate(item, writablePlans, redirects, scoped, byKey) {
|
|
305
332
|
const relationCandidates = writablePlans.map((plan) => ({
|
|
306
333
|
candidate: plan.candidate,
|
|
307
334
|
id: plan.refTarget
|
|
@@ -310,21 +337,79 @@ export class MemoryIngestOrchestrator {
|
|
|
310
337
|
if (proposed.isFailure()) {
|
|
311
338
|
return proposed;
|
|
312
339
|
}
|
|
340
|
+
// Redirect BEFORE validation and BEFORE the cycle guard: an edge naming a
|
|
341
|
+
// collapsed candidate must be judged — and persisted — against the record it
|
|
342
|
+
// collapsed into, not against the address that no longer gets written.
|
|
343
|
+
const edges = MemoryIngestOrchestrator._redirectEdges(proposed.value, redirects);
|
|
313
344
|
// refIds and the existing-record view (`byKey`, shared with the verdict path)
|
|
314
345
|
// both key on the canonical scoped address, so a stem reused across scopes
|
|
315
346
|
// never aliases.
|
|
316
347
|
const refIds = new Set(writablePlans.map((plan) => edgeTargetKey(plan.refTarget)));
|
|
317
|
-
const validation = this._validateEdges(item,
|
|
348
|
+
const validation = this._validateEdges(item, edges, refIds, byKey);
|
|
318
349
|
if (validation.isFailure()) {
|
|
319
350
|
return fail(validation.message);
|
|
320
351
|
}
|
|
321
352
|
if (this._cycleGuard === 'reject') {
|
|
322
|
-
const guard = assertNoCycles(MemoryIngestOrchestrator._existingEdges(scoped),
|
|
353
|
+
const guard = assertNoCycles(MemoryIngestOrchestrator._existingEdges(scoped), edges.map((e) => ({ source: e.source, target: e.edge.target, type: e.edge.type })));
|
|
323
354
|
if (guard.isFailure()) {
|
|
324
355
|
return fail(`ingest '${item.id}': ${guard.message}`);
|
|
325
356
|
}
|
|
326
357
|
}
|
|
327
|
-
return succeed(
|
|
358
|
+
return succeed(edges);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* The stage-5 edge-target redirect map for this pass: every `duplicate-of`
|
|
362
|
+
* candidate's OWN scoped address, mapped to the address of the record its
|
|
363
|
+
* verdict collapsed it into (design note §3).
|
|
364
|
+
*
|
|
365
|
+
* @remarks
|
|
366
|
+
* A `duplicate-of` verdict means "this candidate IS that record". The candidate
|
|
367
|
+
* is therefore not written, and its address never becomes a live reference — so
|
|
368
|
+
* a sibling edge built against it in the same pass would resolve to neither a
|
|
369
|
+
* written candidate nor an existing record and would fail the WHOLE ingest item.
|
|
370
|
+
* That is a second-order hazard of collapsing, not a defect in the edge: the
|
|
371
|
+
* host related two candidates it was correctly told about, and one of them
|
|
372
|
+
* turned out to already exist.
|
|
373
|
+
*
|
|
374
|
+
* Redirecting is the honest repair, and it is independent of {@link DedupScope}
|
|
375
|
+
* — it applies equally to a `'content'` kind, where the collapse is exactly
|
|
376
|
+
* right and the ingest still must not fail.
|
|
377
|
+
*
|
|
378
|
+
* The map cannot chain: {@link MemoryIngestOrchestrator._planFromVerdict}
|
|
379
|
+
* requires every target-bearing verdict's target to already exist in the store
|
|
380
|
+
* snapshot, so a redirect destination is always a persisted record and never
|
|
381
|
+
* another collapsed candidate. One pass is sufficient by construction.
|
|
382
|
+
*/
|
|
383
|
+
static _collapseRedirects(plans) {
|
|
384
|
+
const redirects = new Map();
|
|
385
|
+
for (const plan of plans) {
|
|
386
|
+
if (plan.verdict.verdict === 'duplicate-of') {
|
|
387
|
+
redirects.set(edgeTargetKey(plan.refTarget), plan.verdict.target);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return redirects;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Rewrite each edge whose TARGET names a collapsed candidate to point at the
|
|
394
|
+
* record that candidate collapsed into.
|
|
395
|
+
*
|
|
396
|
+
* @remarks
|
|
397
|
+
* Only the target is redirected. An edge whose SOURCE is a collapsed candidate
|
|
398
|
+
* stays untouched and is still rejected by {@link
|
|
399
|
+
* MemoryIngestOrchestrator._validateEdges}: sources are the records an edge is
|
|
400
|
+
* written ONTO, a collapsed candidate is never written, and the relation
|
|
401
|
+
* extractor is only ever offered writable candidates as edge sources in the
|
|
402
|
+
* first place. Silently relocating such an edge onto an existing record would
|
|
403
|
+
* attribute a link the host never asked for — the loud failure is correct there.
|
|
404
|
+
*/
|
|
405
|
+
static _redirectEdges(edges, redirects) {
|
|
406
|
+
if (redirects.size === 0) {
|
|
407
|
+
return edges;
|
|
408
|
+
}
|
|
409
|
+
return edges.map((e) => {
|
|
410
|
+
const to = redirects.get(edgeTargetKey(e.edge.target));
|
|
411
|
+
return to === undefined ? e : Object.assign(Object.assign({}, e), { edge: Object.assign(Object.assign({}, e.edge), { target: to }) });
|
|
412
|
+
});
|
|
328
413
|
}
|
|
329
414
|
/**
|
|
330
415
|
* Validate stage-5 edges: each `source` must be a candidate being written; each
|