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

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 (197) hide show
  1. package/.rush/temp/{6dfc331ddf74dc2db0d4483d0c96ea971f351c83.tar.log → b82cf6bdece20481260e6bab946179eeec9d7b46.tar.log} +116 -2
  2. package/.rush/temp/chunked-rush-logs/ts-agent-memory.build.chunks.jsonl +4 -4
  3. package/.rush/temp/operation/build/all.log +4 -4
  4. package/.rush/temp/operation/build/log-chunks.jsonl +4 -4
  5. package/.rush/temp/operation/build/state.json +1 -1
  6. package/.rush/temp/shrinkwrap-deps.json +222 -221
  7. package/config/jest.config.json +1 -1
  8. package/dist/index.js +2 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/packlets/ingest/cycleGuard.js +111 -0
  11. package/dist/packlets/ingest/cycleGuard.js.map +1 -0
  12. package/dist/packlets/ingest/hostStages.js +6 -0
  13. package/dist/packlets/ingest/hostStages.js.map +1 -0
  14. package/dist/packlets/ingest/index.js +9 -0
  15. package/dist/packlets/ingest/index.js.map +1 -0
  16. package/dist/packlets/ingest/model.js +6 -0
  17. package/dist/packlets/ingest/model.js.map +1 -0
  18. package/dist/packlets/ingest/orchestrator.js +438 -0
  19. package/dist/packlets/ingest/orchestrator.js.map +1 -0
  20. package/dist/packlets/retrieve/index.js +1 -0
  21. package/dist/packlets/retrieve/index.js.map +1 -1
  22. package/dist/packlets/retrieve/temporalRetrievers.js +172 -0
  23. package/dist/packlets/retrieve/temporalRetrievers.js.map +1 -0
  24. package/dist/packlets/store/fileTreeMemoryStore.js +323 -41
  25. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  26. package/dist/packlets/tools/index.js +6 -0
  27. package/dist/packlets/tools/index.js.map +1 -0
  28. package/dist/packlets/tools/memoryTools.js +337 -0
  29. package/dist/packlets/tools/memoryTools.js.map +1 -0
  30. package/dist/packlets/types/identityCodec.js +115 -0
  31. package/dist/packlets/types/identityCodec.js.map +1 -1
  32. package/dist/packlets/types/index.js +1 -0
  33. package/dist/packlets/types/index.js.map +1 -1
  34. package/dist/packlets/types/temporal.js +85 -0
  35. package/dist/packlets/types/temporal.js.map +1 -0
  36. package/dist/packlets/types/writePolicy.js +96 -0
  37. package/dist/packlets/types/writePolicy.js.map +1 -1
  38. package/dist/test/unit/converters/antagonistRoundTrip.test.js +95 -0
  39. package/dist/test/unit/converters/antagonistRoundTrip.test.js.map +1 -0
  40. package/dist/test/unit/ingest/antagonistCycleAndParity.test.js +273 -0
  41. package/dist/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -0
  42. package/dist/test/unit/ingest/cycleGuard.test.js +54 -0
  43. package/dist/test/unit/ingest/cycleGuard.test.js.map +1 -0
  44. package/dist/test/unit/ingest/orchestrator.test.js +913 -0
  45. package/dist/test/unit/ingest/orchestrator.test.js.map +1 -0
  46. package/dist/test/unit/retrieve/temporalRetrievers.test.js +182 -0
  47. package/dist/test/unit/retrieve/temporalRetrievers.test.js.map +1 -0
  48. package/dist/test/unit/store/antagonistTemporalBoundary.test.js +120 -0
  49. package/dist/test/unit/store/antagonistTemporalBoundary.test.js.map +1 -0
  50. package/dist/test/unit/store/fileTreeMemoryStore.test.js +74 -7
  51. package/dist/test/unit/store/fileTreeMemoryStore.test.js.map +1 -1
  52. package/dist/test/unit/store/temporalStore.test.js +398 -0
  53. package/dist/test/unit/store/temporalStore.test.js.map +1 -0
  54. package/dist/test/unit/tools/memoryTools.test.js +572 -0
  55. package/dist/test/unit/tools/memoryTools.test.js.map +1 -0
  56. package/dist/test/unit/types/temporalCodec.test.js +203 -0
  57. package/dist/test/unit/types/temporalCodec.test.js.map +1 -0
  58. package/dist/test/unit/types/temporalPolicy.test.js +62 -0
  59. package/dist/test/unit/types/temporalPolicy.test.js.map +1 -0
  60. package/dist/ts-agent-memory.d.ts +1042 -10
  61. package/dist/tsdoc-metadata.json +1 -1
  62. package/etc/ts-agent-memory.api.md +284 -0
  63. package/lib/index.d.ts +2 -0
  64. package/lib/index.d.ts.map +1 -1
  65. package/lib/index.js +2 -0
  66. package/lib/index.js.map +1 -1
  67. package/lib/packlets/ingest/cycleGuard.d.ts +41 -0
  68. package/lib/packlets/ingest/cycleGuard.d.ts.map +1 -0
  69. package/lib/packlets/ingest/cycleGuard.js +115 -0
  70. package/lib/packlets/ingest/cycleGuard.js.map +1 -0
  71. package/lib/packlets/ingest/hostStages.d.ts +88 -0
  72. package/lib/packlets/ingest/hostStages.d.ts.map +1 -0
  73. package/lib/packlets/ingest/hostStages.js +7 -0
  74. package/lib/packlets/ingest/hostStages.js.map +1 -0
  75. package/lib/packlets/ingest/index.d.ts +5 -0
  76. package/lib/packlets/ingest/index.d.ts.map +1 -0
  77. package/lib/packlets/ingest/index.js +25 -0
  78. package/lib/packlets/ingest/index.js.map +1 -0
  79. package/lib/packlets/ingest/model.d.ts +177 -0
  80. package/lib/packlets/ingest/model.d.ts.map +1 -0
  81. package/lib/packlets/ingest/model.js +7 -0
  82. package/lib/packlets/ingest/model.js.map +1 -0
  83. package/lib/packlets/ingest/orchestrator.d.ts +206 -0
  84. package/lib/packlets/ingest/orchestrator.d.ts.map +1 -0
  85. package/lib/packlets/ingest/orchestrator.js +442 -0
  86. package/lib/packlets/ingest/orchestrator.js.map +1 -0
  87. package/lib/packlets/retrieve/index.d.ts +1 -0
  88. package/lib/packlets/retrieve/index.d.ts.map +1 -1
  89. package/lib/packlets/retrieve/index.js +1 -0
  90. package/lib/packlets/retrieve/index.js.map +1 -1
  91. package/lib/packlets/retrieve/temporalRetrievers.d.ts +78 -0
  92. package/lib/packlets/retrieve/temporalRetrievers.d.ts.map +1 -0
  93. package/lib/packlets/retrieve/temporalRetrievers.js +178 -0
  94. package/lib/packlets/retrieve/temporalRetrievers.js.map +1 -0
  95. package/lib/packlets/store/fileTreeMemoryStore.d.ts +118 -9
  96. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  97. package/lib/packlets/store/fileTreeMemoryStore.js +322 -40
  98. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  99. package/lib/packlets/tools/index.d.ts +2 -0
  100. package/lib/packlets/tools/index.d.ts.map +1 -0
  101. package/lib/packlets/tools/index.js +22 -0
  102. package/lib/packlets/tools/index.js.map +1 -0
  103. package/lib/packlets/tools/memoryTools.d.ts +139 -0
  104. package/lib/packlets/tools/memoryTools.d.ts.map +1 -0
  105. package/lib/packlets/tools/memoryTools.js +341 -0
  106. package/lib/packlets/tools/memoryTools.js.map +1 -0
  107. package/lib/packlets/types/identityCodec.d.ts +86 -0
  108. package/lib/packlets/types/identityCodec.d.ts.map +1 -1
  109. package/lib/packlets/types/identityCodec.js +118 -1
  110. package/lib/packlets/types/identityCodec.js.map +1 -1
  111. package/lib/packlets/types/index.d.ts +1 -0
  112. package/lib/packlets/types/index.d.ts.map +1 -1
  113. package/lib/packlets/types/index.js +1 -0
  114. package/lib/packlets/types/index.js.map +1 -1
  115. package/lib/packlets/types/temporal.d.ts +40 -0
  116. package/lib/packlets/types/temporal.d.ts.map +1 -0
  117. package/lib/packlets/types/temporal.js +92 -0
  118. package/lib/packlets/types/temporal.js.map +1 -0
  119. package/lib/packlets/types/writePolicy.d.ts +49 -0
  120. package/lib/packlets/types/writePolicy.d.ts.map +1 -1
  121. package/lib/packlets/types/writePolicy.js +98 -1
  122. package/lib/packlets/types/writePolicy.js.map +1 -1
  123. package/lib/test/unit/converters/antagonistRoundTrip.test.d.ts +10 -0
  124. package/lib/test/unit/converters/antagonistRoundTrip.test.d.ts.map +1 -0
  125. package/lib/test/unit/converters/antagonistRoundTrip.test.js +97 -0
  126. package/lib/test/unit/converters/antagonistRoundTrip.test.js.map +1 -0
  127. package/lib/test/unit/ingest/antagonistCycleAndParity.test.d.ts +9 -0
  128. package/lib/test/unit/ingest/antagonistCycleAndParity.test.d.ts.map +1 -0
  129. package/lib/test/unit/ingest/antagonistCycleAndParity.test.js +275 -0
  130. package/lib/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -0
  131. package/lib/test/unit/ingest/cycleGuard.test.d.ts +2 -0
  132. package/lib/test/unit/ingest/cycleGuard.test.d.ts.map +1 -0
  133. package/lib/test/unit/ingest/cycleGuard.test.js +56 -0
  134. package/lib/test/unit/ingest/cycleGuard.test.js.map +1 -0
  135. package/lib/test/unit/ingest/orchestrator.test.d.ts +2 -0
  136. package/lib/test/unit/ingest/orchestrator.test.d.ts.map +1 -0
  137. package/lib/test/unit/ingest/orchestrator.test.js +915 -0
  138. package/lib/test/unit/ingest/orchestrator.test.js.map +1 -0
  139. package/lib/test/unit/retrieve/temporalRetrievers.test.d.ts +2 -0
  140. package/lib/test/unit/retrieve/temporalRetrievers.test.d.ts.map +1 -0
  141. package/lib/test/unit/retrieve/temporalRetrievers.test.js +184 -0
  142. package/lib/test/unit/retrieve/temporalRetrievers.test.js.map +1 -0
  143. package/lib/test/unit/store/antagonistTemporalBoundary.test.d.ts +9 -0
  144. package/lib/test/unit/store/antagonistTemporalBoundary.test.d.ts.map +1 -0
  145. package/lib/test/unit/store/antagonistTemporalBoundary.test.js +122 -0
  146. package/lib/test/unit/store/antagonistTemporalBoundary.test.js.map +1 -0
  147. package/lib/test/unit/store/fileTreeMemoryStore.test.js +74 -7
  148. package/lib/test/unit/store/fileTreeMemoryStore.test.js.map +1 -1
  149. package/lib/test/unit/store/temporalStore.test.d.ts +2 -0
  150. package/lib/test/unit/store/temporalStore.test.d.ts.map +1 -0
  151. package/lib/test/unit/store/temporalStore.test.js +400 -0
  152. package/lib/test/unit/store/temporalStore.test.js.map +1 -0
  153. package/lib/test/unit/tools/memoryTools.test.d.ts +2 -0
  154. package/lib/test/unit/tools/memoryTools.test.d.ts.map +1 -0
  155. package/lib/test/unit/tools/memoryTools.test.js +574 -0
  156. package/lib/test/unit/tools/memoryTools.test.js.map +1 -0
  157. package/lib/test/unit/types/temporalCodec.test.d.ts +2 -0
  158. package/lib/test/unit/types/temporalCodec.test.d.ts.map +1 -0
  159. package/lib/test/unit/types/temporalCodec.test.js +205 -0
  160. package/lib/test/unit/types/temporalCodec.test.js.map +1 -0
  161. package/lib/test/unit/types/temporalPolicy.test.d.ts +2 -0
  162. package/lib/test/unit/types/temporalPolicy.test.d.ts.map +1 -0
  163. package/lib/test/unit/types/temporalPolicy.test.js +64 -0
  164. package/lib/test/unit/types/temporalPolicy.test.js.map +1 -0
  165. package/package.json +7 -7
  166. package/rush-logs/ts-agent-memory.build.cache.log +1 -1
  167. package/rush-logs/ts-agent-memory.build.log +4 -4
  168. package/src/index.ts +2 -0
  169. package/src/packlets/ingest/cycleGuard.ts +142 -0
  170. package/src/packlets/ingest/hostStages.ts +111 -0
  171. package/src/packlets/ingest/index.ts +9 -0
  172. package/src/packlets/ingest/model.ts +184 -0
  173. package/src/packlets/ingest/orchestrator.ts +797 -0
  174. package/src/packlets/retrieve/index.ts +1 -0
  175. package/src/packlets/retrieve/temporalRetrievers.ts +210 -0
  176. package/src/packlets/store/fileTreeMemoryStore.ts +460 -66
  177. package/src/packlets/tools/index.ts +6 -0
  178. package/src/packlets/tools/memoryTools.ts +579 -0
  179. package/src/packlets/types/identityCodec.ts +184 -0
  180. package/src/packlets/types/index.ts +1 -0
  181. package/src/packlets/types/temporal.ts +96 -0
  182. package/src/packlets/types/writePolicy.ts +127 -0
  183. package/src/test/unit/converters/antagonistRoundTrip.test.ts +110 -0
  184. package/src/test/unit/ingest/antagonistCycleAndParity.test.ts +362 -0
  185. package/src/test/unit/ingest/cycleGuard.test.ts +68 -0
  186. package/src/test/unit/ingest/orchestrator.test.ts +1158 -0
  187. package/src/test/unit/retrieve/temporalRetrievers.test.ts +226 -0
  188. package/src/test/unit/store/antagonistTemporalBoundary.test.ts +158 -0
  189. package/src/test/unit/store/fileTreeMemoryStore.test.ts +98 -7
  190. package/src/test/unit/store/temporalStore.test.ts +469 -0
  191. package/src/test/unit/tools/memoryTools.test.ts +771 -0
  192. package/src/test/unit/types/temporalCodec.test.ts +259 -0
  193. package/src/test/unit/types/temporalPolicy.test.ts +96 -0
  194. package/temp/build/lint/_eslint-5eVG3S6w.json +85 -9
  195. package/temp/build/typescript/ts_8nwakTlr.json +1 -1
  196. package/temp/ts-agent-memory.api.json +11984 -6314
  197. package/temp/ts-agent-memory.api.md +284 -0
@@ -20,9 +20,11 @@ const MEMORY_FILE_EXTENSION = '.md';
20
20
  * write-lock so the index and the on-disk files never interleave.
21
21
  *
22
22
  * @remarks
23
- * B1 supports flat (non-versioned) layout only and string (markdown) bodies.
24
- * A codec reporting `isVersioned: true`, or a non-string body, fails loudly —
25
- * the versioned/temporal write path is a fast-follow.
23
+ * Bodies are string (markdown). Both layouts are supported: flat
24
+ * (one-file-per-entity, non-versioned) and versioned (subtree-per-entity,
25
+ * invalidate-don't-delete), dispatched per kind by the codec's `isVersioned`
26
+ * flag. A kind is flat with zero behavioral impact unless it opts into a
27
+ * {@link ITemporalIdentityCodec}.
26
28
  * @public
27
29
  */
28
30
  class FileTreeMemoryStore {
@@ -76,7 +78,14 @@ class FileTreeMemoryStore {
76
78
  var _a;
77
79
  const result = this._codecFor(kind).onSuccess((codec) => codec.encode(entityId).onSuccess((addr) => {
78
80
  if (addr.isVersioned) {
79
- return (0, ts_utils_1.fail)(`memory get '${entityId}': versioned/temporal layout not yet supported`);
81
+ if (!(0, types_1.isTemporalIdentityCodec)(codec)) {
82
+ return (0, ts_utils_1.fail)(`memory get '${entityId}': codec for versioned kind '${kind}' does not implement the temporal codec interface`);
83
+ }
84
+ // Versioned keyed read resolves the CURRENT version (highest-seq
85
+ // version whose `invalid_at` is null/absent) from the entity subtree,
86
+ // read off the derived index. `asOf` resolution is via `list({ asOf })`
87
+ // and the temporal retrievers.
88
+ return (0, ts_utils_1.succeed)(this._readVersionedCurrent(addr.scope));
80
89
  }
81
90
  return this._readRecord(addr.scope, addr.idStem);
82
91
  }));
@@ -108,7 +117,45 @@ class FileTreeMemoryStore {
108
117
  return true;
109
118
  })
110
119
  .map((entry) => entry.record);
111
- return (0, ts_utils_1.succeed)(matches);
120
+ if ((filter === null || filter === void 0 ? void 0 : filter.asOf) === undefined) {
121
+ // No temporal projection requested: byte-identical to the pre-temporal
122
+ // behavior (the flat-path guarantee — every version is returned).
123
+ return (0, ts_utils_1.succeed)(matches);
124
+ }
125
+ return (0, ts_utils_1.succeed)(FileTreeMemoryStore._projectAsOf(matches, filter.asOf));
126
+ }
127
+ /**
128
+ * Collapse temporal records to the single version valid at `asOf` per entity;
129
+ * non-temporal records are timeless and pass through unchanged (valid-time
130
+ * `asOf` applies only to versioned kinds; transaction-time / full bi-temporal
131
+ * filtering is deferred — OQ-9). An entity with no version valid at `asOf`
132
+ * contributes nothing.
133
+ */
134
+ static _projectAsOf(records, asOf) {
135
+ const passthrough = [];
136
+ const groups = new Map();
137
+ for (const record of records) {
138
+ if (!(0, types_1.isTemporalRecord)(record)) {
139
+ passthrough.push(record);
140
+ continue;
141
+ }
142
+ const key = `${record.envelope.kind}\0${record.envelope.entityId}`;
143
+ const existing = groups.get(key);
144
+ if (existing === undefined) {
145
+ groups.set(key, [record]);
146
+ }
147
+ else {
148
+ existing.push(record);
149
+ }
150
+ }
151
+ const result = [...passthrough];
152
+ for (const versions of groups.values()) {
153
+ const valid = (0, types_1.selectVersionAsOf)(versions, asOf);
154
+ if (valid !== undefined) {
155
+ result.push(valid);
156
+ }
157
+ }
158
+ return result;
112
159
  }
113
160
  /** {@inheritDoc IMemoryStore.put} */
114
161
  async put(record) {
@@ -227,24 +274,26 @@ class FileTreeMemoryStore {
227
274
  return (0, ts_utils_1.fail)(`memory put '${envelope.id}': only string (markdown) bodies are supported (got ${typeof record.body})`);
228
275
  }
229
276
  const body = record.body;
230
- // Resolve the synchronous prefix (body validation codec address content
231
- // hash) into a small descriptor, then bridge to the async write tail (which
232
- // embeds on write). Keeping the sync prefix intact preserves the exact
233
- // dedup/policy ordering of the original; only the embed step is new.
277
+ // Resolve the body converter + codec, then dispatch on layout. The flat
278
+ // (non-versioned) path keeps its exact dedup/policy ordering; the versioned
279
+ // path is a wholly separate branch so the flat path is behaviorally
280
+ // unchanged (the consumer adoption guarantee).
234
281
  return this._registry
235
282
  .convert(envelope.kind, body)
236
283
  .withErrorFormat((msg) => `memory put '${envelope.id}': invalid body: ${msg}`)
237
284
  .onSuccess(() => this._codecFor(envelope.kind))
238
- .onSuccess((codec) => codec.encode(envelope.entityId).onSuccess((addr) => {
285
+ .thenOnSuccess((codec) => codec.encode(envelope.entityId).thenOnSuccess((addr) => {
239
286
  if (addr.isVersioned) {
240
- return (0, ts_utils_1.fail)(`memory put '${envelope.entityId}': versioned/temporal layout not yet supported`);
287
+ if (!(0, types_1.isTemporalIdentityCodec)(codec)) {
288
+ return Promise.resolve((0, ts_utils_1.fail)(`memory put '${envelope.entityId}': codec for versioned kind '${envelope.kind}' does not implement the temporal codec interface`));
289
+ }
290
+ return this._putVersioned(record, body, codec, addr.scope);
241
291
  }
242
292
  if (envelope.id !== addr.idStem) {
243
- return (0, ts_utils_1.fail)(`memory put: envelope id '${envelope.id}' does not match codec-derived stem '${addr.idStem}'`);
293
+ return Promise.resolve((0, ts_utils_1.fail)(`memory put: envelope id '${envelope.id}' does not match codec-derived stem '${addr.idStem}'`));
244
294
  }
245
- return this._contentHash(envelope.kind, body, envelope.links).onSuccess((hash) => (0, ts_utils_1.succeed)({ scope: addr.scope, idStem: addr.idStem, hash }));
246
- }))
247
- .thenOnSuccess((resolved) => this._writeResolved(record, body, resolved.scope, resolved.idStem, resolved.hash));
295
+ return this._contentHash(envelope.kind, body, envelope.links).thenOnSuccess((hash) => this._writeResolved(record, body, addr.scope, addr.idStem, hash));
296
+ }));
248
297
  }
249
298
  /**
250
299
  * Run dedup → policy → stamp → embed → write for a resolved address and content
@@ -266,14 +315,26 @@ class FileTreeMemoryStore {
266
315
  // (Tags / provenance are metadata and are NOT part of the hash; see
267
316
  // design-lock §2.5.)
268
317
  if (dedupScope === 'content') {
269
- const duplicate = this._findByContentHash(scope, hash);
318
+ // Cross-id content collapse: an identical { kind, body, links } triple under
319
+ // a DIFFERENT id is a no-op (knowledge family). A same-id match is excluded
320
+ // here and falls through to the LWW path below so a metadata-only revision
321
+ // (tags / provenance — outside the content hash but inside the policy's
322
+ // mutableFields) actually applies. Content-dedup must never shadow LWW for
323
+ // the same entity.
324
+ const duplicate = this._findByContentHash(scope, hash, record.envelope.id);
270
325
  if (duplicate !== undefined) {
271
326
  return (0, ts_utils_1.succeed)({ record: duplicate, evicted: [] });
272
327
  }
273
328
  }
274
329
  return this._readRecord(scope, idStem).thenOnSuccess((existing) => {
275
- if (dedupScope === 'entity' && existing !== undefined && existing.envelope.contentHash === hash) {
276
- // Entity-scoped dedup: an identical re-put of the same entity is a no-op.
330
+ // Same-id re-put is a no-op ONLY when the content hash matches AND the
331
+ // mutable metadata is also unchanged. The content hash covers
332
+ // { kind, body, links }; a matching hash with revised tags/provenance is a
333
+ // real update that must reach applyUpdate, not be swallowed as a duplicate
334
+ // (both dedup scopes).
335
+ if (existing !== undefined &&
336
+ existing.envelope.contentHash === hash &&
337
+ this._isMutableMetadataUnchanged(existing, record)) {
277
338
  return Promise.resolve((0, ts_utils_1.succeed)({ record: existing, evicted: [] }));
278
339
  }
279
340
  // The admission cohort is the set of records the policy's cap applies to:
@@ -454,28 +515,210 @@ class FileTreeMemoryStore {
454
515
  .onSuccess(() => (0, ts_utils_1.succeed)(record));
455
516
  }
456
517
  async _deleteLocked(kind, entityId) {
457
- return this._codecFor(kind)
458
- .onSuccess((codec) => codec.encode(entityId))
459
- .thenOnSuccess((addr) => {
518
+ return this._codecFor(kind).thenOnSuccess((codec) => codec.encode(entityId).thenOnSuccess((addr) => {
460
519
  if (addr.isVersioned) {
461
- return Promise.resolve((0, ts_utils_1.fail)(`memory delete '${entityId}': versioned/temporal layout not yet supported`));
520
+ if (!(0, types_1.isTemporalIdentityCodec)(codec)) {
521
+ return Promise.resolve((0, ts_utils_1.fail)(`memory delete '${entityId}': codec for versioned kind '${kind}' does not implement the temporal codec interface`));
522
+ }
523
+ return this._deleteVersioned(entityId, addr.scope);
462
524
  }
463
- return this._readRecord(addr.scope, addr.idStem).thenOnSuccess((existing) => {
464
- if (existing === undefined) {
465
- return Promise.resolve((0, ts_utils_1.fail)(`memory delete '${entityId}': no record found`));
525
+ return this._deleteFlat(entityId, addr.scope, addr.idStem);
526
+ }));
527
+ }
528
+ /**
529
+ * Flat (non-versioned) delete: physically remove the record file + index
530
+ * entry, then prune the vector best-effort. Structurally unchanged from the
531
+ * pre-temporal delete path.
532
+ */
533
+ async _deleteFlat(entityId, scope, idStem) {
534
+ return this._readRecord(scope, idStem).thenOnSuccess((existing) => {
535
+ if (existing === undefined) {
536
+ return Promise.resolve((0, ts_utils_1.fail)(`memory delete '${entityId}': no record found`));
537
+ }
538
+ // Delete the record file + index entry (authoritative), then prune the
539
+ // vector best-effort: a committed delete must not fail because the
540
+ // derived index could not be pruned.
541
+ return this._deleteFile(scope, idStem)
542
+ .onSuccess(() => this._index.patch('delete', { scope, record: existing }))
543
+ .thenOnSuccess(async () => {
544
+ await this._removeVectorBestEffort(existing.envelope.id);
545
+ return (0, ts_utils_1.succeed)(existing.envelope.id);
546
+ });
547
+ });
548
+ }
549
+ /**
550
+ * Resolve the current version of a temporal entity from the derived index: the
551
+ * highest-`seq` version under the entity subtree `scope` whose `invalid_at` is
552
+ * null/absent. `undefined` when the entity has no current version (never
553
+ * written, or fully invalidated / soft-deleted).
554
+ */
555
+ _readVersionedCurrent(scope) {
556
+ return (0, types_1.selectCurrentVersion)(this._versionsForEntity(scope));
557
+ }
558
+ /**
559
+ * Every persisted version of the entity whose subtree is `scope`. All version
560
+ * files for one entity live under exactly that scope (which encodes the
561
+ * entityId), so a scope filter over the index isolates one entity's versions.
562
+ */
563
+ _versionsForEntity(scope) {
564
+ return this._index
565
+ .entries()
566
+ .filter((entry) => entry.scope === scope)
567
+ .map((entry) => entry.record);
568
+ }
569
+ /**
570
+ * Versioned write (invalidate-don't-delete). Builds the new version's content
571
+ * (a first version from the incoming record, or a merge of the incoming patch
572
+ * over the current version), persists it as a NEW version file, then sets
573
+ * `invalid_at` on the prior current version.
574
+ *
575
+ * Durability order: the new version is persisted FIRST. It carries the highest
576
+ * `seq`, so a crash before the prior-version invalidation completes still
577
+ * resolves the new version as current (`selectCurrentVersion` breaks a
578
+ * two-current tie by highest `seq`), and `asOf` reads stay correct because each
579
+ * version's `valid_at` lower-bounds its interval.
580
+ */
581
+ async _putVersioned(record, body, codec, scope) {
582
+ var _a;
583
+ const envelope = record.envelope;
584
+ const entityId = envelope.entityId;
585
+ const kind = envelope.kind;
586
+ // Snapshot the entity's versions BEFORE the write. `priorCurrents` is every
587
+ // still-current version at snapshot time — normally one, but two-or-more if a
588
+ // prior invalidation partially failed; invalidating all of them lets the write
589
+ // self-heal a stuck state (P2-7).
590
+ const versions = this._versionsForEntity(scope);
591
+ const priorCurrents = versions.filter(types_1.isVersionCurrent);
592
+ const current = (0, types_1.selectCurrentVersion)(versions);
593
+ const policy = this._policyFor(kind);
594
+ const dedupScope = (_a = policy.dedupScope) !== null && _a !== void 0 ? _a : types_1.DEFAULT_DEDUP_SCOPE;
595
+ return this._contentHash(kind, body, envelope.links).thenOnSuccess((hash) => {
596
+ // Entity-scoped dedup: a re-put is a no-op only when the CURRENT content AND
597
+ // its mutable metadata are unchanged (does not spawn a redundant version).
598
+ // A metadata-only revision (tags/provenance — declared mutable by
599
+ // TemporalVersionedPolicy) must NOT be swallowed here: it mints a new
600
+ // version via the applyUpdate merge in `_buildVersionedRecord`, mirroring
601
+ // the flat path. Content-scoped dedup is not a versioning concern, so only
602
+ // the entity granularity is honored here.
603
+ if (dedupScope === 'entity' &&
604
+ current !== undefined &&
605
+ current.envelope.contentHash === hash &&
606
+ this._isMutableMetadataUnchanged(current, record)) {
607
+ return Promise.resolve((0, ts_utils_1.succeed)({ record: current, evicted: [] }));
608
+ }
609
+ // On the versioned path the admission cohort is the entity's ENTIRE version
610
+ // history (no target id to exclude — the new version does not exist yet),
611
+ // which differs from the flat path's "cohort excluding target id" shape.
612
+ return policy.admit(record, versions).thenOnSuccess((decision) => {
613
+ var _a, _b;
614
+ if (decision.decision === 'reject') {
615
+ return Promise.resolve((0, ts_utils_1.fail)(`memory put: rejected by policy: ${decision.reason}`));
466
616
  }
467
- // Delete the record file + index entry (authoritative), then prune the
468
- // vector best-effort: a committed delete must not fail because the
469
- // derived index could not be pruned.
470
- return this._deleteFile(addr.scope, addr.idStem)
471
- .onSuccess(() => this._index.patch('delete', { scope: addr.scope, record: existing }))
472
- .thenOnSuccess(async () => {
473
- await this._removeVectorBestEffort(existing.envelope.id);
474
- return (0, ts_utils_1.succeed)(existing.envelope.id);
475
- });
617
+ // No culling on the versioned path history is retained (invalidate-don't-delete).
618
+ const now = this._clock();
619
+ const seq = ++this._seq;
620
+ // World-truth start of the new version. Each prior current version's
621
+ // world-truth interval CLOSES at this same instant (not at `now`) so a
622
+ // backdated/future-dated `valid_at` leaves no gap or overlap on the
623
+ // valid-time axis.
624
+ const validAt = (_b = (_a = envelope.temporal) === null || _a === void 0 ? void 0 : _a.valid_at) !== null && _b !== void 0 ? _b : now;
625
+ return codec
626
+ .encodeVersion(entityId, seq)
627
+ .withErrorFormat((msg) => `memory put '${entityId}': ${msg}`)
628
+ .thenOnSuccess((versionStem) => this._buildVersionedRecord(record, body, current, policy, hash, versionStem, validAt, now, seq)
629
+ .thenOnSuccess((built) => this._embedOnWrite(built))
630
+ .onSuccess((embeddedBuilt) => this._persist(embeddedBuilt, scope, versionStem))
631
+ .onSuccess((persisted) => this._invalidateCurrents(scope, priorCurrents, validAt, now).onSuccess(() => (0, ts_utils_1.succeed)(persisted)))
632
+ .onSuccess((persisted) => (0, ts_utils_1.succeed)({ record: persisted, evicted: [] })));
633
+ });
634
+ });
635
+ }
636
+ /**
637
+ * Invalidate a set of still-current versions (close each world-truth interval at
638
+ * `invalidAt`; stamp transaction-time `updated` = `now`). Chained so a mid-list
639
+ * failure propagates. Invalidating every prior current — not just the
640
+ * highest-`seq` pick — self-heals a state where a previous write's invalidation
641
+ * only partially completed (P2-7).
642
+ */
643
+ _invalidateCurrents(scope, currents, invalidAt, now) {
644
+ return currents.reduce((acc, version) => acc.onSuccess(() => this._invalidateVersion(scope, version, invalidAt, now).onSuccess(() => (0, ts_utils_1.succeed)(true))), (0, ts_utils_1.succeed)(true));
645
+ }
646
+ /**
647
+ * Build the new version to persist. A first version takes the incoming
648
+ * content verbatim (its dedup `hash` is reused); a subsequent version projects
649
+ * the incoming record's mutable fields into a merge-patch and lets the policy
650
+ * merge them over the CURRENT version (the merge-patch-under-versioning
651
+ * contract), recomputing the content hash from the policy's output. Each
652
+ * version is its own record with its own transaction time (`created` = `now`)
653
+ * and a store-minted `id` = the version stem, so `id === filename stem` holds.
654
+ */
655
+ _buildVersionedRecord(incoming, body, current, policy,
656
+ // Reused only for the first-version branch (its content is taken verbatim); a
657
+ // subsequent version recomputes its hash from the merge-patched content.
658
+ firstVersionHash, versionStem, validAt, now, seq) {
659
+ const mintedId = versionStem;
660
+ if (current === undefined) {
661
+ const envelope = Object.assign(Object.assign({}, incoming.envelope), { id: mintedId, entityId: incoming.envelope.entityId, created: now, updated: now, seq, contentHash: firstVersionHash, temporal: { valid_at: validAt } });
662
+ return (0, ts_utils_1.succeed)({ envelope, body });
663
+ }
664
+ const patch = this._projectMutablePatch(incoming, policy.mutableFields);
665
+ return policy
666
+ .applyUpdate(current, patch)
667
+ .withErrorFormat((msg) => `memory put '${incoming.envelope.entityId}': update failed: ${msg}`)
668
+ .onSuccess((updated) => {
669
+ if (typeof updated.body !== 'string') {
670
+ return (0, ts_utils_1.fail)(`memory put '${incoming.envelope.entityId}': policy returned a non-string body (${typeof updated.body})`);
671
+ }
672
+ const finalBody = updated.body;
673
+ return this._contentHash(updated.envelope.kind, finalBody, updated.envelope.links).onSuccess((finalHash) => {
674
+ const envelope = Object.assign(Object.assign({}, updated.envelope), { id: mintedId, entityId: incoming.envelope.entityId, created: now, updated: now, seq, contentHash: finalHash, temporal: { valid_at: validAt } });
675
+ return (0, ts_utils_1.succeed)({ envelope, body: finalBody });
476
676
  });
477
677
  });
478
678
  }
679
+ /**
680
+ * Set `invalid_at` on a prior current version (invalidate-don't-delete) and
681
+ * rewrite its file + index entry. The content hash is unchanged — `invalid_at`
682
+ * is temporal metadata, not part of `{ kind, body, links }` — so the version's
683
+ * identity is stable.
684
+ */
685
+ _invalidateVersion(scope, version, invalidAt, now) {
686
+ /* c8 ignore start -- defensive: every persisted version carries a string body (only string bodies are written) */
687
+ if (typeof version.body !== 'string') {
688
+ return (0, ts_utils_1.fail)(`memory put: cannot invalidate version '${version.envelope.id}': non-string body`);
689
+ }
690
+ /* c8 ignore stop */
691
+ // `invalid_at` is the WORLD-TRUTH close of the interval (the superseding version's
692
+ // `valid_at`, or the delete instant); `updated` is the transaction-time stamp.
693
+ const invalidated = {
694
+ envelope: Object.assign(Object.assign({}, version.envelope), { updated: now, temporal: Object.assign(Object.assign({}, version.envelope.temporal), { invalid_at: invalidAt }) }),
695
+ body: version.body
696
+ };
697
+ return this._persist(invalidated, scope, version.envelope.id);
698
+ }
699
+ /**
700
+ * Versioned delete: SOFT delete (invalidate-don't-delete). Sets `invalid_at` on
701
+ * the current version, leaving the entity's history intact and the entity with
702
+ * no current version. Returns the invalidated version's {@link MemoryId}. Fails
703
+ * with "no record found" when there is no current version — matching the flat
704
+ * delete's not-found semantics. History is retained deliberately: temporal
705
+ * kinds exist to preserve the audit trail (and the L3 `contradicts` interlock
706
+ * builds on it), so a hard delete would defeat the purpose.
707
+ */
708
+ async _deleteVersioned(entityId, scope) {
709
+ const versions = this._versionsForEntity(scope);
710
+ const currents = versions.filter(types_1.isVersionCurrent);
711
+ const current = (0, types_1.selectCurrentVersion)(versions);
712
+ if (current === undefined) {
713
+ return (0, ts_utils_1.fail)(`memory delete '${entityId}': no record found`);
714
+ }
715
+ // A delete closes the world-truth interval at the delete instant (`now` for both
716
+ // the transaction stamp and the `invalid_at` boundary). Every still-current
717
+ // version is invalidated (self-heals a stuck two-current state — P2-7); the
718
+ // highest-`seq` current's id is returned.
719
+ const now = this._clock();
720
+ return this._invalidateCurrents(scope, currents, now, now).onSuccess(() => (0, ts_utils_1.succeed)(current.envelope.id));
721
+ }
479
722
  /** Evict (physically delete) a single record file by id, patching the index. */
480
723
  _evict(scope, id) {
481
724
  return this._readRecord(scope, id).onSuccess((existing) => {
@@ -515,13 +758,39 @@ class FileTreeMemoryStore {
515
758
  .filter((entry) => entry.scope === scope && entry.record.envelope.kind === kind && entry.record.envelope.id !== idStem)
516
759
  .map((entry) => entry.record);
517
760
  }
518
- /** Find a record in `scope` whose `contentHash` equals `hash`, if any. */
519
- _findByContentHash(scope, hash) {
761
+ /**
762
+ * Find a record in `scope` whose `contentHash` equals `hash`, if any,
763
+ * optionally excluding a specific id. `excludeId` lets the content-scoped
764
+ * dedup skip the same-id record so it does not shadow the LWW update path.
765
+ */
766
+ _findByContentHash(scope, hash, excludeId) {
520
767
  const match = this._index
521
768
  .entries()
522
- .find((entry) => entry.scope === scope && entry.record.envelope.contentHash === hash);
769
+ .find((entry) => entry.scope === scope &&
770
+ entry.record.envelope.contentHash === hash &&
771
+ entry.record.envelope.id !== excludeId);
523
772
  return match === null || match === void 0 ? void 0 : match.record;
524
773
  }
774
+ /**
775
+ * True when `incoming`'s caller-authored mutable metadata (`tags` / `provenance`)
776
+ * canonically equals `existing`'s. `body` and `links` are covered by the content
777
+ * hash; `embeddingRef` is store-derived (not caller metadata) and is deliberately
778
+ * excluded so a same-content re-put is not treated as changed merely because the
779
+ * store already stamped an embedding. Used to keep an identical re-put a no-op
780
+ * without swallowing a genuine metadata revision.
781
+ *
782
+ * Canonicalization never fails for a validated record (`tags`/`provenance` are
783
+ * always plain JSON); a failure is defaulted to a non-matching sentinel so the
784
+ * write flows to `applyUpdate` (which re-validates) rather than silently
785
+ * no-op-ing on an un-canonicalizable value.
786
+ */
787
+ _isMutableMetadataUnchanged(existing, incoming) {
788
+ const key = (record) => this._hasher
789
+ .canonicalize({ tags: record.envelope.tags, provenance: record.envelope.provenance })
790
+ .orDefault('');
791
+ const existingKey = key(existing);
792
+ return existingKey !== '' && existingKey === key(incoming);
793
+ }
525
794
  _contentHash(kind, body, links) {
526
795
  return this._hasher.computeHash({ kind, body, links });
527
796
  }
@@ -560,15 +829,28 @@ class FileTreeMemoryStore {
560
829
  });
561
830
  });
562
831
  }
563
- /** Enforce `envelope.id === filename stem` and the codec round-trip on load. */
832
+ /**
833
+ * Enforce `envelope.id === filename stem`, the codec round-trip, AND that the
834
+ * envelope's own `entityId` agrees with the id decoded from the subtree scope.
835
+ * The round-trip only validates (scope, stem) consistency; the codec derives
836
+ * `entityId` from the scope path and never reads the envelope's `entityId`
837
+ * field, so a tampered/corrupt file whose frontmatter declares a foreign
838
+ * `entityId` would otherwise load undetected — and `entityId` is trusted
839
+ * verbatim downstream (e.g. merge-into re-addressing). Cross-check it here.
840
+ */
564
841
  _verifyLoaded(scope, file, record) {
565
842
  if (record.envelope.id !== file.baseName) {
566
843
  return (0, ts_utils_1.fail)(`memory file '${file.absolutePath}': envelope id '${record.envelope.id}' does not match filename stem '${file.baseName}'`);
567
844
  }
568
845
  return this._codecFor(record.envelope.kind)
569
- .onSuccess((codec) => codec.verifyRoundTrip(scope, file.baseName))
846
+ .onSuccess((codec) => codec.verifyRoundTrip(scope, file.baseName).onSuccess(() => codec.decode(scope, file.baseName)))
570
847
  .withErrorFormat((msg) => `memory file '${file.absolutePath}': ${msg}`)
571
- .onSuccess(() => (0, ts_utils_1.succeed)(record));
848
+ .onSuccess((decodedEntityId) => {
849
+ if (decodedEntityId !== record.envelope.entityId) {
850
+ return (0, ts_utils_1.fail)(`memory file '${file.absolutePath}': envelope entityId '${record.envelope.entityId}' does not match scope-derived entityId '${decodedEntityId}'`);
851
+ }
852
+ return (0, ts_utils_1.succeed)(record);
853
+ });
572
854
  }
573
855
  /**
574
856
  * Resolve the directory for a scope, returning `undefined` when it does not