@fgv/ts-agent-memory 5.1.0-51 → 5.1.0-52

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 (26) hide show
  1. package/dist/packlets/store/fileTreeMemoryStore.js +24 -100
  2. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  3. package/dist/packlets/store/storeFileAccess.js +99 -0
  4. package/dist/packlets/store/storeFileAccess.js.map +1 -0
  5. package/dist/packlets/store/storeIdentity.js +26 -0
  6. package/dist/packlets/store/storeIdentity.js.map +1 -1
  7. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js +0 -19
  8. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  9. package/dist/ts-agent-memory.d.ts +0 -36
  10. package/lib/packlets/store/fileTreeMemoryStore.d.ts +0 -17
  11. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  12. package/lib/packlets/store/fileTreeMemoryStore.js +23 -99
  13. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  14. package/lib/packlets/store/storeFileAccess.d.ts +40 -0
  15. package/lib/packlets/store/storeFileAccess.d.ts.map +1 -0
  16. package/lib/packlets/store/storeFileAccess.js +105 -0
  17. package/lib/packlets/store/storeFileAccess.js.map +1 -0
  18. package/lib/packlets/store/storeIdentity.d.ts +25 -0
  19. package/lib/packlets/store/storeIdentity.d.ts.map +1 -1
  20. package/lib/packlets/store/storeIdentity.js +27 -0
  21. package/lib/packlets/store/storeIdentity.js.map +1 -1
  22. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts +0 -19
  23. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts.map +1 -1
  24. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js +0 -19
  25. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  26. 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._writeFile(scope, idStem, raw))
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._deleteFile(scope, idStem)
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._deleteFile(scope, id)
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._resolveScopeDir(scope).onSuccess((scopeDir) => {
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._resolveScopeDir(scope).onSuccess((scopeDir) => {
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.