@fgv/ts-agent-memory 5.1.0-36
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/chunked-rush-logs/ts-agent-memory.build.chunks.jsonl +9 -0
- package/.rush/temp/f6a88bfdd66517ccb98c2c7ae1be6e6fe9e15d38.tar.log +282 -0
- package/.rush/temp/operation/build/all.log +9 -0
- package/.rush/temp/operation/build/log-chunks.jsonl +9 -0
- package/.rush/temp/operation/build/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +688 -0
- package/LICENSE +21 -0
- package/README.md +45 -0
- package/config/api-extractor.json +343 -0
- package/config/jest.config.json +14 -0
- package/config/rig.json +4 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/packlets/converters/bodyConverterRegistry.js +51 -0
- package/dist/packlets/converters/bodyConverterRegistry.js.map +1 -0
- package/dist/packlets/converters/envelopeConverter.js +159 -0
- package/dist/packlets/converters/envelopeConverter.js.map +1 -0
- package/dist/packlets/converters/index.js +7 -0
- package/dist/packlets/converters/index.js.map +1 -0
- package/dist/packlets/index/index.js +6 -0
- package/dist/packlets/index/index.js.map +1 -0
- package/dist/packlets/index/memoryIndex.js +170 -0
- package/dist/packlets/index/memoryIndex.js.map +1 -0
- package/dist/packlets/observe/index.js +7 -0
- package/dist/packlets/observe/index.js.map +1 -0
- package/dist/packlets/observe/memoryObservationStore.js +119 -0
- package/dist/packlets/observe/memoryObservationStore.js.map +1 -0
- package/dist/packlets/observe/observer.js +6 -0
- package/dist/packlets/observe/observer.js.map +1 -0
- package/dist/packlets/retrieve/hybridRetriever.js +135 -0
- package/dist/packlets/retrieve/hybridRetriever.js.map +1 -0
- package/dist/packlets/retrieve/index.js +12 -0
- package/dist/packlets/retrieve/index.js.map +1 -0
- package/dist/packlets/retrieve/linkTraversalRetriever.js +143 -0
- package/dist/packlets/retrieve/linkTraversalRetriever.js.map +1 -0
- package/dist/packlets/retrieve/recencyRetriever.js +33 -0
- package/dist/packlets/retrieve/recencyRetriever.js.map +1 -0
- package/dist/packlets/retrieve/retriever.js +110 -0
- package/dist/packlets/retrieve/retriever.js.map +1 -0
- package/dist/packlets/retrieve/semanticRetriever.js +86 -0
- package/dist/packlets/retrieve/semanticRetriever.js.map +1 -0
- package/dist/packlets/retrieve/structuredFilterRetriever.js +37 -0
- package/dist/packlets/retrieve/structuredFilterRetriever.js.map +1 -0
- package/dist/packlets/retrieve/tagRetriever.js +37 -0
- package/dist/packlets/retrieve/tagRetriever.js.map +1 -0
- package/dist/packlets/store/fileTreeMemoryStore.js +698 -0
- package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -0
- package/dist/packlets/store/index.js +7 -0
- package/dist/packlets/store/index.js.map +1 -0
- package/dist/packlets/store/scopeEncoding.js +31 -0
- package/dist/packlets/store/scopeEncoding.js.map +1 -0
- package/dist/packlets/types/envelope.js +6 -0
- package/dist/packlets/types/envelope.js.map +1 -0
- package/dist/packlets/types/filenameSafety.js +52 -0
- package/dist/packlets/types/filenameSafety.js.map +1 -0
- package/dist/packlets/types/identityCodec.js +184 -0
- package/dist/packlets/types/identityCodec.js.map +1 -0
- package/dist/packlets/types/ids.js +67 -0
- package/dist/packlets/types/ids.js.map +1 -0
- package/dist/packlets/types/index.js +10 -0
- package/dist/packlets/types/index.js.map +1 -0
- package/dist/packlets/types/writePolicy.js +263 -0
- package/dist/packlets/types/writePolicy.js.map +1 -0
- package/dist/packlets/vector/inMemoryCosineIndex.js +150 -0
- package/dist/packlets/vector/inMemoryCosineIndex.js.map +1 -0
- package/dist/packlets/vector/index.js +7 -0
- package/dist/packlets/vector/index.js.map +1 -0
- package/dist/packlets/vector/vectorIndex.js +6 -0
- package/dist/packlets/vector/vectorIndex.js.map +1 -0
- package/dist/test/unit/converters/bodyConverterRegistry.test.js +72 -0
- package/dist/test/unit/converters/bodyConverterRegistry.test.js.map +1 -0
- package/dist/test/unit/converters/envelopeConverter.test.js +196 -0
- package/dist/test/unit/converters/envelopeConverter.test.js.map +1 -0
- package/dist/test/unit/index/memoryIndex.test.js +152 -0
- package/dist/test/unit/index/memoryIndex.test.js.map +1 -0
- package/dist/test/unit/observe/memoryObservationStore.test.js +118 -0
- package/dist/test/unit/observe/memoryObservationStore.test.js.map +1 -0
- package/dist/test/unit/retrieve/linkTraversalRetriever.test.js +182 -0
- package/dist/test/unit/retrieve/linkTraversalRetriever.test.js.map +1 -0
- package/dist/test/unit/retrieve/retrievers.test.js +506 -0
- package/dist/test/unit/retrieve/retrievers.test.js.map +1 -0
- package/dist/test/unit/store/embedOnWrite.test.js +260 -0
- package/dist/test/unit/store/embedOnWrite.test.js.map +1 -0
- package/dist/test/unit/store/fileTreeMemoryStore.test.js +647 -0
- package/dist/test/unit/store/fileTreeMemoryStore.test.js.map +1 -0
- package/dist/test/unit/store/observations.test.js +239 -0
- package/dist/test/unit/store/observations.test.js.map +1 -0
- package/dist/test/unit/store/scopeEncoding.test.js +24 -0
- package/dist/test/unit/store/scopeEncoding.test.js.map +1 -0
- package/dist/test/unit/types/identityCodec.test.js +187 -0
- package/dist/test/unit/types/identityCodec.test.js.map +1 -0
- package/dist/test/unit/types/ids.test.js +84 -0
- package/dist/test/unit/types/ids.test.js.map +1 -0
- package/dist/test/unit/types/writePolicy.test.js +241 -0
- package/dist/test/unit/types/writePolicy.test.js.map +1 -0
- package/dist/test/unit/vector/inMemoryCosineIndex.test.js +192 -0
- package/dist/test/unit/vector/inMemoryCosineIndex.test.js.map +1 -0
- package/dist/test/unit/vector/vectorIndex.test.js +42 -0
- package/dist/test/unit/vector/vectorIndex.test.js.map +1 -0
- package/dist/ts-agent-memory.d.ts +1901 -0
- package/dist/tsdoc-metadata.json +11 -0
- package/eslint.config.js +15 -0
- package/etc/ts-agent-memory.api.md +525 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +28 -0
- package/lib/index.js.map +1 -0
- package/lib/packlets/converters/bodyConverterRegistry.d.ts +64 -0
- package/lib/packlets/converters/bodyConverterRegistry.d.ts.map +1 -0
- package/lib/packlets/converters/bodyConverterRegistry.js +55 -0
- package/lib/packlets/converters/bodyConverterRegistry.js.map +1 -0
- package/lib/packlets/converters/envelopeConverter.d.ts +70 -0
- package/lib/packlets/converters/envelopeConverter.d.ts.map +1 -0
- package/lib/packlets/converters/envelopeConverter.js +166 -0
- package/lib/packlets/converters/envelopeConverter.js.map +1 -0
- package/lib/packlets/converters/index.d.ts +3 -0
- package/lib/packlets/converters/index.d.ts.map +1 -0
- package/lib/packlets/converters/index.js +23 -0
- package/lib/packlets/converters/index.js.map +1 -0
- package/lib/packlets/index/index.d.ts +2 -0
- package/lib/packlets/index/index.d.ts.map +1 -0
- package/lib/packlets/index/index.js +22 -0
- package/lib/packlets/index/index.js.map +1 -0
- package/lib/packlets/index/memoryIndex.d.ts +127 -0
- package/lib/packlets/index/memoryIndex.d.ts.map +1 -0
- package/lib/packlets/index/memoryIndex.js +174 -0
- package/lib/packlets/index/memoryIndex.js.map +1 -0
- package/lib/packlets/observe/index.d.ts +3 -0
- package/lib/packlets/observe/index.d.ts.map +1 -0
- package/lib/packlets/observe/index.js +23 -0
- package/lib/packlets/observe/index.js.map +1 -0
- package/lib/packlets/observe/memoryObservationStore.d.ts +91 -0
- package/lib/packlets/observe/memoryObservationStore.d.ts.map +1 -0
- package/lib/packlets/observe/memoryObservationStore.js +123 -0
- package/lib/packlets/observe/memoryObservationStore.js.map +1 -0
- package/lib/packlets/observe/observer.d.ts +110 -0
- package/lib/packlets/observe/observer.d.ts.map +1 -0
- package/lib/packlets/observe/observer.js +7 -0
- package/lib/packlets/observe/observer.js.map +1 -0
- package/lib/packlets/retrieve/hybridRetriever.d.ts +79 -0
- package/lib/packlets/retrieve/hybridRetriever.d.ts.map +1 -0
- package/lib/packlets/retrieve/hybridRetriever.js +140 -0
- package/lib/packlets/retrieve/hybridRetriever.js.map +1 -0
- package/lib/packlets/retrieve/index.d.ts +8 -0
- package/lib/packlets/retrieve/index.d.ts.map +1 -0
- package/lib/packlets/retrieve/index.js +28 -0
- package/lib/packlets/retrieve/index.js.map +1 -0
- package/lib/packlets/retrieve/linkTraversalRetriever.d.ts +61 -0
- package/lib/packlets/retrieve/linkTraversalRetriever.d.ts.map +1 -0
- package/lib/packlets/retrieve/linkTraversalRetriever.js +147 -0
- package/lib/packlets/retrieve/linkTraversalRetriever.js.map +1 -0
- package/lib/packlets/retrieve/recencyRetriever.d.ts +21 -0
- package/lib/packlets/retrieve/recencyRetriever.d.ts.map +1 -0
- package/lib/packlets/retrieve/recencyRetriever.js +37 -0
- package/lib/packlets/retrieve/recencyRetriever.js.map +1 -0
- package/lib/packlets/retrieve/retriever.d.ts +133 -0
- package/lib/packlets/retrieve/retriever.d.ts.map +1 -0
- package/lib/packlets/retrieve/retriever.js +119 -0
- package/lib/packlets/retrieve/retriever.js.map +1 -0
- package/lib/packlets/retrieve/semanticRetriever.d.ts +69 -0
- package/lib/packlets/retrieve/semanticRetriever.d.ts.map +1 -0
- package/lib/packlets/retrieve/semanticRetriever.js +90 -0
- package/lib/packlets/retrieve/semanticRetriever.js.map +1 -0
- package/lib/packlets/retrieve/structuredFilterRetriever.d.ts +22 -0
- package/lib/packlets/retrieve/structuredFilterRetriever.d.ts.map +1 -0
- package/lib/packlets/retrieve/structuredFilterRetriever.js +41 -0
- package/lib/packlets/retrieve/structuredFilterRetriever.js.map +1 -0
- package/lib/packlets/retrieve/tagRetriever.d.ts +22 -0
- package/lib/packlets/retrieve/tagRetriever.d.ts.map +1 -0
- package/lib/packlets/retrieve/tagRetriever.js +41 -0
- package/lib/packlets/retrieve/tagRetriever.js.map +1 -0
- package/lib/packlets/store/fileTreeMemoryStore.d.ts +327 -0
- package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -0
- package/lib/packlets/store/fileTreeMemoryStore.js +702 -0
- package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -0
- package/lib/packlets/store/index.d.ts +3 -0
- package/lib/packlets/store/index.d.ts.map +1 -0
- package/lib/packlets/store/index.js +23 -0
- package/lib/packlets/store/index.js.map +1 -0
- package/lib/packlets/store/scopeEncoding.d.ts +19 -0
- package/lib/packlets/store/scopeEncoding.d.ts.map +1 -0
- package/lib/packlets/store/scopeEncoding.js +34 -0
- package/lib/packlets/store/scopeEncoding.js.map +1 -0
- package/lib/packlets/types/envelope.d.ts +119 -0
- package/lib/packlets/types/envelope.d.ts.map +1 -0
- package/lib/packlets/types/envelope.js +7 -0
- package/lib/packlets/types/envelope.js.map +1 -0
- package/lib/packlets/types/filenameSafety.d.ts +16 -0
- package/lib/packlets/types/filenameSafety.d.ts.map +1 -0
- package/lib/packlets/types/filenameSafety.js +55 -0
- package/lib/packlets/types/filenameSafety.js.map +1 -0
- package/lib/packlets/types/identityCodec.d.ts +136 -0
- package/lib/packlets/types/identityCodec.d.ts.map +1 -0
- package/lib/packlets/types/identityCodec.js +190 -0
- package/lib/packlets/types/identityCodec.js.map +1 -0
- package/lib/packlets/types/ids.d.ts +55 -0
- package/lib/packlets/types/ids.d.ts.map +1 -0
- package/lib/packlets/types/ids.js +70 -0
- package/lib/packlets/types/ids.js.map +1 -0
- package/lib/packlets/types/index.d.ts +6 -0
- package/lib/packlets/types/index.d.ts.map +1 -0
- package/lib/packlets/types/index.js +26 -0
- package/lib/packlets/types/index.js.map +1 -0
- package/lib/packlets/types/writePolicy.d.ts +213 -0
- package/lib/packlets/types/writePolicy.d.ts.map +1 -0
- package/lib/packlets/types/writePolicy.js +268 -0
- package/lib/packlets/types/writePolicy.js.map +1 -0
- package/lib/packlets/vector/inMemoryCosineIndex.d.ts +69 -0
- package/lib/packlets/vector/inMemoryCosineIndex.d.ts.map +1 -0
- package/lib/packlets/vector/inMemoryCosineIndex.js +154 -0
- package/lib/packlets/vector/inMemoryCosineIndex.js.map +1 -0
- package/lib/packlets/vector/index.d.ts +3 -0
- package/lib/packlets/vector/index.d.ts.map +1 -0
- package/lib/packlets/vector/index.js +23 -0
- package/lib/packlets/vector/index.js.map +1 -0
- package/lib/packlets/vector/vectorIndex.d.ts +68 -0
- package/lib/packlets/vector/vectorIndex.d.ts.map +1 -0
- package/lib/packlets/vector/vectorIndex.js +7 -0
- package/lib/packlets/vector/vectorIndex.js.map +1 -0
- package/lib/test/unit/converters/bodyConverterRegistry.test.d.ts +2 -0
- package/lib/test/unit/converters/bodyConverterRegistry.test.d.ts.map +1 -0
- package/lib/test/unit/converters/bodyConverterRegistry.test.js +74 -0
- package/lib/test/unit/converters/bodyConverterRegistry.test.js.map +1 -0
- package/lib/test/unit/converters/envelopeConverter.test.d.ts +2 -0
- package/lib/test/unit/converters/envelopeConverter.test.d.ts.map +1 -0
- package/lib/test/unit/converters/envelopeConverter.test.js +198 -0
- package/lib/test/unit/converters/envelopeConverter.test.js.map +1 -0
- package/lib/test/unit/index/memoryIndex.test.d.ts +2 -0
- package/lib/test/unit/index/memoryIndex.test.d.ts.map +1 -0
- package/lib/test/unit/index/memoryIndex.test.js +154 -0
- package/lib/test/unit/index/memoryIndex.test.js.map +1 -0
- package/lib/test/unit/observe/memoryObservationStore.test.d.ts +2 -0
- package/lib/test/unit/observe/memoryObservationStore.test.d.ts.map +1 -0
- package/lib/test/unit/observe/memoryObservationStore.test.js +120 -0
- package/lib/test/unit/observe/memoryObservationStore.test.js.map +1 -0
- package/lib/test/unit/retrieve/linkTraversalRetriever.test.d.ts +2 -0
- package/lib/test/unit/retrieve/linkTraversalRetriever.test.d.ts.map +1 -0
- package/lib/test/unit/retrieve/linkTraversalRetriever.test.js +184 -0
- package/lib/test/unit/retrieve/linkTraversalRetriever.test.js.map +1 -0
- package/lib/test/unit/retrieve/retrievers.test.d.ts +2 -0
- package/lib/test/unit/retrieve/retrievers.test.d.ts.map +1 -0
- package/lib/test/unit/retrieve/retrievers.test.js +508 -0
- package/lib/test/unit/retrieve/retrievers.test.js.map +1 -0
- package/lib/test/unit/store/embedOnWrite.test.d.ts +2 -0
- package/lib/test/unit/store/embedOnWrite.test.d.ts.map +1 -0
- package/lib/test/unit/store/embedOnWrite.test.js +262 -0
- package/lib/test/unit/store/embedOnWrite.test.js.map +1 -0
- package/lib/test/unit/store/fileTreeMemoryStore.test.d.ts +2 -0
- package/lib/test/unit/store/fileTreeMemoryStore.test.d.ts.map +1 -0
- package/lib/test/unit/store/fileTreeMemoryStore.test.js +649 -0
- package/lib/test/unit/store/fileTreeMemoryStore.test.js.map +1 -0
- package/lib/test/unit/store/observations.test.d.ts +2 -0
- package/lib/test/unit/store/observations.test.d.ts.map +1 -0
- package/lib/test/unit/store/observations.test.js +241 -0
- package/lib/test/unit/store/observations.test.js.map +1 -0
- package/lib/test/unit/store/scopeEncoding.test.d.ts +2 -0
- package/lib/test/unit/store/scopeEncoding.test.d.ts.map +1 -0
- package/lib/test/unit/store/scopeEncoding.test.js +26 -0
- package/lib/test/unit/store/scopeEncoding.test.js.map +1 -0
- package/lib/test/unit/types/identityCodec.test.d.ts +2 -0
- package/lib/test/unit/types/identityCodec.test.d.ts.map +1 -0
- package/lib/test/unit/types/identityCodec.test.js +189 -0
- package/lib/test/unit/types/identityCodec.test.js.map +1 -0
- package/lib/test/unit/types/ids.test.d.ts +2 -0
- package/lib/test/unit/types/ids.test.d.ts.map +1 -0
- package/lib/test/unit/types/ids.test.js +86 -0
- package/lib/test/unit/types/ids.test.js.map +1 -0
- package/lib/test/unit/types/writePolicy.test.d.ts +2 -0
- package/lib/test/unit/types/writePolicy.test.d.ts.map +1 -0
- package/lib/test/unit/types/writePolicy.test.js +243 -0
- package/lib/test/unit/types/writePolicy.test.js.map +1 -0
- package/lib/test/unit/vector/inMemoryCosineIndex.test.d.ts +2 -0
- package/lib/test/unit/vector/inMemoryCosineIndex.test.d.ts.map +1 -0
- package/lib/test/unit/vector/inMemoryCosineIndex.test.js +194 -0
- package/lib/test/unit/vector/inMemoryCosineIndex.test.js.map +1 -0
- package/lib/test/unit/vector/vectorIndex.test.d.ts +2 -0
- package/lib/test/unit/vector/vectorIndex.test.d.ts.map +1 -0
- package/lib/test/unit/vector/vectorIndex.test.js +44 -0
- package/lib/test/unit/vector/vectorIndex.test.js.map +1 -0
- package/package.json +81 -0
- package/rush-logs/ts-agent-memory.build.cache.log +3 -0
- package/rush-logs/ts-agent-memory.build.log +9 -0
- package/src/index.ts +12 -0
- package/src/packlets/converters/bodyConverterRegistry.ts +105 -0
- package/src/packlets/converters/envelopeConverter.ts +210 -0
- package/src/packlets/converters/index.ts +7 -0
- package/src/packlets/index/index.ts +6 -0
- package/src/packlets/index/memoryIndex.ts +268 -0
- package/src/packlets/observe/index.ts +7 -0
- package/src/packlets/observe/memoryObservationStore.ts +153 -0
- package/src/packlets/observe/observer.ts +119 -0
- package/src/packlets/retrieve/hybridRetriever.ts +181 -0
- package/src/packlets/retrieve/index.ts +12 -0
- package/src/packlets/retrieve/linkTraversalRetriever.ts +169 -0
- package/src/packlets/retrieve/recencyRetriever.ts +54 -0
- package/src/packlets/retrieve/retriever.ts +207 -0
- package/src/packlets/retrieve/semanticRetriever.ts +147 -0
- package/src/packlets/retrieve/structuredFilterRetriever.ts +58 -0
- package/src/packlets/retrieve/tagRetriever.ts +58 -0
- package/src/packlets/store/fileTreeMemoryStore.ts +1073 -0
- package/src/packlets/store/index.ts +7 -0
- package/src/packlets/store/scopeEncoding.ts +36 -0
- package/src/packlets/types/envelope.ts +138 -0
- package/src/packlets/types/filenameSafety.ts +57 -0
- package/src/packlets/types/identityCodec.ts +263 -0
- package/src/packlets/types/ids.ts +124 -0
- package/src/packlets/types/index.ts +10 -0
- package/src/packlets/types/writePolicy.ts +447 -0
- package/src/packlets/vector/inMemoryCosineIndex.ts +173 -0
- package/src/packlets/vector/index.ts +7 -0
- package/src/packlets/vector/vectorIndex.ts +78 -0
- package/src/test/unit/converters/bodyConverterRegistry.test.ts +89 -0
- package/src/test/unit/converters/envelopeConverter.test.ts +261 -0
- package/src/test/unit/index/memoryIndex.test.ts +187 -0
- package/src/test/unit/observe/memoryObservationStore.test.ts +158 -0
- package/src/test/unit/retrieve/linkTraversalRetriever.test.ts +230 -0
- package/src/test/unit/retrieve/retrievers.test.ts +662 -0
- package/src/test/unit/store/embedOnWrite.test.ts +346 -0
- package/src/test/unit/store/fileTreeMemoryStore.test.ts +875 -0
- package/src/test/unit/store/observations.test.ts +290 -0
- package/src/test/unit/store/scopeEncoding.test.ts +37 -0
- package/src/test/unit/types/identityCodec.test.ts +266 -0
- package/src/test/unit/types/ids.test.ts +94 -0
- package/src/test/unit/types/writePolicy.test.ts +325 -0
- package/src/test/unit/vector/inMemoryCosineIndex.test.ts +242 -0
- package/src/test/unit/vector/vectorIndex.test.ts +48 -0
- package/temp/build/lint/_eslint-5eVG3S6w.json +182 -0
- package/temp/build/typescript/ts_8nwakTlr.json +1 -0
- package/temp/ts-agent-memory.api.json +12438 -0
- package/temp/ts-agent-memory.api.md +525 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2026 Erik Fortune
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.FileTreeMemoryStore = void 0;
|
|
8
|
+
const ts_utils_1 = require("@fgv/ts-utils");
|
|
9
|
+
const ts_json_base_1 = require("@fgv/ts-json-base");
|
|
10
|
+
const types_1 = require("../types");
|
|
11
|
+
const converters_1 = require("../converters");
|
|
12
|
+
const index_1 = require("../index");
|
|
13
|
+
const scopeEncoding_1 = require("./scopeEncoding");
|
|
14
|
+
/** The on-disk extension for a memory record file. */
|
|
15
|
+
const MEMORY_FILE_EXTENSION = '.md';
|
|
16
|
+
/**
|
|
17
|
+
* Flat-layout, FileTree-backed {@link IMemoryStore}. The FileTree is the source
|
|
18
|
+
* of truth; the {@link IMemoryIndex} is a derived in-memory view patched on
|
|
19
|
+
* every write. Concurrent writes are serialized through a per-instance async
|
|
20
|
+
* write-lock so the index and the on-disk files never interleave.
|
|
21
|
+
*
|
|
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.
|
|
26
|
+
* @public
|
|
27
|
+
*/
|
|
28
|
+
class FileTreeMemoryStore {
|
|
29
|
+
constructor(params) {
|
|
30
|
+
this._root = params.root;
|
|
31
|
+
this._registry = params.registry;
|
|
32
|
+
this._writePolicies = params.writePolicies;
|
|
33
|
+
this._codecs = params.codecs;
|
|
34
|
+
this._defaultCodec = params.defaultCodec;
|
|
35
|
+
this._defaultPolicy = params.defaultPolicy;
|
|
36
|
+
this._scopeEncoding = params.scopeEncoding;
|
|
37
|
+
this._clock = params.clock;
|
|
38
|
+
this._index = params.index;
|
|
39
|
+
this._hasher = new ts_utils_1.Hash.Crc32Normalizer();
|
|
40
|
+
this._observers = params.observers;
|
|
41
|
+
this._logger = params.logger;
|
|
42
|
+
this._vectorIndex = params.vectorIndex;
|
|
43
|
+
this._embed = params.embed;
|
|
44
|
+
this._seq = 0;
|
|
45
|
+
this._observationSeq = 0;
|
|
46
|
+
this._writeTail = Promise.resolve();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Family-convention factory. Builds the derived index and a default LWW
|
|
50
|
+
* policy, then performs an initial FileTree walk so an existing vault is
|
|
51
|
+
* indexed (and the `seq` counter resumes past the highest persisted `seq`).
|
|
52
|
+
*/
|
|
53
|
+
static create(params) {
|
|
54
|
+
return types_1.KnowledgeLwwPolicy.create().onSuccess((defaultPolicy) => index_1.MemoryIndex.create().onSuccess((index) => {
|
|
55
|
+
var _a, _b, _c, _d, _e, _f;
|
|
56
|
+
const store = new FileTreeMemoryStore({
|
|
57
|
+
root: params.root,
|
|
58
|
+
registry: params.registry,
|
|
59
|
+
writePolicies: (_a = params.writePolicies) !== null && _a !== void 0 ? _a : new Map(),
|
|
60
|
+
codecs: (_b = params.codecs) !== null && _b !== void 0 ? _b : new Map(),
|
|
61
|
+
defaultCodec: params.defaultCodec,
|
|
62
|
+
defaultPolicy,
|
|
63
|
+
scopeEncoding: (_c = params.scopeEncoding) !== null && _c !== void 0 ? _c : scopeEncoding_1.defaultMemoryScopeEncoding,
|
|
64
|
+
clock: (_d = params.clock) !== null && _d !== void 0 ? _d : Date.now,
|
|
65
|
+
index,
|
|
66
|
+
observers: (_e = params.observers) !== null && _e !== void 0 ? _e : [],
|
|
67
|
+
logger: (_f = params.logger) !== null && _f !== void 0 ? _f : new ts_utils_1.Logging.NoOpLogger(),
|
|
68
|
+
vectorIndex: params.vectorIndex,
|
|
69
|
+
embed: params.embed
|
|
70
|
+
});
|
|
71
|
+
return store._initialIndex().onSuccess(() => (0, ts_utils_1.succeed)(store));
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
/** {@inheritDoc IMemoryStore.get} */
|
|
75
|
+
async get(kind, entityId) {
|
|
76
|
+
var _a;
|
|
77
|
+
const result = this._codecFor(kind).onSuccess((codec) => codec.encode(entityId).onSuccess((addr) => {
|
|
78
|
+
if (addr.isVersioned) {
|
|
79
|
+
return (0, ts_utils_1.fail)(`memory get '${entityId}': versioned/temporal layout not yet supported`);
|
|
80
|
+
}
|
|
81
|
+
return this._readRecord(addr.scope, addr.idStem);
|
|
82
|
+
}));
|
|
83
|
+
await this._fireObservation('read', kind, entityId, {
|
|
84
|
+
outcome: result.isSuccess() ? 'success' : 'failure',
|
|
85
|
+
id: result.isSuccess() ? (_a = result.value) === null || _a === void 0 ? void 0 : _a.envelope.id : undefined,
|
|
86
|
+
error: result.isFailure() ? result.message : undefined
|
|
87
|
+
});
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
/** {@inheritDoc IMemoryStore.getById} */
|
|
91
|
+
async getById(scope, id) {
|
|
92
|
+
return this._readRecord(scope, id);
|
|
93
|
+
}
|
|
94
|
+
/** {@inheritDoc IMemoryStore.list} */
|
|
95
|
+
async list(filter) {
|
|
96
|
+
const matches = this._index
|
|
97
|
+
.entries()
|
|
98
|
+
.filter((entry) => {
|
|
99
|
+
if ((filter === null || filter === void 0 ? void 0 : filter.scope) !== undefined && entry.scope !== filter.scope) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
if ((filter === null || filter === void 0 ? void 0 : filter.kind) !== undefined && entry.record.envelope.kind !== filter.kind) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
if ((filter === null || filter === void 0 ? void 0 : filter.tag) !== undefined && !entry.record.envelope.tags.includes(filter.tag)) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
})
|
|
110
|
+
.map((entry) => entry.record);
|
|
111
|
+
return (0, ts_utils_1.succeed)(matches);
|
|
112
|
+
}
|
|
113
|
+
/** {@inheritDoc IMemoryStore.put} */
|
|
114
|
+
async put(record) {
|
|
115
|
+
const result = await this._enqueue(() => this._putLocked(record));
|
|
116
|
+
await this._fireObservation('write', record.envelope.kind, record.envelope.entityId, {
|
|
117
|
+
outcome: result.isSuccess() ? 'success' : 'failure',
|
|
118
|
+
id: result.isSuccess() ? result.value.record.envelope.id : record.envelope.id,
|
|
119
|
+
provenance: record.envelope.provenance,
|
|
120
|
+
error: result.isFailure() ? result.message : undefined
|
|
121
|
+
});
|
|
122
|
+
// Fire one `'delete'` observation per record evicted by cap-cull. The
|
|
123
|
+
// evicted records are always in the same `(scope, kind)` cohort as the
|
|
124
|
+
// incoming record, so resolving the scope from the incoming `entityId`
|
|
125
|
+
// yields the evicted record's scope; the observation records the evicted
|
|
126
|
+
// record's own `id`. A no-op / first-write / reject path yields no
|
|
127
|
+
// evictions, so this loop is empty there.
|
|
128
|
+
if (result.isSuccess()) {
|
|
129
|
+
for (const evictedId of result.value.evicted) {
|
|
130
|
+
await this._fireObservation('delete', record.envelope.kind, record.envelope.entityId, {
|
|
131
|
+
outcome: 'success',
|
|
132
|
+
id: evictedId
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return result.onSuccess((outcome) => (0, ts_utils_1.succeed)(outcome.record));
|
|
137
|
+
}
|
|
138
|
+
/** {@inheritDoc IMemoryStore.delete} */
|
|
139
|
+
async delete(kind, entityId) {
|
|
140
|
+
const result = await this._enqueue(() => this._deleteLocked(kind, entityId));
|
|
141
|
+
await this._fireObservation('delete', kind, entityId, {
|
|
142
|
+
outcome: result.isSuccess() ? 'success' : 'failure',
|
|
143
|
+
id: result.isSuccess() ? result.value : undefined,
|
|
144
|
+
error: result.isFailure() ? result.message : undefined
|
|
145
|
+
});
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Serialize a mutating task behind the write-lock. Tasks run in submission
|
|
150
|
+
* order; a failed task does not break the chain for subsequent ones.
|
|
151
|
+
*/
|
|
152
|
+
_enqueue(task) {
|
|
153
|
+
const result = this._writeTail.then(task);
|
|
154
|
+
this._writeTail = result.then(() => undefined, () => undefined);
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Build and fan out one observation record for a completed op. A no-op when no
|
|
159
|
+
* observers are wired (the additive-default path pays nothing). The store is
|
|
160
|
+
* the seq authority — it mints `seq` / `timestamp` so every observer sees the
|
|
161
|
+
* same record. The scope is resolved best-effort via the codec.
|
|
162
|
+
*/
|
|
163
|
+
async _fireObservation(phase, kind, entityId, details) {
|
|
164
|
+
if (this._observers.length === 0) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const record = {
|
|
168
|
+
seq: ++this._observationSeq,
|
|
169
|
+
timestamp: this._clock(),
|
|
170
|
+
phase,
|
|
171
|
+
scope: this._scopeBestEffort(kind, entityId),
|
|
172
|
+
id: details.id,
|
|
173
|
+
kind,
|
|
174
|
+
outcome: details.outcome,
|
|
175
|
+
error: details.error,
|
|
176
|
+
provenance: details.provenance
|
|
177
|
+
};
|
|
178
|
+
const awaited = [];
|
|
179
|
+
for (const observer of this._observers) {
|
|
180
|
+
if (observer.fireAndForget === true) {
|
|
181
|
+
// Intentionally not awaited: a fire-and-forget observer must not extend
|
|
182
|
+
// the store op's latency. `_safeObserve` swallows internally; the
|
|
183
|
+
// `.catch` keeps the detached promise from being flagged as floating.
|
|
184
|
+
this._safeObserve(observer, record).catch(() => undefined);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
awaited.push(this._safeObserve(observer, record));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
await Promise.all(awaited);
|
|
191
|
+
}
|
|
192
|
+
/** Invoke one observer, swallowing any failure or throw (logged at `warn`). */
|
|
193
|
+
async _safeObserve(observer, record) {
|
|
194
|
+
try {
|
|
195
|
+
const observed = await observer.observe(record);
|
|
196
|
+
if (observed.isFailure()) {
|
|
197
|
+
this._warnSwallowed(`memory observer failed (swallowed): ${observed.message}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
this._warnSwallowed(`memory observer threw (swallowed): ${String(error)}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Log a swallowed-issue warning (observer failure or best-effort vector
|
|
206
|
+
* maintenance), tolerating a logger that itself throws — diagnostic logging
|
|
207
|
+
* must never make a store op reject.
|
|
208
|
+
*/
|
|
209
|
+
_warnSwallowed(message) {
|
|
210
|
+
try {
|
|
211
|
+
this._logger.warn(message);
|
|
212
|
+
}
|
|
213
|
+
catch (_a) {
|
|
214
|
+
// Diagnostic logging must not affect the store operation.
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/** Resolve a scope for an observation, best-effort (undefined when unresolvable). */
|
|
218
|
+
_scopeBestEffort(kind, entityId) {
|
|
219
|
+
return this._codecFor(kind)
|
|
220
|
+
.onSuccess((codec) => codec.encode(entityId))
|
|
221
|
+
.onSuccess((addr) => (0, ts_utils_1.succeed)(addr.scope))
|
|
222
|
+
.orDefault();
|
|
223
|
+
}
|
|
224
|
+
async _putLocked(record) {
|
|
225
|
+
const envelope = record.envelope;
|
|
226
|
+
if (typeof record.body !== 'string') {
|
|
227
|
+
return (0, ts_utils_1.fail)(`memory put '${envelope.id}': only string (markdown) bodies are supported (got ${typeof record.body})`);
|
|
228
|
+
}
|
|
229
|
+
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.
|
|
234
|
+
return this._registry
|
|
235
|
+
.convert(envelope.kind, body)
|
|
236
|
+
.withErrorFormat((msg) => `memory put '${envelope.id}': invalid body: ${msg}`)
|
|
237
|
+
.onSuccess(() => this._codecFor(envelope.kind))
|
|
238
|
+
.onSuccess((codec) => codec.encode(envelope.entityId).onSuccess((addr) => {
|
|
239
|
+
if (addr.isVersioned) {
|
|
240
|
+
return (0, ts_utils_1.fail)(`memory put '${envelope.entityId}': versioned/temporal layout not yet supported`);
|
|
241
|
+
}
|
|
242
|
+
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}'`);
|
|
244
|
+
}
|
|
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));
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Run dedup → policy → stamp → embed → write for a resolved address and content
|
|
251
|
+
* hash. Async because the embed-on-write hook (when wired) does a network call
|
|
252
|
+
* or in-process inference; the whole chain runs inside the write-lock so the
|
|
253
|
+
* vector index, the on-disk file, and the derived index never interleave.
|
|
254
|
+
*/
|
|
255
|
+
async _writeResolved(record, body, scope, idStem, hash) {
|
|
256
|
+
var _a;
|
|
257
|
+
const policy = this._policyFor(record.envelope.kind);
|
|
258
|
+
const dedupScope = (_a = policy.dedupScope) !== null && _a !== void 0 ? _a : types_1.DEFAULT_DEDUP_SCOPE;
|
|
259
|
+
// Content-hash dedup runs BEFORE policy. Its granularity is the kind's
|
|
260
|
+
// `dedupScope`:
|
|
261
|
+
// - 'content': an identical { kind, body, links } triple ANYWHERE in the
|
|
262
|
+
// scope (even under a different id) is a no-op (knowledge family).
|
|
263
|
+
// - 'entity' (default): only an identical re-put of the SAME id is a no-op;
|
|
264
|
+
// two distinct entities with identical content never collapse (experience
|
|
265
|
+
// families). The same-id check is folded into the `_readRecord` below.
|
|
266
|
+
// (Tags / provenance are metadata and are NOT part of the hash; see
|
|
267
|
+
// design-lock §2.5.)
|
|
268
|
+
if (dedupScope === 'content') {
|
|
269
|
+
const duplicate = this._findByContentHash(scope, hash);
|
|
270
|
+
if (duplicate !== undefined) {
|
|
271
|
+
return (0, ts_utils_1.succeed)({ record: duplicate, evicted: [] });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
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.
|
|
277
|
+
return Promise.resolve((0, ts_utils_1.succeed)({ record: existing, evicted: [] }));
|
|
278
|
+
}
|
|
279
|
+
// The admission cohort is the set of records the policy's cap applies to:
|
|
280
|
+
// every record in this scope of this kind EXCEPT the target id. On a first
|
|
281
|
+
// write the post-write count is `cohort.length + 1`; on an update the prior
|
|
282
|
+
// same-id record is excluded so the count is still `cohort.length + 1`
|
|
283
|
+
// (a replace, not a grow). The same-id `existing` record is threaded
|
|
284
|
+
// separately into `_buildRecord` for the merge-patch. Knowledge LWW ignores
|
|
285
|
+
// this argument, so its behavior is unchanged by the wider cohort.
|
|
286
|
+
const cohort = this._admissionCohort(scope, record.envelope.kind, idStem);
|
|
287
|
+
return policy
|
|
288
|
+
.admit(record, cohort)
|
|
289
|
+
.thenOnSuccess((decision) => this._admitWrite(record, body, scope, idStem, hash, policy, existing, decision));
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Build → embed → persist → evict for an admitted write.
|
|
294
|
+
*
|
|
295
|
+
* The durable record store is authoritative; the vector index is a **derived,
|
|
296
|
+
* rebuildable** view, so vector maintenance is **best-effort** — a failed embed
|
|
297
|
+
* or `add` is logged and the durable write still succeeds (the index can be
|
|
298
|
+
* rebuilt via {@link InMemoryCosineIndex.rebuild}). Only genuine record-store
|
|
299
|
+
* failures (body/codec/policy, persist, file eviction) fail the `put`.
|
|
300
|
+
*
|
|
301
|
+
* Ordering: the embed + `add` run immediately before the single `_persist` so
|
|
302
|
+
* the index-returned `embeddingRef` lands in one durable write (a post-persist
|
|
303
|
+
* stamp would need a second write whose failure path is effectively untestable).
|
|
304
|
+
* Build and persist the replacement BEFORE evicting the cull-oldest cohort, so a
|
|
305
|
+
* later eviction failure never loses data with nothing written in its place.
|
|
306
|
+
*/
|
|
307
|
+
async _admitWrite(record, body, scope, idStem, hash, policy, existing, decision) {
|
|
308
|
+
if (decision.decision === 'reject') {
|
|
309
|
+
return (0, ts_utils_1.fail)(`memory put: rejected by policy: ${decision.reason}`);
|
|
310
|
+
}
|
|
311
|
+
return this._buildRecord(record, body, existing, policy, hash)
|
|
312
|
+
.thenOnSuccess((built) => this._embedOnWrite(built))
|
|
313
|
+
.onSuccess((embeddedBuilt) => this._persist(embeddedBuilt, scope, idStem))
|
|
314
|
+
.thenOnSuccess(async (persisted) => {
|
|
315
|
+
// Everything after the authoritative `_persist` commit is best-effort and
|
|
316
|
+
// never turns a committed write into a `Failure`:
|
|
317
|
+
// - a cull-oldest eviction that fails is logged (the per-(scope,kind)
|
|
318
|
+
// cap may be transiently exceeded; the next write's admission restores
|
|
319
|
+
// it), and only the successfully-evicted ids flow onward;
|
|
320
|
+
// - vector pruning of the evicted cohort is likewise best-effort.
|
|
321
|
+
const evicted = this._applyEvictions(decision, scope);
|
|
322
|
+
await this._removeEvictedVectors(evicted);
|
|
323
|
+
return (0, ts_utils_1.succeed)({ record: persisted, evicted });
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Best-effort embed-on-write. When a vector index AND an embedder are wired,
|
|
328
|
+
* embeds the built record, `add`s the vector (replace semantics handle a same-id
|
|
329
|
+
* re-embed — no explicit remove), and stamps the returned `embeddingRef`. A
|
|
330
|
+
* failure (returned `fail` OR a thrown/rejected hook) is logged and the
|
|
331
|
+
* unembedded record is returned unchanged — the put still persists, and the
|
|
332
|
+
* derived index is reconciled by a later `rebuild`. A pass-through no-op when
|
|
333
|
+
* unwired (byte-identical record).
|
|
334
|
+
*
|
|
335
|
+
* Always succeeds (`Result` is the chain's shape, never a vector-induced
|
|
336
|
+
* failure).
|
|
337
|
+
*/
|
|
338
|
+
async _embedOnWrite(built) {
|
|
339
|
+
if (this._vectorIndex === undefined || this._embed === undefined) {
|
|
340
|
+
return (0, ts_utils_1.succeed)(built);
|
|
341
|
+
}
|
|
342
|
+
const vectorIndex = this._vectorIndex;
|
|
343
|
+
const embed = this._embed;
|
|
344
|
+
const embedded = await this._tryVectorOp(() => embed(built), `embedding '${built.envelope.id}'`);
|
|
345
|
+
if (embedded.isFailure()) {
|
|
346
|
+
return (0, ts_utils_1.succeed)(built);
|
|
347
|
+
}
|
|
348
|
+
const added = await this._tryVectorOp(() => vectorIndex.add(built.envelope.id, embedded.value), `vector add for '${built.envelope.id}'`);
|
|
349
|
+
if (added.isFailure()) {
|
|
350
|
+
return (0, ts_utils_1.succeed)(built);
|
|
351
|
+
}
|
|
352
|
+
return (0, ts_utils_1.succeed)({ envelope: Object.assign(Object.assign({}, built.envelope), { embeddingRef: added.value }), body: built.body });
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Evict the records named by a `cull-oldest` decision, best-effort. Runs only
|
|
356
|
+
* after the authoritative `_persist`, so a failed eviction is logged (never
|
|
357
|
+
* fatal) and the cap self-corrects on the next admission. Returns the ids that
|
|
358
|
+
* were actually evicted (so observations / vector pruning cover only those).
|
|
359
|
+
* `accept` / `reject` decisions evict nothing.
|
|
360
|
+
*/
|
|
361
|
+
_applyEvictions(decision, scope) {
|
|
362
|
+
if (decision.decision !== 'cull-oldest') {
|
|
363
|
+
return [];
|
|
364
|
+
}
|
|
365
|
+
const evicted = [];
|
|
366
|
+
for (const id of decision.evict) {
|
|
367
|
+
const result = this._evict(scope, id);
|
|
368
|
+
if (result.isSuccess()) {
|
|
369
|
+
evicted.push(result.value);
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
this._warnSwallowed(`memory put: best-effort eviction of '${id}' failed (cap may be transiently exceeded; restored on the next write): ${result.message}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return evicted;
|
|
376
|
+
}
|
|
377
|
+
/** Best-effort vector removal for each evicted record (never fails the put). */
|
|
378
|
+
async _removeEvictedVectors(evicted) {
|
|
379
|
+
for (const id of evicted) {
|
|
380
|
+
await this._removeVectorBestEffort(id);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Run a consumer-supplied vector hook, normalizing a thrown/rejected hook into a
|
|
385
|
+
* `Failure` and logging any failure at `warn`. Best-effort: the caller proceeds
|
|
386
|
+
* regardless, since the index is rebuildable.
|
|
387
|
+
*/
|
|
388
|
+
async _tryVectorOp(op, label) {
|
|
389
|
+
let result;
|
|
390
|
+
try {
|
|
391
|
+
result = await op();
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
result = (0, ts_utils_1.fail)(`${label} threw: ${String(err)}`);
|
|
395
|
+
}
|
|
396
|
+
if (result.isFailure()) {
|
|
397
|
+
this._warnSwallowed(`memory: ${label} failed (best-effort; vector index left for rebuild): ${result.message}`);
|
|
398
|
+
}
|
|
399
|
+
return result;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Best-effort vector removal. A no-op unless the full vector lifecycle is wired
|
|
403
|
+
* (both an index AND an embedder), so an unwired store does no vector work and
|
|
404
|
+
* behaves byte-identically. Failures are logged, never surfaced — a committed
|
|
405
|
+
* delete/eviction must not fail because a derived index could not be pruned.
|
|
406
|
+
*/
|
|
407
|
+
async _removeVectorBestEffort(id) {
|
|
408
|
+
if (this._vectorIndex === undefined || this._embed === undefined) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const vectorIndex = this._vectorIndex;
|
|
412
|
+
await this._tryVectorOp(() => vectorIndex.remove(id), `vector removal for '${id}'`);
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Build the record to persist. On a first write the incoming envelope is the
|
|
416
|
+
* base (final content equals the incoming content, so the dedup `hash` is
|
|
417
|
+
* reused). On an update the incoming record's mutable fields are projected into
|
|
418
|
+
* a merge-patch and the policy's `applyUpdate` merges them over the existing
|
|
419
|
+
* record (preserving `created`); the persisted body and `contentHash` are then
|
|
420
|
+
* taken from the policy's actual output, so a body-transforming policy is never
|
|
421
|
+
* bypassed and the stored hash always matches the stored `{ kind, body, links }`.
|
|
422
|
+
* The store stamps the transaction-time metadata it owns
|
|
423
|
+
* (`created` / `updated` / `seq` / `contentHash`).
|
|
424
|
+
*/
|
|
425
|
+
_buildRecord(incoming, body, existing, policy, hash) {
|
|
426
|
+
const now = this._clock();
|
|
427
|
+
const seq = ++this._seq;
|
|
428
|
+
if (existing === undefined) {
|
|
429
|
+
return (0, ts_utils_1.succeed)({
|
|
430
|
+
envelope: Object.assign(Object.assign({}, incoming.envelope), { created: now, updated: now, seq, contentHash: hash }),
|
|
431
|
+
body
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
const patch = this._projectMutablePatch(incoming, policy.mutableFields);
|
|
435
|
+
return policy
|
|
436
|
+
.applyUpdate(existing, patch)
|
|
437
|
+
.withErrorFormat((msg) => `memory put '${incoming.envelope.id}': update failed: ${msg}`)
|
|
438
|
+
.onSuccess((updated) => {
|
|
439
|
+
if (typeof updated.body !== 'string') {
|
|
440
|
+
return (0, ts_utils_1.fail)(`memory put '${incoming.envelope.id}': policy returned a non-string body (${typeof updated.body})`);
|
|
441
|
+
}
|
|
442
|
+
const finalBody = updated.body;
|
|
443
|
+
return this._contentHash(updated.envelope.kind, finalBody, updated.envelope.links).onSuccess((finalHash) => (0, ts_utils_1.succeed)({
|
|
444
|
+
envelope: Object.assign(Object.assign({}, updated.envelope), { created: existing.envelope.created, updated: now, seq, contentHash: finalHash }),
|
|
445
|
+
body: finalBody
|
|
446
|
+
}));
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
/** Serialize and write a fully-stamped record, then patch the index. */
|
|
450
|
+
_persist(record, scope, idStem) {
|
|
451
|
+
return (0, converters_1.serializeMemoryFile)(record.envelope, record.body)
|
|
452
|
+
.onSuccess((raw) => this._writeFile(scope, idStem, raw))
|
|
453
|
+
.onSuccess(() => this._index.patch('put', { scope, record }))
|
|
454
|
+
.onSuccess(() => (0, ts_utils_1.succeed)(record));
|
|
455
|
+
}
|
|
456
|
+
async _deleteLocked(kind, entityId) {
|
|
457
|
+
return this._codecFor(kind)
|
|
458
|
+
.onSuccess((codec) => codec.encode(entityId))
|
|
459
|
+
.thenOnSuccess((addr) => {
|
|
460
|
+
if (addr.isVersioned) {
|
|
461
|
+
return Promise.resolve((0, ts_utils_1.fail)(`memory delete '${entityId}': versioned/temporal layout not yet supported`));
|
|
462
|
+
}
|
|
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`));
|
|
466
|
+
}
|
|
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
|
+
});
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
/** Evict (physically delete) a single record file by id, patching the index. */
|
|
480
|
+
_evict(scope, id) {
|
|
481
|
+
return this._readRecord(scope, id).onSuccess((existing) => {
|
|
482
|
+
if (existing === undefined) {
|
|
483
|
+
return (0, ts_utils_1.fail)(`memory put: cannot evict '${id}' in scope '${scope}': not found`);
|
|
484
|
+
}
|
|
485
|
+
return this._deleteFile(scope, id)
|
|
486
|
+
.onSuccess(() => this._index.patch('delete', { scope, record: existing }))
|
|
487
|
+
.onSuccess(() => (0, ts_utils_1.succeed)(id));
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
/** Project the incoming record's mutable fields into a merge-patch. */
|
|
491
|
+
_projectMutablePatch(record, mutableFields) {
|
|
492
|
+
const patch = {};
|
|
493
|
+
for (const field of mutableFields) {
|
|
494
|
+
const accessor = FileTreeMemoryStore._mutableFieldAccessors.get(field);
|
|
495
|
+
if (accessor !== undefined) {
|
|
496
|
+
const value = accessor(record);
|
|
497
|
+
if (value !== undefined) {
|
|
498
|
+
patch[field] = value;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return patch;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* The admission cohort for a write: every indexed record in `scope` of `kind`
|
|
506
|
+
* except the one at `idStem` (the record being written or updated). This is
|
|
507
|
+
* the set a per-kind cap (e.g. {@link MemoryCapCullPolicy}) counts against, so
|
|
508
|
+
* a bounded-ring policy can keep a per-scope/per-kind family within
|
|
509
|
+
* `maxRecords`. Excluding the target id makes the post-write count uniform
|
|
510
|
+
* across first-writes and updates.
|
|
511
|
+
*/
|
|
512
|
+
_admissionCohort(scope, kind, idStem) {
|
|
513
|
+
return this._index
|
|
514
|
+
.entries()
|
|
515
|
+
.filter((entry) => entry.scope === scope && entry.record.envelope.kind === kind && entry.record.envelope.id !== idStem)
|
|
516
|
+
.map((entry) => entry.record);
|
|
517
|
+
}
|
|
518
|
+
/** Find a record in `scope` whose `contentHash` equals `hash`, if any. */
|
|
519
|
+
_findByContentHash(scope, hash) {
|
|
520
|
+
const match = this._index
|
|
521
|
+
.entries()
|
|
522
|
+
.find((entry) => entry.scope === scope && entry.record.envelope.contentHash === hash);
|
|
523
|
+
return match === null || match === void 0 ? void 0 : match.record;
|
|
524
|
+
}
|
|
525
|
+
_contentHash(kind, body, links) {
|
|
526
|
+
return this._hasher.computeHash({ kind, body, links });
|
|
527
|
+
}
|
|
528
|
+
_codecFor(kind) {
|
|
529
|
+
var _a;
|
|
530
|
+
const codec = (_a = this._codecs.get(kind)) !== null && _a !== void 0 ? _a : this._defaultCodec;
|
|
531
|
+
if (codec === undefined) {
|
|
532
|
+
return (0, ts_utils_1.fail)(`no identity codec registered for kind '${kind}'`);
|
|
533
|
+
}
|
|
534
|
+
return (0, ts_utils_1.succeed)(codec);
|
|
535
|
+
}
|
|
536
|
+
_policyFor(kind) {
|
|
537
|
+
var _a;
|
|
538
|
+
return (_a = this._writePolicies.get(kind)) !== null && _a !== void 0 ? _a : this._defaultPolicy;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Read and validate the record at `<scope>/<idStem>.md`, returning `undefined`
|
|
542
|
+
* when the scope directory or file is absent. Verifies the on-disk id ↔
|
|
543
|
+
* filename round-trip on every load.
|
|
544
|
+
*/
|
|
545
|
+
_readRecord(scope, idStem) {
|
|
546
|
+
return this._resolveScopeDir(scope).onSuccess((scopeDir) => {
|
|
547
|
+
if (scopeDir === undefined) {
|
|
548
|
+
return (0, ts_utils_1.succeed)(undefined);
|
|
549
|
+
}
|
|
550
|
+
return scopeDir.getChildren().onSuccess((children) => {
|
|
551
|
+
const targetName = `${idStem}${MEMORY_FILE_EXTENSION}`;
|
|
552
|
+
const file = children.find((c) => c.type === 'file' && c.name === targetName);
|
|
553
|
+
if (file === undefined) {
|
|
554
|
+
return (0, ts_utils_1.succeed)(undefined);
|
|
555
|
+
}
|
|
556
|
+
return file
|
|
557
|
+
.getRawContents()
|
|
558
|
+
.onSuccess((raw) => (0, converters_1.parseMemoryFile)(raw, this._registry))
|
|
559
|
+
.onSuccess((parsedRecord) => this._verifyLoaded(scope, file, parsedRecord));
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
/** Enforce `envelope.id === filename stem` and the codec round-trip on load. */
|
|
564
|
+
_verifyLoaded(scope, file, record) {
|
|
565
|
+
if (record.envelope.id !== file.baseName) {
|
|
566
|
+
return (0, ts_utils_1.fail)(`memory file '${file.absolutePath}': envelope id '${record.envelope.id}' does not match filename stem '${file.baseName}'`);
|
|
567
|
+
}
|
|
568
|
+
return this._codecFor(record.envelope.kind)
|
|
569
|
+
.onSuccess((codec) => codec.verifyRoundTrip(scope, file.baseName))
|
|
570
|
+
.withErrorFormat((msg) => `memory file '${file.absolutePath}': ${msg}`)
|
|
571
|
+
.onSuccess(() => (0, ts_utils_1.succeed)(record));
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Resolve the directory for a scope, returning `undefined` when it does not
|
|
575
|
+
* exist. Navigation only — does not create. Folds the path segments through
|
|
576
|
+
* `getChildren` so an absent segment short-circuits to `undefined`.
|
|
577
|
+
*/
|
|
578
|
+
_resolveScopeDir(scope) {
|
|
579
|
+
return this._scopeEncoding(scope).onSuccess((encoded) => {
|
|
580
|
+
const segments = encoded.split('/').filter((s) => s.length > 0);
|
|
581
|
+
return segments.reduce((acc, segment) => acc.onSuccess((current) => {
|
|
582
|
+
if (current === undefined) {
|
|
583
|
+
return (0, ts_utils_1.succeed)(undefined);
|
|
584
|
+
}
|
|
585
|
+
return current
|
|
586
|
+
.getChildren()
|
|
587
|
+
.onSuccess((children) => (0, ts_utils_1.succeed)(children.find((c) => c.type === 'directory' && c.name === segment)));
|
|
588
|
+
}), (0, ts_utils_1.succeed)(this._root));
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
/** Ensure the scope directory exists, creating segments as needed. */
|
|
592
|
+
_ensureScopeDir(scope) {
|
|
593
|
+
return this._scopeEncoding(scope).onSuccess((encoded) => {
|
|
594
|
+
const segments = encoded.split('/').filter((s) => s.length > 0);
|
|
595
|
+
return segments.reduce((acc, segment) => acc.onSuccess((current) => current.getChildren().onSuccess((children) => {
|
|
596
|
+
const existing = children.find((c) => c.type === 'directory' && c.name === segment);
|
|
597
|
+
if (existing === undefined) {
|
|
598
|
+
return current.createChildDirectory(segment);
|
|
599
|
+
}
|
|
600
|
+
/* 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 */
|
|
601
|
+
if (!ts_json_base_1.FileTree.isMutableDirectoryItem(existing)) {
|
|
602
|
+
return (0, ts_utils_1.fail)(`${existing.absolutePath}: directory is not mutable`);
|
|
603
|
+
}
|
|
604
|
+
return (0, ts_utils_1.succeed)(existing);
|
|
605
|
+
})), (0, ts_utils_1.succeed)(this._root));
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
/** Write (create or overwrite) `<scope>/<idStem>.md` with `raw`. */
|
|
609
|
+
_writeFile(scope, idStem, raw) {
|
|
610
|
+
return this._ensureScopeDir(scope).onSuccess((scopeDir) => scopeDir.getChildren().onSuccess((children) => {
|
|
611
|
+
const fileName = `${idStem}${MEMORY_FILE_EXTENSION}`;
|
|
612
|
+
const existing = children.find((c) => c.type === 'file' && c.name === fileName);
|
|
613
|
+
if (existing === undefined) {
|
|
614
|
+
return scopeDir.createChildFile(fileName, raw).onSuccess(() => (0, ts_utils_1.succeed)(true));
|
|
615
|
+
}
|
|
616
|
+
/* c8 ignore next 3 -- defensive: a file in a mutable tree is mutable; guards a read-only adapter */
|
|
617
|
+
if (!ts_json_base_1.FileTree.isMutableFileItem(existing)) {
|
|
618
|
+
return (0, ts_utils_1.fail)(`${existing.absolutePath}: file is not mutable`);
|
|
619
|
+
}
|
|
620
|
+
return existing.setRawContents(raw).onSuccess(() => (0, ts_utils_1.succeed)(true));
|
|
621
|
+
}));
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Physically delete `<scope>/<idStem>.md`. The scope-missing and file-missing
|
|
625
|
+
* guards are unreachable through the callers (`delete` / `_evict` both read the
|
|
626
|
+
* record first, so the directory and file exist) but are kept so a future
|
|
627
|
+
* direct caller degrades loudly rather than silently.
|
|
628
|
+
*/
|
|
629
|
+
_deleteFile(scope, idStem) {
|
|
630
|
+
return this._resolveScopeDir(scope).onSuccess((scopeDir) => {
|
|
631
|
+
/* c8 ignore next 3 -- unreachable: callers read the record (hence the scope dir) first */
|
|
632
|
+
if (scopeDir === undefined) {
|
|
633
|
+
return (0, ts_utils_1.fail)(`memory delete: scope '${scope}' not found`);
|
|
634
|
+
}
|
|
635
|
+
const fileName = `${idStem}${MEMORY_FILE_EXTENSION}`;
|
|
636
|
+
return scopeDir.getChildren().onSuccess((children) => {
|
|
637
|
+
const file = children.find((c) => c.type === 'file' && c.name === fileName);
|
|
638
|
+
/* c8 ignore next 3 -- unreachable: callers read the record (hence the file) first */
|
|
639
|
+
if (file === undefined) {
|
|
640
|
+
return (0, ts_utils_1.fail)(`memory delete: file '${fileName}' not found in scope '${scope}'`);
|
|
641
|
+
}
|
|
642
|
+
/* c8 ignore next 3 -- defensive: a file in a mutable tree is mutable; guards a read-only adapter */
|
|
643
|
+
if (!ts_json_base_1.FileTree.isMutableFileItem(file)) {
|
|
644
|
+
return (0, ts_utils_1.fail)(`${file.absolutePath}: file is not mutable`);
|
|
645
|
+
}
|
|
646
|
+
return file.delete().onSuccess(() => (0, ts_utils_1.succeed)(true));
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Walk the FileTree once and rebuild the index. Also resumes the `seq`
|
|
652
|
+
* counter past the highest persisted `seq` so new writes stay monotonic.
|
|
653
|
+
*/
|
|
654
|
+
_initialIndex() {
|
|
655
|
+
return this._collectEntries(this._root, []).onSuccess((entries) => this._index.rebuild(entries).onSuccess(() => {
|
|
656
|
+
for (const entry of entries) {
|
|
657
|
+
if (entry.record.envelope.seq > this._seq) {
|
|
658
|
+
this._seq = entry.record.envelope.seq;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return (0, ts_utils_1.succeed)(true);
|
|
662
|
+
}));
|
|
663
|
+
}
|
|
664
|
+
/** Recursively collect every `.md` record under `dir` (scope = path segments). */
|
|
665
|
+
_collectEntries(dir, scopeSegments) {
|
|
666
|
+
return dir.getChildren().onSuccess((children) => {
|
|
667
|
+
const results = children.map((child) => {
|
|
668
|
+
if (child.type === 'directory') {
|
|
669
|
+
return this._collectEntries(child, [...scopeSegments, child.name]);
|
|
670
|
+
}
|
|
671
|
+
if (!child.name.endsWith(MEMORY_FILE_EXTENSION) || scopeSegments.length === 0) {
|
|
672
|
+
// Skip non-record files and any record-shaped file sitting at the root
|
|
673
|
+
// (records always live under at least one scope segment).
|
|
674
|
+
return (0, ts_utils_1.succeed)([]);
|
|
675
|
+
}
|
|
676
|
+
const scope = scopeSegments.join('/');
|
|
677
|
+
return child
|
|
678
|
+
.getRawContents()
|
|
679
|
+
.onSuccess((raw) => (0, converters_1.parseMemoryFile)(raw, this._registry))
|
|
680
|
+
.onSuccess((parsedRecord) => this._verifyLoaded(scope, child, parsedRecord))
|
|
681
|
+
.onSuccess((verified) => (0, ts_utils_1.succeed)([{ scope, record: verified }]));
|
|
682
|
+
});
|
|
683
|
+
return (0, ts_utils_1.mapResults)(results).onSuccess((perChild) => (0, ts_utils_1.succeed)(perChild.flat()));
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
exports.FileTreeMemoryStore = FileTreeMemoryStore;
|
|
688
|
+
/**
|
|
689
|
+
* The record-level mutable-field vocabulary: maps a declared mutable field
|
|
690
|
+
* name to its canonical location on a record. Used to project an incoming
|
|
691
|
+
* record into a merge-patch on update. B1 ships the knowledge-LWW surface
|
|
692
|
+
* (body + envelope metadata); body-internal mutable fields (Phase-C cap-cull)
|
|
693
|
+
* extend this map.
|
|
694
|
+
*/
|
|
695
|
+
FileTreeMemoryStore._mutableFieldAccessors = new Map([
|
|
696
|
+
['body', (r) => r.body],
|
|
697
|
+
['tags', (r) => r.envelope.tags],
|
|
698
|
+
['links', (r) => r.envelope.links],
|
|
699
|
+
['provenance', (r) => r.envelope.provenance],
|
|
700
|
+
['embeddingRef', (r) => r.envelope.embeddingRef]
|
|
701
|
+
]);
|
|
702
|
+
//# sourceMappingURL=fileTreeMemoryStore.js.map
|