@fgv/ts-agent-memory 5.1.0-51 → 5.1.0-53
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/dist/packlets/store/fileTreeMemoryStore.js +24 -100
- package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
- package/dist/packlets/store/storeFileAccess.js +99 -0
- package/dist/packlets/store/storeFileAccess.js.map +1 -0
- package/dist/packlets/store/storeIdentity.js +26 -0
- package/dist/packlets/store/storeIdentity.js.map +1 -1
- package/dist/packlets/vector/inMemoryFragmentCosineIndex.js +0 -19
- package/dist/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
- package/dist/ts-agent-memory.d.ts +0 -36
- package/lib/packlets/store/fileTreeMemoryStore.d.ts +0 -17
- package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
- package/lib/packlets/store/fileTreeMemoryStore.js +23 -99
- package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
- package/lib/packlets/store/storeFileAccess.d.ts +40 -0
- package/lib/packlets/store/storeFileAccess.d.ts.map +1 -0
- package/lib/packlets/store/storeFileAccess.js +105 -0
- package/lib/packlets/store/storeFileAccess.js.map +1 -0
- package/lib/packlets/store/storeIdentity.d.ts +25 -0
- package/lib/packlets/store/storeIdentity.d.ts.map +1 -1
- package/lib/packlets/store/storeIdentity.js +27 -0
- package/lib/packlets/store/storeIdentity.js.map +1 -1
- package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts +0 -19
- package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts.map +1 -1
- package/lib/packlets/vector/inMemoryFragmentCosineIndex.js +0 -19
- package/lib/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
- package/package.json +7 -7
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
* SPDX-License-Identifier: MIT
|
|
4
4
|
*/
|
|
5
5
|
import { Hash, Logging, fail, mapResults, mapSuccess, succeed } from '@fgv/ts-utils';
|
|
6
|
-
import { FileTree } from '@fgv/ts-json-base';
|
|
7
6
|
import { DEFAULT_DEDUP_SCOPE, KnowledgeLwwPolicy, isTemporalIdentityCodec, isTemporalRecord, isVersionCurrent, selectCurrentVersion, selectVersionAsOf } from '../types';
|
|
8
7
|
import { parseMemoryFile, serializeMemoryFile, splitFrontmatter } from '../converters';
|
|
9
8
|
import { VectorMaintenance } from './vectorMaintenance';
|
|
10
9
|
import { computeCoverage } from './storeCoverage';
|
|
11
|
-
import { codecFor, resolveIdentity, verifyLoadedIdentity } from './storeIdentity';
|
|
10
|
+
import { codecFor, resolveIdentity, verifyLoadedIdentity, verifyOccupantKind } from './storeIdentity';
|
|
11
|
+
import { deleteRecordFile, resolveScopeDir, writeRecordFile } from './storeFileAccess';
|
|
12
12
|
import { reconcileVectors } from './storeReconcile';
|
|
13
13
|
import { MemoryIndex } from '../index';
|
|
14
14
|
import { defaultMemoryScopeEncoding } from './scopeEncoding';
|
|
@@ -129,9 +129,9 @@ export class FileTreeMemoryStore {
|
|
|
129
129
|
// version whose `invalid_at` is null/absent) from the entity subtree,
|
|
130
130
|
// read off the derived index. `asOf` resolution is via `list({ asOf })`
|
|
131
131
|
// and the temporal retrievers.
|
|
132
|
-
return this._readVersionedCurrent(addr.scope);
|
|
132
|
+
return this._readVersionedCurrent(addr.scope, kind);
|
|
133
133
|
}
|
|
134
|
-
return this._readRecord(addr.scope, addr.idStem);
|
|
134
|
+
return this._readRecord(addr.scope, addr.idStem, kind);
|
|
135
135
|
}));
|
|
136
136
|
await this._fireObservation('read', kind, entityId, {
|
|
137
137
|
outcome: result.isSuccess() ? 'success' : 'failure',
|
|
@@ -490,7 +490,7 @@ export class FileTreeMemoryStore {
|
|
|
490
490
|
return succeed({ record: duplicate.value, evicted: [] });
|
|
491
491
|
}
|
|
492
492
|
}
|
|
493
|
-
return this._readRecord(scope, idStem).thenOnSuccess((existing) => {
|
|
493
|
+
return this._readRecord(scope, idStem, record.envelope.kind).thenOnSuccess((existing) => {
|
|
494
494
|
// Same-id re-put is a no-op ONLY when the content hash matches AND the
|
|
495
495
|
// mutable metadata is also unchanged. The content hash covers
|
|
496
496
|
// { kind, body, links }; a matching hash with revised tags/provenance is a
|
|
@@ -616,7 +616,7 @@ export class FileTreeMemoryStore {
|
|
|
616
616
|
/** Serialize and write a fully-stamped record, then patch the index. */
|
|
617
617
|
_persist(record, scope, idStem) {
|
|
618
618
|
return serializeMemoryFile(record.envelope, record.body)
|
|
619
|
-
.onSuccess((raw) => this.
|
|
619
|
+
.onSuccess((raw) => writeRecordFile(this._root, this._scopeEncoding, scope, idStem, raw))
|
|
620
620
|
.onSuccess(() => this._index.patch('put', { scope, record }))
|
|
621
621
|
.onSuccess(() => succeed(record));
|
|
622
622
|
}
|
|
@@ -626,9 +626,9 @@ export class FileTreeMemoryStore {
|
|
|
626
626
|
if (!isTemporalIdentityCodec(codec)) {
|
|
627
627
|
return Promise.resolve(fail(`memory delete '${entityId}': codec for versioned kind '${kind}' does not implement the temporal codec interface`));
|
|
628
628
|
}
|
|
629
|
-
return this._deleteVersioned(entityId, addr.scope);
|
|
629
|
+
return this._deleteVersioned(entityId, addr.scope, kind);
|
|
630
630
|
}
|
|
631
|
-
return this._deleteFlat(entityId, addr.scope, addr.idStem);
|
|
631
|
+
return this._deleteFlat(entityId, addr.scope, addr.idStem, kind);
|
|
632
632
|
}));
|
|
633
633
|
}
|
|
634
634
|
/**
|
|
@@ -636,15 +636,15 @@ export class FileTreeMemoryStore {
|
|
|
636
636
|
* entry, then prune the vector best-effort. Structurally unchanged from the
|
|
637
637
|
* pre-temporal delete path.
|
|
638
638
|
*/
|
|
639
|
-
async _deleteFlat(entityId, scope, idStem) {
|
|
640
|
-
return this._readRecord(scope, idStem).thenOnSuccess((existing) => {
|
|
639
|
+
async _deleteFlat(entityId, scope, idStem, kind) {
|
|
640
|
+
return this._readRecord(scope, idStem, kind).thenOnSuccess((existing) => {
|
|
641
641
|
if (existing === undefined) {
|
|
642
642
|
return Promise.resolve(fail(`memory delete '${entityId}': no record found`));
|
|
643
643
|
}
|
|
644
644
|
// Delete the record file + index entry (authoritative), then prune the
|
|
645
645
|
// vector best-effort: a committed delete must not fail because the
|
|
646
646
|
// derived index could not be pruned.
|
|
647
|
-
return this.
|
|
647
|
+
return deleteRecordFile(this._root, this._scopeEncoding, scope, idStem)
|
|
648
648
|
.onSuccess(() => this._index.patch('delete', { scope, record: existing }))
|
|
649
649
|
.thenOnSuccess(async () => {
|
|
650
650
|
await this._vectors.removeAll({ scope, id: existing.envelope.id });
|
|
@@ -658,7 +658,7 @@ export class FileTreeMemoryStore {
|
|
|
658
658
|
* null/absent. `undefined` when the entity has no current version (never
|
|
659
659
|
* written, or fully invalidated / soft-deleted).
|
|
660
660
|
*/
|
|
661
|
-
_readVersionedCurrent(scope) {
|
|
661
|
+
_readVersionedCurrent(scope, expectedKind) {
|
|
662
662
|
// Select over ENVELOPES, then materialize the one winner — which is what
|
|
663
663
|
// `IMemoryStore.get`'s docstring promises and what `selectCurrentVersion`
|
|
664
664
|
// being generic over `IEnvelopeCarrier` exists for. Materializing every
|
|
@@ -668,21 +668,21 @@ export class FileTreeMemoryStore {
|
|
|
668
668
|
if (current === undefined) {
|
|
669
669
|
return succeed(undefined);
|
|
670
670
|
}
|
|
671
|
-
return this._resolveRequired(current);
|
|
671
|
+
return this._resolveRequired(current).onSuccess((materialized) => verifyOccupantKind(expectedKind, scope, materialized.envelope.id, materialized));
|
|
672
672
|
}
|
|
673
673
|
/**
|
|
674
674
|
* Every persisted version of the entity whose subtree is `scope`. All version
|
|
675
675
|
* files for one entity live under exactly that scope (which encodes the
|
|
676
676
|
* entityId), so a scope filter over the index isolates one entity's versions.
|
|
677
677
|
*/
|
|
678
|
-
_versionsForEntity(scope) {
|
|
678
|
+
_versionsForEntity(scope, expectedKind) {
|
|
679
679
|
// Selected on the envelope (scope), materialized after — so a versioned write
|
|
680
680
|
// reads only that entity's versions, never the vault. Bounded by the entity's
|
|
681
681
|
// version count.
|
|
682
682
|
return mapResults(this._index
|
|
683
683
|
.entries()
|
|
684
684
|
.filter((entry) => entry.scope === scope)
|
|
685
|
-
.map((entry) => this._resolveRequired(entry)));
|
|
685
|
+
.map((entry) => this._resolveRequired(entry).onSuccess((r) => verifyOccupantKind(expectedKind, scope, r.envelope.id, r))));
|
|
686
686
|
}
|
|
687
687
|
/**
|
|
688
688
|
* Versioned write (invalidate-don't-delete). Builds the new version's content
|
|
@@ -704,7 +704,7 @@ export class FileTreeMemoryStore {
|
|
|
704
704
|
// still-current version at snapshot time — normally one, but two-or-more if a
|
|
705
705
|
// prior invalidation partially failed; invalidating all of them lets the write
|
|
706
706
|
// self-heal a stuck state (P2-7).
|
|
707
|
-
const snapshot = this._versionsForEntity(scope);
|
|
707
|
+
const snapshot = this._versionsForEntity(scope, kind);
|
|
708
708
|
if (snapshot.isFailure()) {
|
|
709
709
|
return fail(snapshot.message);
|
|
710
710
|
}
|
|
@@ -833,8 +833,8 @@ export class FileTreeMemoryStore {
|
|
|
833
833
|
* kinds exist to preserve the audit trail (and the L3 `contradicts` interlock
|
|
834
834
|
* builds on it), so a hard delete would defeat the purpose.
|
|
835
835
|
*/
|
|
836
|
-
async _deleteVersioned(entityId, scope) {
|
|
837
|
-
const snapshot = this._versionsForEntity(scope);
|
|
836
|
+
async _deleteVersioned(entityId, scope, kind) {
|
|
837
|
+
const snapshot = this._versionsForEntity(scope, kind);
|
|
838
838
|
if (snapshot.isFailure()) {
|
|
839
839
|
return fail(snapshot.message);
|
|
840
840
|
}
|
|
@@ -857,7 +857,7 @@ export class FileTreeMemoryStore {
|
|
|
857
857
|
if (existing === undefined) {
|
|
858
858
|
return fail(`memory put: cannot evict '${id}' in scope '${scope}': not found`);
|
|
859
859
|
}
|
|
860
|
-
return this.
|
|
860
|
+
return deleteRecordFile(this._root, this._scopeEncoding, scope, id)
|
|
861
861
|
.onSuccess(() => this._index.patch('delete', { scope, record: existing }))
|
|
862
862
|
.onSuccess(() => succeed(id));
|
|
863
863
|
});
|
|
@@ -1007,7 +1007,7 @@ export class FileTreeMemoryStore {
|
|
|
1007
1007
|
* already-consistent store touches no files.
|
|
1008
1008
|
*/
|
|
1009
1009
|
_rewriteEnvelope(scope, id, mutate) {
|
|
1010
|
-
return this.
|
|
1010
|
+
return resolveScopeDir(this._root, this._scopeEncoding, scope).onSuccess((scopeDir) => {
|
|
1011
1011
|
/* c8 ignore next 3 - defensive: the scope dir exists for any indexed record */
|
|
1012
1012
|
if (scopeDir === undefined) {
|
|
1013
1013
|
return fail(`'${id}': scope '${scope}' not found`);
|
|
@@ -1102,8 +1102,8 @@ export class FileTreeMemoryStore {
|
|
|
1102
1102
|
* when the scope directory or file is absent. Verifies the on-disk id ↔
|
|
1103
1103
|
* filename round-trip on every load.
|
|
1104
1104
|
*/
|
|
1105
|
-
_readRecord(scope, idStem) {
|
|
1106
|
-
return this.
|
|
1105
|
+
_readRecord(scope, idStem, expectedKind) {
|
|
1106
|
+
return resolveScopeDir(this._root, this._scopeEncoding, scope).onSuccess((scopeDir) => {
|
|
1107
1107
|
if (scopeDir === undefined) {
|
|
1108
1108
|
return succeed(undefined);
|
|
1109
1109
|
}
|
|
@@ -1116,7 +1116,8 @@ export class FileTreeMemoryStore {
|
|
|
1116
1116
|
return file
|
|
1117
1117
|
.getRawContents()
|
|
1118
1118
|
.onSuccess((raw) => parseMemoryFile(raw, this._registry))
|
|
1119
|
-
.onSuccess((parsedRecord) => this._verifyLoaded(scope, file, parsedRecord))
|
|
1119
|
+
.onSuccess((parsedRecord) => this._verifyLoaded(scope, file, parsedRecord))
|
|
1120
|
+
.onSuccess((loaded) => verifyOccupantKind(expectedKind, scope, idStem, loaded));
|
|
1120
1121
|
});
|
|
1121
1122
|
});
|
|
1122
1123
|
}
|
|
@@ -1132,83 +1133,6 @@ export class FileTreeMemoryStore {
|
|
|
1132
1133
|
_verifyLoaded(scope, file, record) {
|
|
1133
1134
|
return verifyLoadedIdentity(this._codec(record.envelope.kind), scope, file, record);
|
|
1134
1135
|
}
|
|
1135
|
-
/**
|
|
1136
|
-
* Resolve the directory for a scope, returning `undefined` when it does not
|
|
1137
|
-
* exist. Navigation only — does not create. Folds the path segments through
|
|
1138
|
-
* `getChildren` so an absent segment short-circuits to `undefined`.
|
|
1139
|
-
*/
|
|
1140
|
-
_resolveScopeDir(scope) {
|
|
1141
|
-
return this._scopeEncoding(scope).onSuccess((encoded) => {
|
|
1142
|
-
const segments = encoded.split('/').filter((s) => s.length > 0);
|
|
1143
|
-
return segments.reduce((acc, segment) => acc.onSuccess((current) => {
|
|
1144
|
-
if (current === undefined) {
|
|
1145
|
-
return succeed(undefined);
|
|
1146
|
-
}
|
|
1147
|
-
return current
|
|
1148
|
-
.getChildren()
|
|
1149
|
-
.onSuccess((children) => succeed(children.find((c) => c.type === 'directory' && c.name === segment)));
|
|
1150
|
-
}), succeed(this._root));
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
/** Ensure the scope directory exists, creating segments as needed. */
|
|
1154
|
-
_ensureScopeDir(scope) {
|
|
1155
|
-
return this._scopeEncoding(scope).onSuccess((encoded) => {
|
|
1156
|
-
const segments = encoded.split('/').filter((s) => s.length > 0);
|
|
1157
|
-
return segments.reduce((acc, segment) => acc.onSuccess((current) => current.getChildren().onSuccess((children) => {
|
|
1158
|
-
const existing = children.find((c) => c.type === 'directory' && c.name === segment);
|
|
1159
|
-
if (existing === undefined) {
|
|
1160
|
-
return current.createChildDirectory(segment);
|
|
1161
|
-
}
|
|
1162
|
-
/* c8 ignore next 3 -- defensive: a child of a mutable in-memory/fs tree is itself mutable; the guard protects against a read-only adapter handed in as root */
|
|
1163
|
-
if (!FileTree.isMutableDirectoryItem(existing)) {
|
|
1164
|
-
return fail(`${existing.absolutePath}: directory is not mutable`);
|
|
1165
|
-
}
|
|
1166
|
-
return succeed(existing);
|
|
1167
|
-
})), succeed(this._root));
|
|
1168
|
-
});
|
|
1169
|
-
}
|
|
1170
|
-
/** Write (create or overwrite) `<scope>/<idStem>.md` with `raw`. */
|
|
1171
|
-
_writeFile(scope, idStem, raw) {
|
|
1172
|
-
return this._ensureScopeDir(scope).onSuccess((scopeDir) => scopeDir.getChildren().onSuccess((children) => {
|
|
1173
|
-
const fileName = `${idStem}${MEMORY_FILE_EXTENSION}`;
|
|
1174
|
-
const existing = children.find((c) => c.type === 'file' && c.name === fileName);
|
|
1175
|
-
if (existing === undefined) {
|
|
1176
|
-
return scopeDir.createChildFile(fileName, raw).onSuccess(() => succeed(true));
|
|
1177
|
-
}
|
|
1178
|
-
/* c8 ignore next 3 -- defensive: a file in a mutable tree is mutable; guards a read-only adapter */
|
|
1179
|
-
if (!FileTree.isMutableFileItem(existing)) {
|
|
1180
|
-
return fail(`${existing.absolutePath}: file is not mutable`);
|
|
1181
|
-
}
|
|
1182
|
-
return existing.setRawContents(raw).onSuccess(() => succeed(true));
|
|
1183
|
-
}));
|
|
1184
|
-
}
|
|
1185
|
-
/**
|
|
1186
|
-
* Physically delete `<scope>/<idStem>.md`. The scope-missing and file-missing
|
|
1187
|
-
* guards are unreachable through the callers (`delete` / `_evict` both read the
|
|
1188
|
-
* record first, so the directory and file exist) but are kept so a future
|
|
1189
|
-
* direct caller degrades loudly rather than silently.
|
|
1190
|
-
*/
|
|
1191
|
-
_deleteFile(scope, idStem) {
|
|
1192
|
-
return this._resolveScopeDir(scope).onSuccess((scopeDir) => {
|
|
1193
|
-
/* c8 ignore next 3 -- unreachable: callers read the record (hence the scope dir) first */
|
|
1194
|
-
if (scopeDir === undefined) {
|
|
1195
|
-
return fail(`memory delete: scope '${scope}' not found`);
|
|
1196
|
-
}
|
|
1197
|
-
const fileName = `${idStem}${MEMORY_FILE_EXTENSION}`;
|
|
1198
|
-
return scopeDir.getChildren().onSuccess((children) => {
|
|
1199
|
-
const file = children.find((c) => c.type === 'file' && c.name === fileName);
|
|
1200
|
-
/* c8 ignore next 3 -- unreachable: callers read the record (hence the file) first */
|
|
1201
|
-
if (file === undefined) {
|
|
1202
|
-
return fail(`memory delete: file '${fileName}' not found in scope '${scope}'`);
|
|
1203
|
-
}
|
|
1204
|
-
/* c8 ignore next 3 -- defensive: a file in a mutable tree is mutable; guards a read-only adapter */
|
|
1205
|
-
if (!FileTree.isMutableFileItem(file)) {
|
|
1206
|
-
return fail(`${file.absolutePath}: file is not mutable`);
|
|
1207
|
-
}
|
|
1208
|
-
return file.delete().onSuccess(() => succeed(true));
|
|
1209
|
-
});
|
|
1210
|
-
});
|
|
1211
|
-
}
|
|
1212
1136
|
/**
|
|
1213
1137
|
* Walk the FileTree once and rebuild the index. Also resumes the `seq`
|
|
1214
1138
|
* counter past the highest persisted `seq` so new writes stay monotonic.
|