@fgv/ts-agent-memory 5.1.0-49 → 5.1.0-50
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/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 +221 -79
- 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/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/index.js +1 -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 +67 -12
- 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 +1035 -106
- 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/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 +53 -166
- package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
- package/lib/packlets/store/fileTreeMemoryStore.js +221 -79
- 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/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/index.d.ts +1 -0
- package/lib/packlets/types/index.d.ts.map +1 -1
- package/lib/packlets/types/index.js +1 -0
- package/lib/packlets/types/index.js.map +1 -1
- package/lib/packlets/types/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 +6 -3
- package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts.map +1 -1
- package/lib/packlets/vector/inMemoryFragmentCosineIndex.js +66 -11
- 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 +270 -15
- 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,12 @@ 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 { reconcileVectors } from './storeReconcile';
|
|
10
12
|
import { MemoryIndex } from '../index';
|
|
11
13
|
import { defaultMemoryScopeEncoding } from './scopeEncoding';
|
|
14
|
+
import { isWholeVaultScan } from './listSelection';
|
|
15
|
+
import { vectorRecordSource } from './vectorRecordSource';
|
|
12
16
|
/** The on-disk extension for a memory record file. */
|
|
13
17
|
const MEMORY_FILE_EXTENSION = '.md';
|
|
14
18
|
/**
|
|
@@ -124,7 +128,7 @@ export class FileTreeMemoryStore {
|
|
|
124
128
|
// version whose `invalid_at` is null/absent) from the entity subtree,
|
|
125
129
|
// read off the derived index. `asOf` resolution is via `list({ asOf })`
|
|
126
130
|
// and the temporal retrievers.
|
|
127
|
-
return
|
|
131
|
+
return this._readVersionedCurrent(addr.scope);
|
|
128
132
|
}
|
|
129
133
|
return this._readRecord(addr.scope, addr.idStem);
|
|
130
134
|
}));
|
|
@@ -140,50 +144,130 @@ export class FileTreeMemoryStore {
|
|
|
140
144
|
return this._readRecord(scope, id);
|
|
141
145
|
}
|
|
142
146
|
/** {@inheritDoc IMemoryStore.list} */
|
|
143
|
-
async list(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
async list(selection) {
|
|
148
|
+
// Deliberately NOT annotated `: boolean`. An explicit annotation discards the
|
|
149
|
+
// type predicate, and TypeScript's aliased-condition narrowing is what lets
|
|
150
|
+
// the ternary below see `selection` as a filter on the false branch. With the
|
|
151
|
+
// annotation this line compiles and the next one does not.
|
|
152
|
+
const scan = isWholeVaultScan(selection);
|
|
153
|
+
const filter = scan ? {} : selection;
|
|
154
|
+
if (!scan && filter.scope === undefined && filter.kind === undefined && filter.tag === undefined) {
|
|
155
|
+
// `asOf` alone does not narrow — it collapses versions, it does not exclude
|
|
156
|
+
// entities — so a selection carrying only `asOf` lands here too.
|
|
157
|
+
return fail('memory list: a selection must narrow by scope, kind or tag; ' +
|
|
158
|
+
'pass scanEveryRecord() to read every record in the vault deliberately');
|
|
159
|
+
}
|
|
160
|
+
// Select over envelopes, project temporally, and materialize LAST — so a
|
|
161
|
+
// narrowed list reads only the files it is going to return.
|
|
162
|
+
const selected = this._index.entries().filter((entry) => {
|
|
163
|
+
if (filter.scope !== undefined && entry.scope !== filter.scope) {
|
|
148
164
|
return false;
|
|
149
165
|
}
|
|
150
|
-
if (
|
|
166
|
+
if (filter.kind !== undefined && entry.envelope.kind !== filter.kind) {
|
|
151
167
|
return false;
|
|
152
168
|
}
|
|
153
|
-
if (
|
|
169
|
+
if (filter.tag !== undefined && !entry.envelope.tags.includes(filter.tag)) {
|
|
154
170
|
return false;
|
|
155
171
|
}
|
|
156
172
|
return true;
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
173
|
+
});
|
|
174
|
+
const projected = selection.asOf === undefined
|
|
175
|
+
? // No temporal projection requested: the flat-path guarantee — every
|
|
176
|
+
// version is returned.
|
|
177
|
+
selected
|
|
178
|
+
: FileTreeMemoryStore._projectAsOf(selected, selection.asOf);
|
|
179
|
+
return this._materialize(projected);
|
|
180
|
+
}
|
|
181
|
+
/** {@inheritDoc IMemoryStore.coverage} */
|
|
182
|
+
coverage() {
|
|
183
|
+
return Promise.resolve(computeCoverage({
|
|
184
|
+
entries: this._index.entries(),
|
|
185
|
+
hasRankProjector: (kind) => this._rankProjectors.has(kind),
|
|
186
|
+
anyRankProjector: this._rankProjectors.size > 0,
|
|
187
|
+
embedsKind: (kind) => this.embedsKind(kind),
|
|
188
|
+
vectorIndex: this._vectors.vectorIndex,
|
|
189
|
+
fragmentIndex: this._vectors.fragmentIndex
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
/** {@inheritDoc IMemoryStore.listEntries} */
|
|
193
|
+
async listEntries() {
|
|
194
|
+
return succeed(this._index.entries());
|
|
165
195
|
}
|
|
166
196
|
/** {@inheritDoc IMemoryStore.listScoped} */
|
|
167
197
|
async listScoped() {
|
|
168
|
-
// The derived index already carries each record's scope
|
|
169
|
-
// (
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
198
|
+
// The derived index already carries each record's scope, so the scoped
|
|
199
|
+
// projection keys straight off it — `(scope, id)` is exactly the address the
|
|
200
|
+
// vector index uses. Bodies ARE materialized here: this seam feeds an
|
|
201
|
+
// embedder, which is the one consumer that genuinely needs every body. No
|
|
202
|
+
// filter/temporal projection — it re-embeds the whole vault by contract.
|
|
203
|
+
//
|
|
204
|
+
// **Complete or failed — never quietly short.** This call is the sole feed
|
|
205
|
+
// for `asRecordSource()`, and therefore for `IVectorIndex.rebuild`'s coverage
|
|
206
|
+
// report. A record dropped here would land in none of `records` / `excluded`
|
|
207
|
+
// / `indexed` / `declined` / `skipped`, so a caller computing coverage would
|
|
208
|
+
// undercount *in the direction of looking healthier* — which is precisely the
|
|
209
|
+
// failure `vectorRecordSource`'s own tally exists to prevent, reintroduced one
|
|
210
|
+
// layer down. A loud failure is recoverable; a silent undercount is not.
|
|
211
|
+
//
|
|
212
|
+
// Two consequences worth stating rather than discovering. Before the index
|
|
213
|
+
// was projected this method read nothing and could not fail, because the
|
|
214
|
+
// index held the records themselves; it now costs a full-vault read and is
|
|
215
|
+
// fallible. And it is NOT write-locked, so a concurrent `put` whose cap-cull
|
|
216
|
+
// physically evicts a record between the snapshot and the read will fail it.
|
|
217
|
+
// That window is real and is tracked in `docs/FUTURE.md` — the answer is to
|
|
218
|
+
// make the loss *countable*, not to swallow it.
|
|
219
|
+
return mapResults(this._index.entries().map((entry) => {
|
|
220
|
+
const target = { scope: entry.scope, id: entry.envelope.id };
|
|
221
|
+
return this._resolveRequired(entry).onSuccess((record) => succeed({ target, record }));
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
/** {@inheritDoc IMemoryRecordResolver.resolveRecord} */
|
|
225
|
+
resolveRecord(scope, id) {
|
|
226
|
+
return this._readRecord(scope, id);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Materialize a selected set of entries into records, dropping any that have
|
|
230
|
+
* vanished since selection.
|
|
231
|
+
*
|
|
232
|
+
* @remarks
|
|
233
|
+
* A miss is not a failure. Selection reads the in-memory index and
|
|
234
|
+
* materialization reads storage, so a record deleted in between is a legitimate
|
|
235
|
+
* race and yields a shorter list rather than an error. A read that FAILS is a
|
|
236
|
+
* real fault and propagates.
|
|
237
|
+
*/
|
|
238
|
+
_materialize(entries) {
|
|
239
|
+
return mapResults(entries.map((entry) => this._readRecord(entry.scope, entry.envelope.id))).onSuccess((records) => succeed(records.filter((r) => r !== undefined)));
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Materialize one entry, treating "gone" as a fault rather than a miss.
|
|
243
|
+
*
|
|
244
|
+
* @remarks
|
|
245
|
+
* For paths where a vanished record really does mean the index and the vault
|
|
246
|
+
* disagree, rather than that something legitimately removed it in between.
|
|
247
|
+
*
|
|
248
|
+
* Three of the four callers hold the write lock, so nothing can have removed
|
|
249
|
+
* the record since the entry was read. `get()`'s versioned path does not, and
|
|
250
|
+
* is safe only because temporal kinds never physically delete a version — they
|
|
251
|
+
* invalidate in place, and cap-cull does not apply to them. **If eviction is
|
|
252
|
+
* ever added to the temporal path, that caller must change**, or it
|
|
253
|
+
* reintroduces the race this method exists to detect.
|
|
254
|
+
*
|
|
255
|
+
* `listScoped` also does not hold the lock, and uses this deliberately anyway:
|
|
256
|
+
* it feeds a coverage report, so a silent drop there is worse than a loud
|
|
257
|
+
* failure. See its comment, and `docs/FUTURE.md` for the eviction window.
|
|
258
|
+
*
|
|
259
|
+
* The drop-tolerant counterpart is {@link FileTreeMemoryStore._materialize},
|
|
260
|
+
* for readers where a record that vanished between selection and
|
|
261
|
+
* materialization is a miss rather than a fault.
|
|
262
|
+
*/
|
|
263
|
+
_resolveRequired(entry) {
|
|
264
|
+
return this._readRecord(entry.scope, entry.envelope.id).onSuccess((record) => record === undefined
|
|
265
|
+
? fail(`memory: index entry '${entry.scope}/${entry.envelope.id}' has no record in the vault`)
|
|
266
|
+
: succeed(record));
|
|
176
267
|
}
|
|
177
268
|
/** {@inheritDoc IMemoryStore.asRecordSource} */
|
|
178
269
|
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
|
-
};
|
|
270
|
+
return vectorRecordSource(this);
|
|
187
271
|
}
|
|
188
272
|
/**
|
|
189
273
|
* Collapse temporal records to the single version valid at `asOf` per entity;
|
|
@@ -192,21 +276,21 @@ export class FileTreeMemoryStore {
|
|
|
192
276
|
* filtering is deferred — OQ-9). An entity with no version valid at `asOf`
|
|
193
277
|
* contributes nothing.
|
|
194
278
|
*/
|
|
195
|
-
static _projectAsOf(
|
|
279
|
+
static _projectAsOf(entries, asOf) {
|
|
196
280
|
const passthrough = [];
|
|
197
281
|
const groups = new Map();
|
|
198
|
-
for (const
|
|
199
|
-
if (!isTemporalRecord(
|
|
200
|
-
passthrough.push(
|
|
282
|
+
for (const entry of entries) {
|
|
283
|
+
if (!isTemporalRecord(entry)) {
|
|
284
|
+
passthrough.push(entry);
|
|
201
285
|
continue;
|
|
202
286
|
}
|
|
203
|
-
const key = `${
|
|
287
|
+
const key = `${entry.envelope.kind}\0${entry.envelope.entityId}`;
|
|
204
288
|
const existing = groups.get(key);
|
|
205
289
|
if (existing === undefined) {
|
|
206
|
-
groups.set(key, [
|
|
290
|
+
groups.set(key, [entry]);
|
|
207
291
|
}
|
|
208
292
|
else {
|
|
209
|
-
existing.push(
|
|
293
|
+
existing.push(entry);
|
|
210
294
|
}
|
|
211
295
|
}
|
|
212
296
|
const result = [...passthrough];
|
|
@@ -218,9 +302,9 @@ export class FileTreeMemoryStore {
|
|
|
218
302
|
}
|
|
219
303
|
return result;
|
|
220
304
|
}
|
|
221
|
-
/** {@inheritDoc IMemoryStore.
|
|
222
|
-
async
|
|
223
|
-
return this._enqueue(() => this.
|
|
305
|
+
/** {@inheritDoc IMemoryStore.reconcile} */
|
|
306
|
+
async reconcile(kind, artifact) {
|
|
307
|
+
return this._enqueue(() => this._reconcileLocked(kind, artifact));
|
|
224
308
|
}
|
|
225
309
|
/** {@inheritDoc IMemoryStore.put} */
|
|
226
310
|
async put(record) {
|
|
@@ -391,8 +475,11 @@ export class FileTreeMemoryStore {
|
|
|
391
475
|
// mutableFields) actually applies. Content-dedup must never shadow LWW for
|
|
392
476
|
// the same entity.
|
|
393
477
|
const duplicate = this._findByContentHash(scope, hash, record.envelope.id);
|
|
394
|
-
if (duplicate
|
|
395
|
-
return
|
|
478
|
+
if (duplicate.isFailure()) {
|
|
479
|
+
return fail(duplicate.message);
|
|
480
|
+
}
|
|
481
|
+
if (duplicate.value !== undefined) {
|
|
482
|
+
return succeed({ record: duplicate.value, evicted: [] });
|
|
396
483
|
}
|
|
397
484
|
}
|
|
398
485
|
return this._readRecord(scope, idStem).thenOnSuccess((existing) => {
|
|
@@ -413,9 +500,8 @@ export class FileTreeMemoryStore {
|
|
|
413
500
|
// (a replace, not a grow). The same-id `existing` record is threaded
|
|
414
501
|
// separately into `_buildRecord` for the merge-patch. Knowledge LWW ignores
|
|
415
502
|
// this argument, so its behavior is unchanged by the wider cohort.
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
.admit(record, cohort)
|
|
503
|
+
return this._admissionCohort(scope, record.envelope.kind, idStem)
|
|
504
|
+
.onSuccess((cohort) => policy.admit(record, cohort))
|
|
419
505
|
.thenOnSuccess((decision) => this._admitWrite(record, body, scope, idStem, hash, policy, existing, decision));
|
|
420
506
|
});
|
|
421
507
|
}
|
|
@@ -565,7 +651,16 @@ export class FileTreeMemoryStore {
|
|
|
565
651
|
* written, or fully invalidated / soft-deleted).
|
|
566
652
|
*/
|
|
567
653
|
_readVersionedCurrent(scope) {
|
|
568
|
-
|
|
654
|
+
// Select over ENVELOPES, then materialize the one winner — which is what
|
|
655
|
+
// `IMemoryStore.get`'s docstring promises and what `selectCurrentVersion`
|
|
656
|
+
// being generic over `IEnvelopeCarrier` exists for. Materializing every
|
|
657
|
+
// version first (as this did) cost N file reads and N body validations to
|
|
658
|
+
// return one record, and made that docstring false.
|
|
659
|
+
const current = selectCurrentVersion(this._index.entries().filter((entry) => entry.scope === scope));
|
|
660
|
+
if (current === undefined) {
|
|
661
|
+
return succeed(undefined);
|
|
662
|
+
}
|
|
663
|
+
return this._resolveRequired(current);
|
|
569
664
|
}
|
|
570
665
|
/**
|
|
571
666
|
* Every persisted version of the entity whose subtree is `scope`. All version
|
|
@@ -573,10 +668,13 @@ export class FileTreeMemoryStore {
|
|
|
573
668
|
* entityId), so a scope filter over the index isolates one entity's versions.
|
|
574
669
|
*/
|
|
575
670
|
_versionsForEntity(scope) {
|
|
576
|
-
|
|
671
|
+
// Selected on the envelope (scope), materialized after — so a versioned write
|
|
672
|
+
// reads only that entity's versions, never the vault. Bounded by the entity's
|
|
673
|
+
// version count.
|
|
674
|
+
return mapResults(this._index
|
|
577
675
|
.entries()
|
|
578
676
|
.filter((entry) => entry.scope === scope)
|
|
579
|
-
.map((entry) => entry
|
|
677
|
+
.map((entry) => this._resolveRequired(entry)));
|
|
580
678
|
}
|
|
581
679
|
/**
|
|
582
680
|
* Versioned write (invalidate-don't-delete). Builds the new version's content
|
|
@@ -598,7 +696,11 @@ export class FileTreeMemoryStore {
|
|
|
598
696
|
// still-current version at snapshot time — normally one, but two-or-more if a
|
|
599
697
|
// prior invalidation partially failed; invalidating all of them lets the write
|
|
600
698
|
// self-heal a stuck state (P2-7).
|
|
601
|
-
const
|
|
699
|
+
const snapshot = this._versionsForEntity(scope);
|
|
700
|
+
if (snapshot.isFailure()) {
|
|
701
|
+
return fail(snapshot.message);
|
|
702
|
+
}
|
|
703
|
+
const versions = snapshot.value;
|
|
602
704
|
const priorCurrents = versions.filter(isVersionCurrent);
|
|
603
705
|
const current = selectCurrentVersion(versions);
|
|
604
706
|
const policy = this._policyFor(kind);
|
|
@@ -724,7 +826,11 @@ export class FileTreeMemoryStore {
|
|
|
724
826
|
* builds on it), so a hard delete would defeat the purpose.
|
|
725
827
|
*/
|
|
726
828
|
async _deleteVersioned(entityId, scope) {
|
|
727
|
-
const
|
|
829
|
+
const snapshot = this._versionsForEntity(scope);
|
|
830
|
+
if (snapshot.isFailure()) {
|
|
831
|
+
return fail(snapshot.message);
|
|
832
|
+
}
|
|
833
|
+
const versions = snapshot.value;
|
|
728
834
|
const currents = versions.filter(isVersionCurrent);
|
|
729
835
|
const current = selectCurrentVersion(versions);
|
|
730
836
|
if (current === undefined) {
|
|
@@ -771,10 +877,12 @@ export class FileTreeMemoryStore {
|
|
|
771
877
|
* across first-writes and updates.
|
|
772
878
|
*/
|
|
773
879
|
_admissionCohort(scope, kind, idStem) {
|
|
774
|
-
|
|
880
|
+
// Envelope-only selection; the cohort is per-(scope, kind) and bounded by the
|
|
881
|
+
// cull cap, so materializing it is small by construction.
|
|
882
|
+
return mapResults(this._index
|
|
775
883
|
.entries()
|
|
776
|
-
.filter((entry) => entry.scope === scope && entry.
|
|
777
|
-
.map((entry) => entry
|
|
884
|
+
.filter((entry) => entry.scope === scope && entry.envelope.kind === kind && entry.envelope.id !== idStem)
|
|
885
|
+
.map((entry) => this._resolveRequired(entry)));
|
|
778
886
|
}
|
|
779
887
|
/**
|
|
780
888
|
* Find a record in `scope` whose `contentHash` equals `hash`, if any,
|
|
@@ -782,12 +890,13 @@ export class FileTreeMemoryStore {
|
|
|
782
890
|
* dedup skip the same-id record so it does not shadow the LWW update path.
|
|
783
891
|
*/
|
|
784
892
|
_findByContentHash(scope, hash, excludeId) {
|
|
893
|
+
// The clearest win of the projection: the hash lives on the envelope, so this
|
|
894
|
+
// scans envelopes and reads exactly ONE file — the match — where it used to
|
|
895
|
+
// hold every body in the scope to look at one field.
|
|
785
896
|
const match = this._index
|
|
786
897
|
.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;
|
|
898
|
+
.find((entry) => entry.scope === scope && entry.envelope.contentHash === hash && entry.envelope.id !== excludeId);
|
|
899
|
+
return match === undefined ? succeed(undefined) : this._resolveRequired(match);
|
|
791
900
|
}
|
|
792
901
|
/**
|
|
793
902
|
* True when `incoming`'s caller-authored mutable metadata (`tags` / `provenance`)
|
|
@@ -813,7 +922,7 @@ export class FileTreeMemoryStore {
|
|
|
813
922
|
return this._hasher.computeHash({ kind, body, links });
|
|
814
923
|
}
|
|
815
924
|
/**
|
|
816
|
-
* The
|
|
925
|
+
* The rank branch of {@link FileTreeMemoryStore.reconcile}, under the write lock.
|
|
817
926
|
*
|
|
818
927
|
* @remarks
|
|
819
928
|
* Re-reads each record's file rather than trusting the in-memory index, for
|
|
@@ -842,31 +951,54 @@ export class FileTreeMemoryStore {
|
|
|
842
951
|
* another on a reconcile. `_stampRank` itself is reused verbatim, which also
|
|
843
952
|
* inherits its throw semantics (logged at `warn`, `rank` cleared).
|
|
844
953
|
*/
|
|
845
|
-
async
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
}
|
|
954
|
+
async _reconcileLocked(kind, artifact) {
|
|
955
|
+
// Envelope-only selection: the walk needs `(scope, id)` and the kind, and
|
|
956
|
+
// each branch materializes only the records it decides to repair.
|
|
849
957
|
const targets = this._index
|
|
850
958
|
.entries()
|
|
851
|
-
.filter((entry) => entry.
|
|
852
|
-
|
|
959
|
+
.filter((entry) => entry.envelope.kind === kind);
|
|
960
|
+
if (artifact === 'rank') {
|
|
961
|
+
return this._reconcileRankLocked(kind, targets);
|
|
962
|
+
}
|
|
963
|
+
return reconcileVectors({
|
|
964
|
+
kind,
|
|
965
|
+
artifact,
|
|
966
|
+
targets,
|
|
967
|
+
maintenance: this._vectors,
|
|
968
|
+
embedsKind: (k) => this.embedsKind(k),
|
|
969
|
+
resolve: (scope, id) => this._readRecord(scope, id),
|
|
970
|
+
stampRef: (scope, id, ref) => this._rewriteEnvelope(scope, id, (r) => r.envelope.embeddingRef === ref
|
|
971
|
+
? undefined
|
|
972
|
+
: { envelope: Object.assign(Object.assign({}, r.envelope), { embeddingRef: ref }), body: r.body })
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
/** The rank branch of {@link FileTreeMemoryStore._reconcileLocked}. */
|
|
976
|
+
async _reconcileRankLocked(kind, targets) {
|
|
977
|
+
if (!this._rankProjectors.has(kind)) {
|
|
978
|
+
return fail(`memory reconcile '${kind}' rank: no rank projector is registered for this kind`);
|
|
979
|
+
}
|
|
980
|
+
// No materialization at all: `_rewriteEnvelope` re-reads each record itself.
|
|
981
|
+
let repaired = 0;
|
|
853
982
|
for (const target of targets) {
|
|
854
|
-
const applied = this.
|
|
983
|
+
const applied = this._rewriteEnvelope(target.scope, target.envelope.id, (r) => {
|
|
984
|
+
const stamped = this._stampRank(r);
|
|
985
|
+
return stamped.envelope.rank === r.envelope.rank ? undefined : stamped;
|
|
986
|
+
});
|
|
855
987
|
if (applied.isFailure()) {
|
|
856
|
-
return fail(`memory
|
|
988
|
+
return fail(`memory reconcile '${kind}' rank: ${applied.message}`);
|
|
857
989
|
}
|
|
858
990
|
if (applied.value) {
|
|
859
|
-
|
|
991
|
+
repaired++;
|
|
860
992
|
}
|
|
861
993
|
}
|
|
862
|
-
return succeed(
|
|
994
|
+
return succeed({ artifact: 'rank', kind, examined: targets.length, repaired, failed: [] });
|
|
863
995
|
}
|
|
864
996
|
/**
|
|
865
997
|
* Re-apply the rank projector to one record on disk. Returns whether `rank`
|
|
866
998
|
* actually changed — an unchanged rank writes nothing, so a reconcile over an
|
|
867
999
|
* already-consistent store touches no files.
|
|
868
1000
|
*/
|
|
869
|
-
|
|
1001
|
+
_rewriteEnvelope(scope, id, mutate) {
|
|
870
1002
|
return this._resolveScopeDir(scope).onSuccess((scopeDir) => {
|
|
871
1003
|
/* c8 ignore next 3 - defensive: the scope dir exists for any indexed record */
|
|
872
1004
|
if (scopeDir === undefined) {
|
|
@@ -894,12 +1026,16 @@ export class FileTreeMemoryStore {
|
|
|
894
1026
|
.onSuccess((parsed) => splitFrontmatter(raw)
|
|
895
1027
|
.withErrorFormat((msg) => `'${id}': ${msg}`)
|
|
896
1028
|
.onSuccess((parts) => {
|
|
897
|
-
|
|
898
|
-
|
|
1029
|
+
// The mutator says "nothing to change" with `undefined` rather
|
|
1030
|
+
// than by returning an equal record: persisting unconditionally
|
|
1031
|
+
// would bump `updated` on every record of the kind, trading a
|
|
1032
|
+
// wrong value for a wrong timestamp, and a deep comparison here
|
|
1033
|
+
// would have to know which fields each caller touches.
|
|
1034
|
+
const stamped = mutate({
|
|
899
1035
|
envelope: parsed.envelope,
|
|
900
1036
|
body: parts.body
|
|
901
1037
|
});
|
|
902
|
-
if (stamped
|
|
1038
|
+
if (stamped === undefined) {
|
|
903
1039
|
return succeed(false);
|
|
904
1040
|
}
|
|
905
1041
|
return this._persist(stamped, scope, id).onSuccess(() => succeed(true));
|
|
@@ -1095,8 +1231,8 @@ export class FileTreeMemoryStore {
|
|
|
1095
1231
|
_initialIndex(onRecordError) {
|
|
1096
1232
|
return this._collectEntries(this._root, [], onRecordError).onSuccess((entries) => this._index.rebuild(entries).onSuccess(() => {
|
|
1097
1233
|
for (const entry of entries) {
|
|
1098
|
-
if (entry.
|
|
1099
|
-
this._seq = entry.
|
|
1234
|
+
if (entry.envelope.seq > this._seq) {
|
|
1235
|
+
this._seq = entry.envelope.seq;
|
|
1100
1236
|
}
|
|
1101
1237
|
}
|
|
1102
1238
|
return succeed(true);
|
|
@@ -1137,11 +1273,17 @@ export class FileTreeMemoryStore {
|
|
|
1137
1273
|
* failure passes through untouched so the historical error is byte-identical.
|
|
1138
1274
|
*/
|
|
1139
1275
|
_loadRecordFile(scope, child, onRecordError) {
|
|
1140
|
-
return child
|
|
1276
|
+
return (child
|
|
1141
1277
|
.getRawContents()
|
|
1142
1278
|
.onSuccess((raw) => parseMemoryFile(raw, this._registry))
|
|
1143
1279
|
.onSuccess((parsedRecord) => this._verifyLoaded(scope, child, parsedRecord))
|
|
1144
|
-
|
|
1280
|
+
// Parse → validate → PROJECT → discard, per file. The body is read and
|
|
1281
|
+
// fully validated (which is what gives `onRecordError` its meaning), then
|
|
1282
|
+
// dropped here rather than carried into the index. Peak body residency
|
|
1283
|
+
// across the whole open is therefore ONE record, not N — which is the
|
|
1284
|
+
// resident-memory moment this whole surface exists to fix, and it is why
|
|
1285
|
+
// the projection had to reach `rebuild` and not just the read methods.
|
|
1286
|
+
.onSuccess((verified) => succeed([{ scope, envelope: verified.envelope }]))
|
|
1145
1287
|
.onFailure((message) => {
|
|
1146
1288
|
if (onRecordError === 'skip') {
|
|
1147
1289
|
const path = `${scope}/${child.name}`;
|
|
@@ -1150,7 +1292,7 @@ export class FileTreeMemoryStore {
|
|
|
1150
1292
|
this._warnSwallowed(error);
|
|
1151
1293
|
}
|
|
1152
1294
|
return fail(message);
|
|
1153
|
-
});
|
|
1295
|
+
}));
|
|
1154
1296
|
}
|
|
1155
1297
|
}
|
|
1156
1298
|
/**
|