@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.
- package/.rush/temp/{6dfc331ddf74dc2db0d4483d0c96ea971f351c83.tar.log → b82cf6bdece20481260e6bab946179eeec9d7b46.tar.log} +116 -2
- package/.rush/temp/chunked-rush-logs/ts-agent-memory.build.chunks.jsonl +4 -4
- package/.rush/temp/operation/build/all.log +4 -4
- package/.rush/temp/operation/build/log-chunks.jsonl +4 -4
- package/.rush/temp/operation/build/state.json +1 -1
- package/.rush/temp/shrinkwrap-deps.json +222 -221
- package/config/jest.config.json +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/packlets/ingest/cycleGuard.js +111 -0
- package/dist/packlets/ingest/cycleGuard.js.map +1 -0
- package/dist/packlets/ingest/hostStages.js +6 -0
- package/dist/packlets/ingest/hostStages.js.map +1 -0
- package/dist/packlets/ingest/index.js +9 -0
- package/dist/packlets/ingest/index.js.map +1 -0
- package/dist/packlets/ingest/model.js +6 -0
- package/dist/packlets/ingest/model.js.map +1 -0
- package/dist/packlets/ingest/orchestrator.js +438 -0
- package/dist/packlets/ingest/orchestrator.js.map +1 -0
- package/dist/packlets/retrieve/index.js +1 -0
- package/dist/packlets/retrieve/index.js.map +1 -1
- package/dist/packlets/retrieve/temporalRetrievers.js +172 -0
- package/dist/packlets/retrieve/temporalRetrievers.js.map +1 -0
- package/dist/packlets/store/fileTreeMemoryStore.js +323 -41
- package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
- package/dist/packlets/tools/index.js +6 -0
- package/dist/packlets/tools/index.js.map +1 -0
- package/dist/packlets/tools/memoryTools.js +337 -0
- package/dist/packlets/tools/memoryTools.js.map +1 -0
- package/dist/packlets/types/identityCodec.js +115 -0
- package/dist/packlets/types/identityCodec.js.map +1 -1
- package/dist/packlets/types/index.js +1 -0
- package/dist/packlets/types/index.js.map +1 -1
- package/dist/packlets/types/temporal.js +85 -0
- package/dist/packlets/types/temporal.js.map +1 -0
- package/dist/packlets/types/writePolicy.js +96 -0
- package/dist/packlets/types/writePolicy.js.map +1 -1
- package/dist/test/unit/converters/antagonistRoundTrip.test.js +95 -0
- package/dist/test/unit/converters/antagonistRoundTrip.test.js.map +1 -0
- package/dist/test/unit/ingest/antagonistCycleAndParity.test.js +273 -0
- package/dist/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -0
- package/dist/test/unit/ingest/cycleGuard.test.js +54 -0
- package/dist/test/unit/ingest/cycleGuard.test.js.map +1 -0
- package/dist/test/unit/ingest/orchestrator.test.js +913 -0
- package/dist/test/unit/ingest/orchestrator.test.js.map +1 -0
- package/dist/test/unit/retrieve/temporalRetrievers.test.js +182 -0
- package/dist/test/unit/retrieve/temporalRetrievers.test.js.map +1 -0
- package/dist/test/unit/store/antagonistTemporalBoundary.test.js +120 -0
- package/dist/test/unit/store/antagonistTemporalBoundary.test.js.map +1 -0
- package/dist/test/unit/store/fileTreeMemoryStore.test.js +74 -7
- package/dist/test/unit/store/fileTreeMemoryStore.test.js.map +1 -1
- package/dist/test/unit/store/temporalStore.test.js +398 -0
- package/dist/test/unit/store/temporalStore.test.js.map +1 -0
- package/dist/test/unit/tools/memoryTools.test.js +572 -0
- package/dist/test/unit/tools/memoryTools.test.js.map +1 -0
- package/dist/test/unit/types/temporalCodec.test.js +203 -0
- package/dist/test/unit/types/temporalCodec.test.js.map +1 -0
- package/dist/test/unit/types/temporalPolicy.test.js +62 -0
- package/dist/test/unit/types/temporalPolicy.test.js.map +1 -0
- package/dist/ts-agent-memory.d.ts +1042 -10
- package/dist/tsdoc-metadata.json +1 -1
- package/etc/ts-agent-memory.api.md +284 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/packlets/ingest/cycleGuard.d.ts +41 -0
- package/lib/packlets/ingest/cycleGuard.d.ts.map +1 -0
- package/lib/packlets/ingest/cycleGuard.js +115 -0
- package/lib/packlets/ingest/cycleGuard.js.map +1 -0
- package/lib/packlets/ingest/hostStages.d.ts +88 -0
- package/lib/packlets/ingest/hostStages.d.ts.map +1 -0
- package/lib/packlets/ingest/hostStages.js +7 -0
- package/lib/packlets/ingest/hostStages.js.map +1 -0
- package/lib/packlets/ingest/index.d.ts +5 -0
- package/lib/packlets/ingest/index.d.ts.map +1 -0
- package/lib/packlets/ingest/index.js +25 -0
- package/lib/packlets/ingest/index.js.map +1 -0
- package/lib/packlets/ingest/model.d.ts +177 -0
- package/lib/packlets/ingest/model.d.ts.map +1 -0
- package/lib/packlets/ingest/model.js +7 -0
- package/lib/packlets/ingest/model.js.map +1 -0
- package/lib/packlets/ingest/orchestrator.d.ts +206 -0
- package/lib/packlets/ingest/orchestrator.d.ts.map +1 -0
- package/lib/packlets/ingest/orchestrator.js +442 -0
- package/lib/packlets/ingest/orchestrator.js.map +1 -0
- package/lib/packlets/retrieve/index.d.ts +1 -0
- package/lib/packlets/retrieve/index.d.ts.map +1 -1
- package/lib/packlets/retrieve/index.js +1 -0
- package/lib/packlets/retrieve/index.js.map +1 -1
- package/lib/packlets/retrieve/temporalRetrievers.d.ts +78 -0
- package/lib/packlets/retrieve/temporalRetrievers.d.ts.map +1 -0
- package/lib/packlets/retrieve/temporalRetrievers.js +178 -0
- package/lib/packlets/retrieve/temporalRetrievers.js.map +1 -0
- package/lib/packlets/store/fileTreeMemoryStore.d.ts +118 -9
- package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
- package/lib/packlets/store/fileTreeMemoryStore.js +322 -40
- package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
- package/lib/packlets/tools/index.d.ts +2 -0
- package/lib/packlets/tools/index.d.ts.map +1 -0
- package/lib/packlets/tools/index.js +22 -0
- package/lib/packlets/tools/index.js.map +1 -0
- package/lib/packlets/tools/memoryTools.d.ts +139 -0
- package/lib/packlets/tools/memoryTools.d.ts.map +1 -0
- package/lib/packlets/tools/memoryTools.js +341 -0
- package/lib/packlets/tools/memoryTools.js.map +1 -0
- package/lib/packlets/types/identityCodec.d.ts +86 -0
- package/lib/packlets/types/identityCodec.d.ts.map +1 -1
- package/lib/packlets/types/identityCodec.js +118 -1
- package/lib/packlets/types/identityCodec.js.map +1 -1
- package/lib/packlets/types/index.d.ts +1 -0
- package/lib/packlets/types/index.d.ts.map +1 -1
- package/lib/packlets/types/index.js +1 -0
- package/lib/packlets/types/index.js.map +1 -1
- package/lib/packlets/types/temporal.d.ts +40 -0
- package/lib/packlets/types/temporal.d.ts.map +1 -0
- package/lib/packlets/types/temporal.js +92 -0
- package/lib/packlets/types/temporal.js.map +1 -0
- package/lib/packlets/types/writePolicy.d.ts +49 -0
- package/lib/packlets/types/writePolicy.d.ts.map +1 -1
- package/lib/packlets/types/writePolicy.js +98 -1
- package/lib/packlets/types/writePolicy.js.map +1 -1
- package/lib/test/unit/converters/antagonistRoundTrip.test.d.ts +10 -0
- package/lib/test/unit/converters/antagonistRoundTrip.test.d.ts.map +1 -0
- package/lib/test/unit/converters/antagonistRoundTrip.test.js +97 -0
- package/lib/test/unit/converters/antagonistRoundTrip.test.js.map +1 -0
- package/lib/test/unit/ingest/antagonistCycleAndParity.test.d.ts +9 -0
- package/lib/test/unit/ingest/antagonistCycleAndParity.test.d.ts.map +1 -0
- package/lib/test/unit/ingest/antagonistCycleAndParity.test.js +275 -0
- package/lib/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -0
- package/lib/test/unit/ingest/cycleGuard.test.d.ts +2 -0
- package/lib/test/unit/ingest/cycleGuard.test.d.ts.map +1 -0
- package/lib/test/unit/ingest/cycleGuard.test.js +56 -0
- package/lib/test/unit/ingest/cycleGuard.test.js.map +1 -0
- package/lib/test/unit/ingest/orchestrator.test.d.ts +2 -0
- package/lib/test/unit/ingest/orchestrator.test.d.ts.map +1 -0
- package/lib/test/unit/ingest/orchestrator.test.js +915 -0
- package/lib/test/unit/ingest/orchestrator.test.js.map +1 -0
- package/lib/test/unit/retrieve/temporalRetrievers.test.d.ts +2 -0
- package/lib/test/unit/retrieve/temporalRetrievers.test.d.ts.map +1 -0
- package/lib/test/unit/retrieve/temporalRetrievers.test.js +184 -0
- package/lib/test/unit/retrieve/temporalRetrievers.test.js.map +1 -0
- package/lib/test/unit/store/antagonistTemporalBoundary.test.d.ts +9 -0
- package/lib/test/unit/store/antagonistTemporalBoundary.test.d.ts.map +1 -0
- package/lib/test/unit/store/antagonistTemporalBoundary.test.js +122 -0
- package/lib/test/unit/store/antagonistTemporalBoundary.test.js.map +1 -0
- package/lib/test/unit/store/fileTreeMemoryStore.test.js +74 -7
- package/lib/test/unit/store/fileTreeMemoryStore.test.js.map +1 -1
- package/lib/test/unit/store/temporalStore.test.d.ts +2 -0
- package/lib/test/unit/store/temporalStore.test.d.ts.map +1 -0
- package/lib/test/unit/store/temporalStore.test.js +400 -0
- package/lib/test/unit/store/temporalStore.test.js.map +1 -0
- package/lib/test/unit/tools/memoryTools.test.d.ts +2 -0
- package/lib/test/unit/tools/memoryTools.test.d.ts.map +1 -0
- package/lib/test/unit/tools/memoryTools.test.js +574 -0
- package/lib/test/unit/tools/memoryTools.test.js.map +1 -0
- package/lib/test/unit/types/temporalCodec.test.d.ts +2 -0
- package/lib/test/unit/types/temporalCodec.test.d.ts.map +1 -0
- package/lib/test/unit/types/temporalCodec.test.js +205 -0
- package/lib/test/unit/types/temporalCodec.test.js.map +1 -0
- package/lib/test/unit/types/temporalPolicy.test.d.ts +2 -0
- package/lib/test/unit/types/temporalPolicy.test.d.ts.map +1 -0
- package/lib/test/unit/types/temporalPolicy.test.js +64 -0
- package/lib/test/unit/types/temporalPolicy.test.js.map +1 -0
- package/package.json +7 -7
- package/rush-logs/ts-agent-memory.build.cache.log +1 -1
- package/rush-logs/ts-agent-memory.build.log +4 -4
- package/src/index.ts +2 -0
- package/src/packlets/ingest/cycleGuard.ts +142 -0
- package/src/packlets/ingest/hostStages.ts +111 -0
- package/src/packlets/ingest/index.ts +9 -0
- package/src/packlets/ingest/model.ts +184 -0
- package/src/packlets/ingest/orchestrator.ts +797 -0
- package/src/packlets/retrieve/index.ts +1 -0
- package/src/packlets/retrieve/temporalRetrievers.ts +210 -0
- package/src/packlets/store/fileTreeMemoryStore.ts +460 -66
- package/src/packlets/tools/index.ts +6 -0
- package/src/packlets/tools/memoryTools.ts +579 -0
- package/src/packlets/types/identityCodec.ts +184 -0
- package/src/packlets/types/index.ts +1 -0
- package/src/packlets/types/temporal.ts +96 -0
- package/src/packlets/types/writePolicy.ts +127 -0
- package/src/test/unit/converters/antagonistRoundTrip.test.ts +110 -0
- package/src/test/unit/ingest/antagonistCycleAndParity.test.ts +362 -0
- package/src/test/unit/ingest/cycleGuard.test.ts +68 -0
- package/src/test/unit/ingest/orchestrator.test.ts +1158 -0
- package/src/test/unit/retrieve/temporalRetrievers.test.ts +226 -0
- package/src/test/unit/store/antagonistTemporalBoundary.test.ts +158 -0
- package/src/test/unit/store/fileTreeMemoryStore.test.ts +98 -7
- package/src/test/unit/store/temporalStore.test.ts +469 -0
- package/src/test/unit/tools/memoryTools.test.ts +771 -0
- package/src/test/unit/types/temporalCodec.test.ts +259 -0
- package/src/test/unit/types/temporalPolicy.test.ts +96 -0
- package/temp/build/lint/_eslint-5eVG3S6w.json +85 -9
- package/temp/build/typescript/ts_8nwakTlr.json +1 -1
- package/temp/ts-agent-memory.api.json +11984 -6314
- 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
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
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
|
-
.
|
|
285
|
+
.thenOnSuccess((codec) => codec.encode(envelope.entityId).thenOnSuccess((addr) => {
|
|
239
286
|
if (addr.isVersioned) {
|
|
240
|
-
|
|
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).
|
|
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
|
-
|
|
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
|
-
|
|
276
|
-
|
|
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
|
-
|
|
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.
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
//
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
/**
|
|
519
|
-
|
|
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 &&
|
|
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
|
-
/**
|
|
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(() =>
|
|
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
|