@fgv/ts-agent-memory 5.1.0-49 → 5.1.0-51
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/index/memoryIndex.js +23 -16
- package/dist/packlets/index/memoryIndex.js.map +1 -1
- package/dist/packlets/ingest/orchestrator.js +13 -1
- package/dist/packlets/ingest/orchestrator.js.map +1 -1
- package/dist/packlets/retrieve/fragmentSemanticRetriever.js +94 -5
- package/dist/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -1
- package/dist/packlets/retrieve/linkTraversalRetriever.js +12 -26
- package/dist/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
- package/dist/packlets/retrieve/recencyRetriever.js +7 -7
- package/dist/packlets/retrieve/recencyRetriever.js.map +1 -1
- package/dist/packlets/retrieve/retriever.js +91 -10
- package/dist/packlets/retrieve/retriever.js.map +1 -1
- package/dist/packlets/retrieve/semanticRetriever.js +16 -16
- package/dist/packlets/retrieve/semanticRetriever.js.map +1 -1
- package/dist/packlets/retrieve/structuredFilterRetriever.js +7 -7
- package/dist/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
- package/dist/packlets/retrieve/tagRetriever.js +7 -7
- package/dist/packlets/retrieve/tagRetriever.js.map +1 -1
- package/dist/packlets/retrieve/temporalRetrievers.js +23 -20
- package/dist/packlets/retrieve/temporalRetrievers.js.map +1 -1
- package/dist/packlets/store/coverage.js +6 -0
- package/dist/packlets/store/coverage.js.map +1 -0
- package/dist/packlets/store/fileTreeMemoryStore.js +234 -103
- package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
- package/dist/packlets/store/index.js +4 -0
- package/dist/packlets/store/index.js.map +1 -1
- package/dist/packlets/store/listSelection.js +36 -0
- package/dist/packlets/store/listSelection.js.map +1 -0
- package/dist/packlets/store/memoryStore.js +6 -0
- package/dist/packlets/store/memoryStore.js.map +1 -0
- package/dist/packlets/store/reconcile.js +6 -0
- package/dist/packlets/store/reconcile.js.map +1 -0
- package/dist/packlets/store/storeCoverage.js +102 -0
- package/dist/packlets/store/storeCoverage.js.map +1 -0
- package/dist/packlets/store/storeIdentity.js +62 -0
- package/dist/packlets/store/storeIdentity.js.map +1 -0
- package/dist/packlets/store/storeReconcile.js +122 -0
- package/dist/packlets/store/storeReconcile.js.map +1 -0
- package/dist/packlets/store/vectorMaintenance.js +116 -8
- package/dist/packlets/store/vectorMaintenance.js.map +1 -1
- package/dist/packlets/store/vectorRecordSource.js +44 -0
- package/dist/packlets/store/vectorRecordSource.js.map +1 -0
- package/dist/packlets/tools/memoryTools.js +25 -2
- package/dist/packlets/tools/memoryTools.js.map +1 -1
- package/dist/packlets/types/envelope.js +25 -0
- package/dist/packlets/types/envelope.js.map +1 -1
- package/dist/packlets/types/identityResolver.js +6 -0
- package/dist/packlets/types/identityResolver.js.map +1 -0
- package/dist/packlets/types/index.js +2 -0
- package/dist/packlets/types/index.js.map +1 -1
- package/dist/packlets/types/recordResolver.js +6 -0
- package/dist/packlets/types/recordResolver.js.map +1 -0
- package/dist/packlets/types/temporal.js.map +1 -1
- package/dist/packlets/vector/inMemoryCosineIndex.js +39 -18
- package/dist/packlets/vector/inMemoryCosineIndex.js.map +1 -1
- package/dist/packlets/vector/inMemoryFragmentCosineIndex.js +109 -14
- package/dist/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
- package/dist/packlets/vector/rebuildHelpers.js +38 -0
- package/dist/packlets/vector/rebuildHelpers.js.map +1 -0
- package/dist/packlets/vector/vectorIndex.js.map +1 -1
- package/dist/ts-agent-memory.d.ts +1226 -114
- package/lib/packlets/index/memoryIndex.d.ts +118 -27
- package/lib/packlets/index/memoryIndex.d.ts.map +1 -1
- package/lib/packlets/index/memoryIndex.js +23 -16
- package/lib/packlets/index/memoryIndex.js.map +1 -1
- package/lib/packlets/ingest/orchestrator.d.ts.map +1 -1
- package/lib/packlets/ingest/orchestrator.js +13 -1
- package/lib/packlets/ingest/orchestrator.js.map +1 -1
- package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts +79 -1
- package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/fragmentSemanticRetriever.js +94 -5
- package/lib/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -1
- package/lib/packlets/retrieve/linkTraversalRetriever.d.ts +3 -10
- package/lib/packlets/retrieve/linkTraversalRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/linkTraversalRetriever.js +11 -25
- package/lib/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
- package/lib/packlets/retrieve/recencyRetriever.d.ts +3 -3
- package/lib/packlets/retrieve/recencyRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/recencyRetriever.js +6 -6
- package/lib/packlets/retrieve/recencyRetriever.js.map +1 -1
- package/lib/packlets/retrieve/retriever.d.ts +88 -7
- package/lib/packlets/retrieve/retriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/retriever.js +94 -9
- package/lib/packlets/retrieve/retriever.js.map +1 -1
- package/lib/packlets/retrieve/semanticRetriever.d.ts +3 -5
- package/lib/packlets/retrieve/semanticRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/semanticRetriever.js +15 -15
- package/lib/packlets/retrieve/semanticRetriever.js.map +1 -1
- package/lib/packlets/retrieve/structuredFilterRetriever.d.ts +3 -3
- package/lib/packlets/retrieve/structuredFilterRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/structuredFilterRetriever.js +6 -6
- package/lib/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
- package/lib/packlets/retrieve/tagRetriever.d.ts +3 -3
- package/lib/packlets/retrieve/tagRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/tagRetriever.js +6 -6
- package/lib/packlets/retrieve/tagRetriever.js.map +1 -1
- package/lib/packlets/retrieve/temporalRetrievers.d.ts +7 -5
- package/lib/packlets/retrieve/temporalRetrievers.d.ts.map +1 -1
- package/lib/packlets/retrieve/temporalRetrievers.js +22 -19
- package/lib/packlets/retrieve/temporalRetrievers.js.map +1 -1
- package/lib/packlets/store/coverage.d.ts +102 -0
- package/lib/packlets/store/coverage.d.ts.map +1 -0
- package/lib/packlets/store/coverage.js +7 -0
- package/lib/packlets/store/coverage.js.map +1 -0
- package/lib/packlets/store/fileTreeMemoryStore.d.ts +56 -167
- package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
- package/lib/packlets/store/fileTreeMemoryStore.js +234 -103
- package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
- package/lib/packlets/store/index.d.ts +4 -0
- package/lib/packlets/store/index.d.ts.map +1 -1
- package/lib/packlets/store/index.js +4 -0
- package/lib/packlets/store/index.js.map +1 -1
- package/lib/packlets/store/listSelection.d.ts +101 -0
- package/lib/packlets/store/listSelection.d.ts.map +1 -0
- package/lib/packlets/store/listSelection.js +40 -0
- package/lib/packlets/store/listSelection.js.map +1 -0
- package/lib/packlets/store/memoryStore.d.ts +237 -0
- package/lib/packlets/store/memoryStore.d.ts.map +1 -0
- package/lib/packlets/store/memoryStore.js +7 -0
- package/lib/packlets/store/memoryStore.js.map +1 -0
- package/lib/packlets/store/reconcile.d.ts +82 -0
- package/lib/packlets/store/reconcile.d.ts.map +1 -0
- package/lib/packlets/store/reconcile.js +7 -0
- package/lib/packlets/store/reconcile.js.map +1 -0
- package/lib/packlets/store/storeCoverage.d.ts +45 -0
- package/lib/packlets/store/storeCoverage.d.ts.map +1 -0
- package/lib/packlets/store/storeCoverage.js +105 -0
- package/lib/packlets/store/storeCoverage.js.map +1 -0
- package/lib/packlets/store/storeIdentity.d.ts +38 -0
- package/lib/packlets/store/storeIdentity.d.ts.map +1 -0
- package/lib/packlets/store/storeIdentity.js +67 -0
- package/lib/packlets/store/storeIdentity.js.map +1 -0
- package/lib/packlets/store/storeReconcile.d.ts +41 -0
- package/lib/packlets/store/storeReconcile.d.ts.map +1 -0
- package/lib/packlets/store/storeReconcile.js +125 -0
- package/lib/packlets/store/storeReconcile.js.map +1 -0
- package/lib/packlets/store/vectorMaintenance.d.ts +74 -0
- package/lib/packlets/store/vectorMaintenance.d.ts.map +1 -1
- package/lib/packlets/store/vectorMaintenance.js +117 -8
- package/lib/packlets/store/vectorMaintenance.js.map +1 -1
- package/lib/packlets/store/vectorRecordSource.d.ts +36 -0
- package/lib/packlets/store/vectorRecordSource.d.ts.map +1 -0
- package/lib/packlets/store/vectorRecordSource.js +47 -0
- package/lib/packlets/store/vectorRecordSource.js.map +1 -0
- package/lib/packlets/tools/memoryTools.d.ts.map +1 -1
- package/lib/packlets/tools/memoryTools.js +25 -2
- package/lib/packlets/tools/memoryTools.js.map +1 -1
- package/lib/packlets/types/envelope.d.ts +24 -2
- package/lib/packlets/types/envelope.d.ts.map +1 -1
- package/lib/packlets/types/envelope.js +26 -0
- package/lib/packlets/types/envelope.js.map +1 -1
- package/lib/packlets/types/identityResolver.d.ts +42 -0
- package/lib/packlets/types/identityResolver.d.ts.map +1 -0
- package/lib/packlets/types/identityResolver.js +7 -0
- package/lib/packlets/types/identityResolver.js.map +1 -0
- package/lib/packlets/types/index.d.ts +2 -0
- package/lib/packlets/types/index.d.ts.map +1 -1
- package/lib/packlets/types/index.js +2 -0
- package/lib/packlets/types/index.js.map +1 -1
- package/lib/packlets/types/recordResolver.d.ts +39 -0
- package/lib/packlets/types/recordResolver.d.ts.map +1 -0
- package/lib/packlets/types/recordResolver.js +7 -0
- package/lib/packlets/types/recordResolver.js.map +1 -0
- package/lib/packlets/types/temporal.d.ts +26 -6
- package/lib/packlets/types/temporal.d.ts.map +1 -1
- package/lib/packlets/types/temporal.js.map +1 -1
- package/lib/packlets/vector/inMemoryCosineIndex.d.ts +9 -2
- package/lib/packlets/vector/inMemoryCosineIndex.d.ts.map +1 -1
- package/lib/packlets/vector/inMemoryCosineIndex.js +40 -19
- package/lib/packlets/vector/inMemoryCosineIndex.js.map +1 -1
- package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts +18 -4
- package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts.map +1 -1
- package/lib/packlets/vector/inMemoryFragmentCosineIndex.js +108 -13
- package/lib/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
- package/lib/packlets/vector/rebuildHelpers.d.ts +30 -0
- package/lib/packlets/vector/rebuildHelpers.d.ts.map +1 -0
- package/lib/packlets/vector/rebuildHelpers.js +42 -0
- package/lib/packlets/vector/rebuildHelpers.js.map +1 -0
- package/lib/packlets/vector/vectorIndex.d.ts +326 -20
- package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
- package/lib/packlets/vector/vectorIndex.js.map +1 -1
- package/package.json +7 -7
|
@@ -7,8 +7,13 @@ import { FileTree } from '@fgv/ts-json-base';
|
|
|
7
7
|
import { DEFAULT_DEDUP_SCOPE, KnowledgeLwwPolicy, isTemporalIdentityCodec, isTemporalRecord, isVersionCurrent, selectCurrentVersion, selectVersionAsOf } from '../types';
|
|
8
8
|
import { parseMemoryFile, serializeMemoryFile, splitFrontmatter } from '../converters';
|
|
9
9
|
import { VectorMaintenance } from './vectorMaintenance';
|
|
10
|
+
import { computeCoverage } from './storeCoverage';
|
|
11
|
+
import { codecFor, resolveIdentity, verifyLoadedIdentity } from './storeIdentity';
|
|
12
|
+
import { reconcileVectors } from './storeReconcile';
|
|
10
13
|
import { MemoryIndex } from '../index';
|
|
11
14
|
import { defaultMemoryScopeEncoding } from './scopeEncoding';
|
|
15
|
+
import { isWholeVaultScan } from './listSelection';
|
|
16
|
+
import { vectorRecordSource } from './vectorRecordSource';
|
|
12
17
|
/** The on-disk extension for a memory record file. */
|
|
13
18
|
const MEMORY_FILE_EXTENSION = '.md';
|
|
14
19
|
/**
|
|
@@ -115,7 +120,7 @@ export class FileTreeMemoryStore {
|
|
|
115
120
|
/** {@inheritDoc IMemoryStore.get} */
|
|
116
121
|
async get(kind, entityId) {
|
|
117
122
|
var _a;
|
|
118
|
-
const result = this.
|
|
123
|
+
const result = codecFor(this._codecs, this._defaultCodec, kind).onSuccess((codec) => codec.encode(entityId).onSuccess((addr) => {
|
|
119
124
|
if (addr.isVersioned) {
|
|
120
125
|
if (!isTemporalIdentityCodec(codec)) {
|
|
121
126
|
return fail(`memory get '${entityId}': codec for versioned kind '${kind}' does not implement the temporal codec interface`);
|
|
@@ -124,7 +129,7 @@ export class FileTreeMemoryStore {
|
|
|
124
129
|
// version whose `invalid_at` is null/absent) from the entity subtree,
|
|
125
130
|
// read off the derived index. `asOf` resolution is via `list({ asOf })`
|
|
126
131
|
// and the temporal retrievers.
|
|
127
|
-
return
|
|
132
|
+
return this._readVersionedCurrent(addr.scope);
|
|
128
133
|
}
|
|
129
134
|
return this._readRecord(addr.scope, addr.idStem);
|
|
130
135
|
}));
|
|
@@ -140,50 +145,137 @@ export class FileTreeMemoryStore {
|
|
|
140
145
|
return this._readRecord(scope, id);
|
|
141
146
|
}
|
|
142
147
|
/** {@inheritDoc IMemoryStore.list} */
|
|
143
|
-
async list(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
+
async list(selection) {
|
|
149
|
+
// Deliberately NOT annotated `: boolean`. An explicit annotation discards the
|
|
150
|
+
// type predicate, and TypeScript's aliased-condition narrowing is what lets
|
|
151
|
+
// the ternary below see `selection` as a filter on the false branch. With the
|
|
152
|
+
// annotation this line compiles and the next one does not.
|
|
153
|
+
const scan = isWholeVaultScan(selection);
|
|
154
|
+
const filter = scan ? {} : selection;
|
|
155
|
+
if (!scan && filter.scope === undefined && filter.kind === undefined && filter.tag === undefined) {
|
|
156
|
+
// `asOf` alone does not narrow — it collapses versions, it does not exclude
|
|
157
|
+
// entities — so a selection carrying only `asOf` lands here too.
|
|
158
|
+
return fail('memory list: a selection must narrow by scope, kind or tag; ' +
|
|
159
|
+
'pass scanEveryRecord() to read every record in the vault deliberately');
|
|
160
|
+
}
|
|
161
|
+
// Select over envelopes, project temporally, and materialize LAST — so a
|
|
162
|
+
// narrowed list reads only the files it is going to return.
|
|
163
|
+
const selected = this._index.entries().filter((entry) => {
|
|
164
|
+
if (filter.scope !== undefined && entry.scope !== filter.scope) {
|
|
148
165
|
return false;
|
|
149
166
|
}
|
|
150
|
-
if (
|
|
167
|
+
if (filter.kind !== undefined && entry.envelope.kind !== filter.kind) {
|
|
151
168
|
return false;
|
|
152
169
|
}
|
|
153
|
-
if (
|
|
170
|
+
if (filter.tag !== undefined && !entry.envelope.tags.includes(filter.tag)) {
|
|
154
171
|
return false;
|
|
155
172
|
}
|
|
156
173
|
return true;
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
174
|
+
});
|
|
175
|
+
const projected = selection.asOf === undefined
|
|
176
|
+
? // No temporal projection requested: the flat-path guarantee — every
|
|
177
|
+
// version is returned.
|
|
178
|
+
selected
|
|
179
|
+
: FileTreeMemoryStore._projectAsOf(selected, selection.asOf);
|
|
180
|
+
return this._materialize(projected);
|
|
181
|
+
}
|
|
182
|
+
/** {@inheritDoc IMemoryStore.coverage} */
|
|
183
|
+
coverage() {
|
|
184
|
+
return Promise.resolve(computeCoverage({
|
|
185
|
+
entries: this._index.entries(),
|
|
186
|
+
hasRankProjector: (kind) => this._rankProjectors.has(kind),
|
|
187
|
+
anyRankProjector: this._rankProjectors.size > 0,
|
|
188
|
+
embedsKind: (kind) => this.embedsKind(kind),
|
|
189
|
+
vectorIndex: this._vectors.vectorIndex,
|
|
190
|
+
fragmentIndex: this._vectors.fragmentIndex
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
/** {@inheritDoc IMemoryStore.listEntries} */
|
|
194
|
+
async listEntries() {
|
|
195
|
+
return succeed(this._index.entries());
|
|
165
196
|
}
|
|
166
197
|
/** {@inheritDoc IMemoryStore.listScoped} */
|
|
167
198
|
async listScoped() {
|
|
168
|
-
// The derived index already carries each record's scope
|
|
169
|
-
// (
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
199
|
+
// The derived index already carries each record's scope, so the scoped
|
|
200
|
+
// projection keys straight off it — `(scope, id)` is exactly the address the
|
|
201
|
+
// vector index uses. Bodies ARE materialized here: this seam feeds an
|
|
202
|
+
// embedder, which is the one consumer that genuinely needs every body. No
|
|
203
|
+
// filter/temporal projection — it re-embeds the whole vault by contract.
|
|
204
|
+
//
|
|
205
|
+
// **Complete or failed — never quietly short.** This call is the sole feed
|
|
206
|
+
// for `asRecordSource()`, and therefore for `IVectorIndex.rebuild`'s coverage
|
|
207
|
+
// report. A record dropped here would land in none of `records` / `excluded`
|
|
208
|
+
// / `indexed` / `declined` / `skipped`, so a caller computing coverage would
|
|
209
|
+
// undercount *in the direction of looking healthier* — which is precisely the
|
|
210
|
+
// failure `vectorRecordSource`'s own tally exists to prevent, reintroduced one
|
|
211
|
+
// layer down. A loud failure is recoverable; a silent undercount is not.
|
|
212
|
+
//
|
|
213
|
+
// Two consequences worth stating rather than discovering. Before the index
|
|
214
|
+
// was projected this method read nothing and could not fail, because the
|
|
215
|
+
// index held the records themselves; it now costs a full-vault read and is
|
|
216
|
+
// fallible. And it is NOT write-locked, so a concurrent `put` whose cap-cull
|
|
217
|
+
// physically evicts a record between the snapshot and the read will fail it.
|
|
218
|
+
// That window is real and is tracked in `docs/FUTURE.md` — the answer is to
|
|
219
|
+
// make the loss *countable*, not to swallow it.
|
|
220
|
+
return mapResults(this._index.entries().map((entry) => {
|
|
221
|
+
const target = { scope: entry.scope, id: entry.envelope.id };
|
|
222
|
+
return this._resolveRequired(entry).onSuccess((record) => succeed({ target, record }));
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
/** {@inheritDoc IMemoryRecordResolver.resolveRecord} */
|
|
226
|
+
resolveRecord(scope, id) {
|
|
227
|
+
return this._readRecord(scope, id);
|
|
228
|
+
}
|
|
229
|
+
_codec(kind) {
|
|
230
|
+
return codecFor(this._codecs, this._defaultCodec, kind);
|
|
231
|
+
}
|
|
232
|
+
/** {@inheritDoc IIdentityResolver.resolveIdentity} */
|
|
233
|
+
resolveIdentity(kind, entityId) {
|
|
234
|
+
return resolveIdentity(this._codecs, this._defaultCodec, kind, entityId);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Materialize a selected set of entries into records, dropping any that have
|
|
238
|
+
* vanished since selection.
|
|
239
|
+
*
|
|
240
|
+
* @remarks
|
|
241
|
+
* A miss is not a failure. Selection reads the in-memory index and
|
|
242
|
+
* materialization reads storage, so a record deleted in between is a legitimate
|
|
243
|
+
* race and yields a shorter list rather than an error. A read that FAILS is a
|
|
244
|
+
* real fault and propagates.
|
|
245
|
+
*/
|
|
246
|
+
_materialize(entries) {
|
|
247
|
+
return mapResults(entries.map((entry) => this._readRecord(entry.scope, entry.envelope.id))).onSuccess((records) => succeed(records.filter((r) => r !== undefined)));
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Materialize one entry, treating "gone" as a fault rather than a miss.
|
|
251
|
+
*
|
|
252
|
+
* @remarks
|
|
253
|
+
* For paths where a vanished record really does mean the index and the vault
|
|
254
|
+
* disagree, rather than that something legitimately removed it in between.
|
|
255
|
+
*
|
|
256
|
+
* Three of the four callers hold the write lock, so nothing can have removed
|
|
257
|
+
* the record since the entry was read. `get()`'s versioned path does not, and
|
|
258
|
+
* is safe only because temporal kinds never physically delete a version — they
|
|
259
|
+
* invalidate in place, and cap-cull does not apply to them. **If eviction is
|
|
260
|
+
* ever added to the temporal path, that caller must change**, or it
|
|
261
|
+
* reintroduces the race this method exists to detect.
|
|
262
|
+
*
|
|
263
|
+
* `listScoped` also does not hold the lock, and uses this deliberately anyway:
|
|
264
|
+
* it feeds a coverage report, so a silent drop there is worse than a loud
|
|
265
|
+
* failure. See its comment, and `docs/FUTURE.md` for the eviction window.
|
|
266
|
+
*
|
|
267
|
+
* The drop-tolerant counterpart is {@link FileTreeMemoryStore._materialize},
|
|
268
|
+
* for readers where a record that vanished between selection and
|
|
269
|
+
* materialization is a miss rather than a fault.
|
|
270
|
+
*/
|
|
271
|
+
_resolveRequired(entry) {
|
|
272
|
+
return this._readRecord(entry.scope, entry.envelope.id).onSuccess((record) => record === undefined
|
|
273
|
+
? fail(`memory: index entry '${entry.scope}/${entry.envelope.id}' has no record in the vault`)
|
|
274
|
+
: succeed(record));
|
|
176
275
|
}
|
|
177
276
|
/** {@inheritDoc IMemoryStore.asRecordSource} */
|
|
178
277
|
asRecordSource() {
|
|
179
|
-
return
|
|
180
|
-
// Filtered to the kinds that participate in the record vector index. This
|
|
181
|
-
// source exists to drive `IVectorIndex` rebuilds, so a kind excluded from the
|
|
182
|
-
// index has no business being re-embedded on open — which is where the cost
|
|
183
|
-
// is worst, since a rebuild embeds the whole vault serially. With no
|
|
184
|
-
// `embedKinds` declaration every kind passes and this is the identity filter.
|
|
185
|
-
list: async () => (await this.listScoped()).onSuccess((scoped) => succeed(scoped.filter((s) => this.embedsKind(s.record.envelope.kind))))
|
|
186
|
-
};
|
|
278
|
+
return vectorRecordSource(this);
|
|
187
279
|
}
|
|
188
280
|
/**
|
|
189
281
|
* Collapse temporal records to the single version valid at `asOf` per entity;
|
|
@@ -192,21 +284,21 @@ export class FileTreeMemoryStore {
|
|
|
192
284
|
* filtering is deferred — OQ-9). An entity with no version valid at `asOf`
|
|
193
285
|
* contributes nothing.
|
|
194
286
|
*/
|
|
195
|
-
static _projectAsOf(
|
|
287
|
+
static _projectAsOf(entries, asOf) {
|
|
196
288
|
const passthrough = [];
|
|
197
289
|
const groups = new Map();
|
|
198
|
-
for (const
|
|
199
|
-
if (!isTemporalRecord(
|
|
200
|
-
passthrough.push(
|
|
290
|
+
for (const entry of entries) {
|
|
291
|
+
if (!isTemporalRecord(entry)) {
|
|
292
|
+
passthrough.push(entry);
|
|
201
293
|
continue;
|
|
202
294
|
}
|
|
203
|
-
const key = `${
|
|
295
|
+
const key = `${entry.envelope.kind}\0${entry.envelope.entityId}`;
|
|
204
296
|
const existing = groups.get(key);
|
|
205
297
|
if (existing === undefined) {
|
|
206
|
-
groups.set(key, [
|
|
298
|
+
groups.set(key, [entry]);
|
|
207
299
|
}
|
|
208
300
|
else {
|
|
209
|
-
existing.push(
|
|
301
|
+
existing.push(entry);
|
|
210
302
|
}
|
|
211
303
|
}
|
|
212
304
|
const result = [...passthrough];
|
|
@@ -218,9 +310,9 @@ export class FileTreeMemoryStore {
|
|
|
218
310
|
}
|
|
219
311
|
return result;
|
|
220
312
|
}
|
|
221
|
-
/** {@inheritDoc IMemoryStore.
|
|
222
|
-
async
|
|
223
|
-
return this._enqueue(() => this.
|
|
313
|
+
/** {@inheritDoc IMemoryStore.reconcile} */
|
|
314
|
+
async reconcile(kind, artifact) {
|
|
315
|
+
return this._enqueue(() => this._reconcileLocked(kind, artifact));
|
|
224
316
|
}
|
|
225
317
|
/** {@inheritDoc IMemoryStore.put} */
|
|
226
318
|
async put(record) {
|
|
@@ -330,7 +422,7 @@ export class FileTreeMemoryStore {
|
|
|
330
422
|
}
|
|
331
423
|
/** Resolve a scope for an observation, best-effort (undefined when unresolvable). */
|
|
332
424
|
_scopeBestEffort(kind, entityId) {
|
|
333
|
-
return this.
|
|
425
|
+
return this._codec(kind)
|
|
334
426
|
.onSuccess((codec) => codec.encode(entityId))
|
|
335
427
|
.onSuccess((addr) => succeed(addr.scope))
|
|
336
428
|
.orDefault();
|
|
@@ -348,7 +440,7 @@ export class FileTreeMemoryStore {
|
|
|
348
440
|
return this._registry
|
|
349
441
|
.convert(envelope.kind, body)
|
|
350
442
|
.withErrorFormat((msg) => `memory put '${envelope.id}': invalid body: ${msg}`)
|
|
351
|
-
.onSuccess(() => this.
|
|
443
|
+
.onSuccess(() => this._codec(envelope.kind))
|
|
352
444
|
.thenOnSuccess((codec) => codec.encode(envelope.entityId).thenOnSuccess((addr) => {
|
|
353
445
|
if (addr.isVersioned) {
|
|
354
446
|
if (!isTemporalIdentityCodec(codec)) {
|
|
@@ -391,8 +483,11 @@ export class FileTreeMemoryStore {
|
|
|
391
483
|
// mutableFields) actually applies. Content-dedup must never shadow LWW for
|
|
392
484
|
// the same entity.
|
|
393
485
|
const duplicate = this._findByContentHash(scope, hash, record.envelope.id);
|
|
394
|
-
if (duplicate
|
|
395
|
-
return
|
|
486
|
+
if (duplicate.isFailure()) {
|
|
487
|
+
return fail(duplicate.message);
|
|
488
|
+
}
|
|
489
|
+
if (duplicate.value !== undefined) {
|
|
490
|
+
return succeed({ record: duplicate.value, evicted: [] });
|
|
396
491
|
}
|
|
397
492
|
}
|
|
398
493
|
return this._readRecord(scope, idStem).thenOnSuccess((existing) => {
|
|
@@ -413,9 +508,8 @@ export class FileTreeMemoryStore {
|
|
|
413
508
|
// (a replace, not a grow). The same-id `existing` record is threaded
|
|
414
509
|
// separately into `_buildRecord` for the merge-patch. Knowledge LWW ignores
|
|
415
510
|
// this argument, so its behavior is unchanged by the wider cohort.
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
.admit(record, cohort)
|
|
511
|
+
return this._admissionCohort(scope, record.envelope.kind, idStem)
|
|
512
|
+
.onSuccess((cohort) => policy.admit(record, cohort))
|
|
419
513
|
.thenOnSuccess((decision) => this._admitWrite(record, body, scope, idStem, hash, policy, existing, decision));
|
|
420
514
|
});
|
|
421
515
|
}
|
|
@@ -527,7 +621,7 @@ export class FileTreeMemoryStore {
|
|
|
527
621
|
.onSuccess(() => succeed(record));
|
|
528
622
|
}
|
|
529
623
|
async _deleteLocked(kind, entityId) {
|
|
530
|
-
return this.
|
|
624
|
+
return this._codec(kind).thenOnSuccess((codec) => codec.encode(entityId).thenOnSuccess((addr) => {
|
|
531
625
|
if (addr.isVersioned) {
|
|
532
626
|
if (!isTemporalIdentityCodec(codec)) {
|
|
533
627
|
return Promise.resolve(fail(`memory delete '${entityId}': codec for versioned kind '${kind}' does not implement the temporal codec interface`));
|
|
@@ -565,7 +659,16 @@ export class FileTreeMemoryStore {
|
|
|
565
659
|
* written, or fully invalidated / soft-deleted).
|
|
566
660
|
*/
|
|
567
661
|
_readVersionedCurrent(scope) {
|
|
568
|
-
|
|
662
|
+
// Select over ENVELOPES, then materialize the one winner — which is what
|
|
663
|
+
// `IMemoryStore.get`'s docstring promises and what `selectCurrentVersion`
|
|
664
|
+
// being generic over `IEnvelopeCarrier` exists for. Materializing every
|
|
665
|
+
// version first (as this did) cost N file reads and N body validations to
|
|
666
|
+
// return one record, and made that docstring false.
|
|
667
|
+
const current = selectCurrentVersion(this._index.entries().filter((entry) => entry.scope === scope));
|
|
668
|
+
if (current === undefined) {
|
|
669
|
+
return succeed(undefined);
|
|
670
|
+
}
|
|
671
|
+
return this._resolveRequired(current);
|
|
569
672
|
}
|
|
570
673
|
/**
|
|
571
674
|
* Every persisted version of the entity whose subtree is `scope`. All version
|
|
@@ -573,10 +676,13 @@ export class FileTreeMemoryStore {
|
|
|
573
676
|
* entityId), so a scope filter over the index isolates one entity's versions.
|
|
574
677
|
*/
|
|
575
678
|
_versionsForEntity(scope) {
|
|
576
|
-
|
|
679
|
+
// Selected on the envelope (scope), materialized after — so a versioned write
|
|
680
|
+
// reads only that entity's versions, never the vault. Bounded by the entity's
|
|
681
|
+
// version count.
|
|
682
|
+
return mapResults(this._index
|
|
577
683
|
.entries()
|
|
578
684
|
.filter((entry) => entry.scope === scope)
|
|
579
|
-
.map((entry) => entry
|
|
685
|
+
.map((entry) => this._resolveRequired(entry)));
|
|
580
686
|
}
|
|
581
687
|
/**
|
|
582
688
|
* Versioned write (invalidate-don't-delete). Builds the new version's content
|
|
@@ -598,7 +704,11 @@ export class FileTreeMemoryStore {
|
|
|
598
704
|
// still-current version at snapshot time — normally one, but two-or-more if a
|
|
599
705
|
// prior invalidation partially failed; invalidating all of them lets the write
|
|
600
706
|
// self-heal a stuck state (P2-7).
|
|
601
|
-
const
|
|
707
|
+
const snapshot = this._versionsForEntity(scope);
|
|
708
|
+
if (snapshot.isFailure()) {
|
|
709
|
+
return fail(snapshot.message);
|
|
710
|
+
}
|
|
711
|
+
const versions = snapshot.value;
|
|
602
712
|
const priorCurrents = versions.filter(isVersionCurrent);
|
|
603
713
|
const current = selectCurrentVersion(versions);
|
|
604
714
|
const policy = this._policyFor(kind);
|
|
@@ -724,7 +834,11 @@ export class FileTreeMemoryStore {
|
|
|
724
834
|
* builds on it), so a hard delete would defeat the purpose.
|
|
725
835
|
*/
|
|
726
836
|
async _deleteVersioned(entityId, scope) {
|
|
727
|
-
const
|
|
837
|
+
const snapshot = this._versionsForEntity(scope);
|
|
838
|
+
if (snapshot.isFailure()) {
|
|
839
|
+
return fail(snapshot.message);
|
|
840
|
+
}
|
|
841
|
+
const versions = snapshot.value;
|
|
728
842
|
const currents = versions.filter(isVersionCurrent);
|
|
729
843
|
const current = selectCurrentVersion(versions);
|
|
730
844
|
if (current === undefined) {
|
|
@@ -771,10 +885,12 @@ export class FileTreeMemoryStore {
|
|
|
771
885
|
* across first-writes and updates.
|
|
772
886
|
*/
|
|
773
887
|
_admissionCohort(scope, kind, idStem) {
|
|
774
|
-
|
|
888
|
+
// Envelope-only selection; the cohort is per-(scope, kind) and bounded by the
|
|
889
|
+
// cull cap, so materializing it is small by construction.
|
|
890
|
+
return mapResults(this._index
|
|
775
891
|
.entries()
|
|
776
|
-
.filter((entry) => entry.scope === scope && entry.
|
|
777
|
-
.map((entry) => entry
|
|
892
|
+
.filter((entry) => entry.scope === scope && entry.envelope.kind === kind && entry.envelope.id !== idStem)
|
|
893
|
+
.map((entry) => this._resolveRequired(entry)));
|
|
778
894
|
}
|
|
779
895
|
/**
|
|
780
896
|
* Find a record in `scope` whose `contentHash` equals `hash`, if any,
|
|
@@ -782,12 +898,13 @@ export class FileTreeMemoryStore {
|
|
|
782
898
|
* dedup skip the same-id record so it does not shadow the LWW update path.
|
|
783
899
|
*/
|
|
784
900
|
_findByContentHash(scope, hash, excludeId) {
|
|
901
|
+
// The clearest win of the projection: the hash lives on the envelope, so this
|
|
902
|
+
// scans envelopes and reads exactly ONE file — the match — where it used to
|
|
903
|
+
// hold every body in the scope to look at one field.
|
|
785
904
|
const match = this._index
|
|
786
905
|
.entries()
|
|
787
|
-
.find((entry) => entry.scope === scope &&
|
|
788
|
-
|
|
789
|
-
entry.record.envelope.id !== excludeId);
|
|
790
|
-
return match === null || match === void 0 ? void 0 : match.record;
|
|
906
|
+
.find((entry) => entry.scope === scope && entry.envelope.contentHash === hash && entry.envelope.id !== excludeId);
|
|
907
|
+
return match === undefined ? succeed(undefined) : this._resolveRequired(match);
|
|
791
908
|
}
|
|
792
909
|
/**
|
|
793
910
|
* True when `incoming`'s caller-authored mutable metadata (`tags` / `provenance`)
|
|
@@ -813,7 +930,7 @@ export class FileTreeMemoryStore {
|
|
|
813
930
|
return this._hasher.computeHash({ kind, body, links });
|
|
814
931
|
}
|
|
815
932
|
/**
|
|
816
|
-
* The
|
|
933
|
+
* The rank branch of {@link FileTreeMemoryStore.reconcile}, under the write lock.
|
|
817
934
|
*
|
|
818
935
|
* @remarks
|
|
819
936
|
* Re-reads each record's file rather than trusting the in-memory index, for
|
|
@@ -842,31 +959,54 @@ export class FileTreeMemoryStore {
|
|
|
842
959
|
* another on a reconcile. `_stampRank` itself is reused verbatim, which also
|
|
843
960
|
* inherits its throw semantics (logged at `warn`, `rank` cleared).
|
|
844
961
|
*/
|
|
845
|
-
async
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
}
|
|
962
|
+
async _reconcileLocked(kind, artifact) {
|
|
963
|
+
// Envelope-only selection: the walk needs `(scope, id)` and the kind, and
|
|
964
|
+
// each branch materializes only the records it decides to repair.
|
|
849
965
|
const targets = this._index
|
|
850
966
|
.entries()
|
|
851
|
-
.filter((entry) => entry.
|
|
852
|
-
|
|
967
|
+
.filter((entry) => entry.envelope.kind === kind);
|
|
968
|
+
if (artifact === 'rank') {
|
|
969
|
+
return this._reconcileRankLocked(kind, targets);
|
|
970
|
+
}
|
|
971
|
+
return reconcileVectors({
|
|
972
|
+
kind,
|
|
973
|
+
artifact,
|
|
974
|
+
targets,
|
|
975
|
+
maintenance: this._vectors,
|
|
976
|
+
embedsKind: (k) => this.embedsKind(k),
|
|
977
|
+
resolve: (scope, id) => this._readRecord(scope, id),
|
|
978
|
+
stampRef: (scope, id, ref) => this._rewriteEnvelope(scope, id, (r) => r.envelope.embeddingRef === ref
|
|
979
|
+
? undefined
|
|
980
|
+
: { envelope: Object.assign(Object.assign({}, r.envelope), { embeddingRef: ref }), body: r.body })
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
/** The rank branch of {@link FileTreeMemoryStore._reconcileLocked}. */
|
|
984
|
+
async _reconcileRankLocked(kind, targets) {
|
|
985
|
+
if (!this._rankProjectors.has(kind)) {
|
|
986
|
+
return fail(`memory reconcile '${kind}' rank: no rank projector is registered for this kind`);
|
|
987
|
+
}
|
|
988
|
+
// No materialization at all: `_rewriteEnvelope` re-reads each record itself.
|
|
989
|
+
let repaired = 0;
|
|
853
990
|
for (const target of targets) {
|
|
854
|
-
const applied = this.
|
|
991
|
+
const applied = this._rewriteEnvelope(target.scope, target.envelope.id, (r) => {
|
|
992
|
+
const stamped = this._stampRank(r);
|
|
993
|
+
return stamped.envelope.rank === r.envelope.rank ? undefined : stamped;
|
|
994
|
+
});
|
|
855
995
|
if (applied.isFailure()) {
|
|
856
|
-
return fail(`memory
|
|
996
|
+
return fail(`memory reconcile '${kind}' rank: ${applied.message}`);
|
|
857
997
|
}
|
|
858
998
|
if (applied.value) {
|
|
859
|
-
|
|
999
|
+
repaired++;
|
|
860
1000
|
}
|
|
861
1001
|
}
|
|
862
|
-
return succeed(
|
|
1002
|
+
return succeed({ artifact: 'rank', kind, examined: targets.length, repaired, failed: [] });
|
|
863
1003
|
}
|
|
864
1004
|
/**
|
|
865
1005
|
* Re-apply the rank projector to one record on disk. Returns whether `rank`
|
|
866
1006
|
* actually changed — an unchanged rank writes nothing, so a reconcile over an
|
|
867
1007
|
* already-consistent store touches no files.
|
|
868
1008
|
*/
|
|
869
|
-
|
|
1009
|
+
_rewriteEnvelope(scope, id, mutate) {
|
|
870
1010
|
return this._resolveScopeDir(scope).onSuccess((scopeDir) => {
|
|
871
1011
|
/* c8 ignore next 3 - defensive: the scope dir exists for any indexed record */
|
|
872
1012
|
if (scopeDir === undefined) {
|
|
@@ -894,12 +1034,16 @@ export class FileTreeMemoryStore {
|
|
|
894
1034
|
.onSuccess((parsed) => splitFrontmatter(raw)
|
|
895
1035
|
.withErrorFormat((msg) => `'${id}': ${msg}`)
|
|
896
1036
|
.onSuccess((parts) => {
|
|
897
|
-
|
|
898
|
-
|
|
1037
|
+
// The mutator says "nothing to change" with `undefined` rather
|
|
1038
|
+
// than by returning an equal record: persisting unconditionally
|
|
1039
|
+
// would bump `updated` on every record of the kind, trading a
|
|
1040
|
+
// wrong value for a wrong timestamp, and a deep comparison here
|
|
1041
|
+
// would have to know which fields each caller touches.
|
|
1042
|
+
const stamped = mutate({
|
|
899
1043
|
envelope: parsed.envelope,
|
|
900
1044
|
body: parts.body
|
|
901
1045
|
});
|
|
902
|
-
if (stamped
|
|
1046
|
+
if (stamped === undefined) {
|
|
903
1047
|
return succeed(false);
|
|
904
1048
|
}
|
|
905
1049
|
return this._persist(stamped, scope, id).onSuccess(() => succeed(true));
|
|
@@ -938,14 +1082,6 @@ export class FileTreeMemoryStore {
|
|
|
938
1082
|
return { envelope: Object.assign(Object.assign({}, record.envelope), { rank: undefined }), body: record.body };
|
|
939
1083
|
}
|
|
940
1084
|
}
|
|
941
|
-
_codecFor(kind) {
|
|
942
|
-
var _a;
|
|
943
|
-
const codec = (_a = this._codecs.get(kind)) !== null && _a !== void 0 ? _a : this._defaultCodec;
|
|
944
|
-
if (codec === undefined) {
|
|
945
|
-
return fail(`no identity codec registered for kind '${kind}'`);
|
|
946
|
-
}
|
|
947
|
-
return succeed(codec);
|
|
948
|
-
}
|
|
949
1085
|
_policyFor(kind) {
|
|
950
1086
|
var _a;
|
|
951
1087
|
return (_a = this._writePolicies.get(kind)) !== null && _a !== void 0 ? _a : this._defaultPolicy;
|
|
@@ -994,18 +1130,7 @@ export class FileTreeMemoryStore {
|
|
|
994
1130
|
* verbatim downstream (e.g. merge-into re-addressing). Cross-check it here.
|
|
995
1131
|
*/
|
|
996
1132
|
_verifyLoaded(scope, file, record) {
|
|
997
|
-
|
|
998
|
-
return fail(`memory file '${file.absolutePath}': envelope id '${record.envelope.id}' does not match filename stem '${file.baseName}'`);
|
|
999
|
-
}
|
|
1000
|
-
return this._codecFor(record.envelope.kind)
|
|
1001
|
-
.onSuccess((codec) => codec.verifyRoundTrip(scope, file.baseName).onSuccess(() => codec.decode(scope, file.baseName)))
|
|
1002
|
-
.withErrorFormat((msg) => `memory file '${file.absolutePath}': ${msg}`)
|
|
1003
|
-
.onSuccess((decodedEntityId) => {
|
|
1004
|
-
if (decodedEntityId !== record.envelope.entityId) {
|
|
1005
|
-
return fail(`memory file '${file.absolutePath}': envelope entityId '${record.envelope.entityId}' does not match scope-derived entityId '${decodedEntityId}'`);
|
|
1006
|
-
}
|
|
1007
|
-
return succeed(record);
|
|
1008
|
-
});
|
|
1133
|
+
return verifyLoadedIdentity(this._codec(record.envelope.kind), scope, file, record);
|
|
1009
1134
|
}
|
|
1010
1135
|
/**
|
|
1011
1136
|
* Resolve the directory for a scope, returning `undefined` when it does not
|
|
@@ -1095,8 +1220,8 @@ export class FileTreeMemoryStore {
|
|
|
1095
1220
|
_initialIndex(onRecordError) {
|
|
1096
1221
|
return this._collectEntries(this._root, [], onRecordError).onSuccess((entries) => this._index.rebuild(entries).onSuccess(() => {
|
|
1097
1222
|
for (const entry of entries) {
|
|
1098
|
-
if (entry.
|
|
1099
|
-
this._seq = entry.
|
|
1223
|
+
if (entry.envelope.seq > this._seq) {
|
|
1224
|
+
this._seq = entry.envelope.seq;
|
|
1100
1225
|
}
|
|
1101
1226
|
}
|
|
1102
1227
|
return succeed(true);
|
|
@@ -1137,11 +1262,17 @@ export class FileTreeMemoryStore {
|
|
|
1137
1262
|
* failure passes through untouched so the historical error is byte-identical.
|
|
1138
1263
|
*/
|
|
1139
1264
|
_loadRecordFile(scope, child, onRecordError) {
|
|
1140
|
-
return child
|
|
1265
|
+
return (child
|
|
1141
1266
|
.getRawContents()
|
|
1142
1267
|
.onSuccess((raw) => parseMemoryFile(raw, this._registry))
|
|
1143
1268
|
.onSuccess((parsedRecord) => this._verifyLoaded(scope, child, parsedRecord))
|
|
1144
|
-
|
|
1269
|
+
// Parse → validate → PROJECT → discard, per file. The body is read and
|
|
1270
|
+
// fully validated (which is what gives `onRecordError` its meaning), then
|
|
1271
|
+
// dropped here rather than carried into the index. Peak body residency
|
|
1272
|
+
// across the whole open is therefore ONE record, not N — which is the
|
|
1273
|
+
// resident-memory moment this whole surface exists to fix, and it is why
|
|
1274
|
+
// the projection had to reach `rebuild` and not just the read methods.
|
|
1275
|
+
.onSuccess((verified) => succeed([{ scope, envelope: verified.envelope }]))
|
|
1145
1276
|
.onFailure((message) => {
|
|
1146
1277
|
if (onRecordError === 'skip') {
|
|
1147
1278
|
const path = `${scope}/${child.name}`;
|
|
@@ -1150,7 +1281,7 @@ export class FileTreeMemoryStore {
|
|
|
1150
1281
|
this._warnSwallowed(error);
|
|
1151
1282
|
}
|
|
1152
1283
|
return fail(message);
|
|
1153
|
-
});
|
|
1284
|
+
}));
|
|
1154
1285
|
}
|
|
1155
1286
|
}
|
|
1156
1287
|
/**
|