@framers/agentos 0.10.10 → 0.10.12
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/README.md +2 -1
- package/dist/api/generateText.d.ts +10 -3
- package/dist/api/generateText.d.ts.map +1 -1
- package/dist/api/generateText.js.map +1 -1
- package/dist/cognition/memory/AgentMemory.d.ts +4 -0
- package/dist/cognition/memory/AgentMemory.d.ts.map +1 -1
- package/dist/cognition/memory/AgentMemory.js +56 -17
- package/dist/cognition/memory/AgentMemory.js.map +1 -1
- package/dist/cognition/memory/CognitiveMemoryManager.d.ts +6 -0
- package/dist/cognition/memory/CognitiveMemoryManager.d.ts.map +1 -1
- package/dist/cognition/memory/CognitiveMemoryManager.js +100 -8
- package/dist/cognition/memory/CognitiveMemoryManager.js.map +1 -1
- package/dist/cognition/memory/io/extension/CognitiveMemoryExtension.d.ts.map +1 -1
- package/dist/cognition/memory/io/extension/CognitiveMemoryExtension.js +8 -2
- package/dist/cognition/memory/io/extension/CognitiveMemoryExtension.js.map +1 -1
- package/dist/cognition/memory/pipeline/consolidation/ConsolidationPipeline.d.ts +7 -0
- package/dist/cognition/memory/pipeline/consolidation/ConsolidationPipeline.d.ts.map +1 -1
- package/dist/cognition/memory/pipeline/consolidation/ConsolidationPipeline.js +84 -55
- package/dist/cognition/memory/pipeline/consolidation/ConsolidationPipeline.js.map +1 -1
- package/dist/cognition/memory/retrieval/store/Brain.d.ts +14 -2
- package/dist/cognition/memory/retrieval/store/Brain.d.ts.map +1 -1
- package/dist/cognition/memory/retrieval/store/Brain.js +264 -15
- package/dist/cognition/memory/retrieval/store/Brain.js.map +1 -1
- package/dist/cognition/memory/retrieval/store/MemoryStore.d.ts +41 -2
- package/dist/cognition/memory/retrieval/store/MemoryStore.d.ts.map +1 -1
- package/dist/cognition/memory/retrieval/store/MemoryStore.js +676 -131
- package/dist/cognition/memory/retrieval/store/MemoryStore.js.map +1 -1
- package/dist/core/llm/providers/model-effort.d.ts +8 -4
- package/dist/core/llm/providers/model-effort.d.ts.map +1 -1
- package/dist/core/llm/providers/model-effort.js +29 -4
- package/dist/core/llm/providers/model-effort.js.map +1 -1
- package/package.json +1 -1
|
@@ -144,6 +144,205 @@ function blobToEmbedding(blob) {
|
|
|
144
144
|
return null;
|
|
145
145
|
return Array.from(new Float32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / 4));
|
|
146
146
|
}
|
|
147
|
+
const WeakRefImplementation = typeof WeakRef === 'function' ? WeakRef : undefined;
|
|
148
|
+
const FinalizationRegistryImplementation = typeof FinalizationRegistry === 'function' ? FinalizationRegistry : undefined;
|
|
149
|
+
function createReference(target) {
|
|
150
|
+
if (WeakRefImplementation)
|
|
151
|
+
return new WeakRefImplementation(target);
|
|
152
|
+
// Compatibility fallback for runtimes without WeakRef. MemoryStore.dispose
|
|
153
|
+
// deterministically removes the strong registration.
|
|
154
|
+
return { deref: () => target };
|
|
155
|
+
}
|
|
156
|
+
const namespaceCoordinators = new WeakMap();
|
|
157
|
+
const vectorStoreNamespaces = new WeakMap();
|
|
158
|
+
function getNamespaceCoordinator(namespace) {
|
|
159
|
+
let coordinator = namespaceCoordinators.get(namespace);
|
|
160
|
+
if (!coordinator) {
|
|
161
|
+
coordinator = {
|
|
162
|
+
deleteEpoch: 0,
|
|
163
|
+
pendingOperations: 0,
|
|
164
|
+
operations: new Map(),
|
|
165
|
+
stores: new Set(),
|
|
166
|
+
};
|
|
167
|
+
namespaceCoordinators.set(namespace, coordinator);
|
|
168
|
+
}
|
|
169
|
+
return coordinator;
|
|
170
|
+
}
|
|
171
|
+
function compactNamespaceCoordinator(namespace, coordinator) {
|
|
172
|
+
for (const reference of coordinator.stores) {
|
|
173
|
+
if (!reference.deref())
|
|
174
|
+
coordinator.stores.delete(reference);
|
|
175
|
+
}
|
|
176
|
+
if (coordinator.stores.size === 0 &&
|
|
177
|
+
coordinator.pendingOperations === 0 &&
|
|
178
|
+
coordinator.operations.size === 0 &&
|
|
179
|
+
namespaceCoordinators.get(namespace) === coordinator) {
|
|
180
|
+
namespaceCoordinators.delete(namespace);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const storeFinalizer = FinalizationRegistryImplementation
|
|
184
|
+
? new FinalizationRegistryImplementation((registration) => {
|
|
185
|
+
const coordinator = namespaceCoordinators.get(registration.namespace);
|
|
186
|
+
if (!coordinator)
|
|
187
|
+
return;
|
|
188
|
+
coordinator.stores.delete(registration.reference);
|
|
189
|
+
compactNamespaceCoordinator(registration.namespace, coordinator);
|
|
190
|
+
})
|
|
191
|
+
: null;
|
|
192
|
+
function registerCoordinationTarget(namespace, target) {
|
|
193
|
+
const coordinator = getNamespaceCoordinator(namespace);
|
|
194
|
+
const reference = createReference(target);
|
|
195
|
+
const registration = { namespace, reference };
|
|
196
|
+
coordinator.stores.add(reference);
|
|
197
|
+
storeFinalizer?.register(target, registration, reference);
|
|
198
|
+
return registration;
|
|
199
|
+
}
|
|
200
|
+
function unregisterCoordinationTarget(registration) {
|
|
201
|
+
storeFinalizer?.unregister(registration.reference);
|
|
202
|
+
const coordinator = namespaceCoordinators.get(registration.namespace);
|
|
203
|
+
if (!coordinator)
|
|
204
|
+
return;
|
|
205
|
+
coordinator.stores.delete(registration.reference);
|
|
206
|
+
compactNamespaceCoordinator(registration.namespace, coordinator);
|
|
207
|
+
}
|
|
208
|
+
function vectorStoreNamespace(vectorStore, collectionPrefix) {
|
|
209
|
+
let namespaces = vectorStoreNamespaces.get(vectorStore);
|
|
210
|
+
if (!namespaces) {
|
|
211
|
+
namespaces = new Map();
|
|
212
|
+
vectorStoreNamespaces.set(vectorStore, namespaces);
|
|
213
|
+
}
|
|
214
|
+
let namespace = namespaces.get(collectionPrefix);
|
|
215
|
+
if (!namespace) {
|
|
216
|
+
namespace = {};
|
|
217
|
+
namespaces.set(collectionPrefix, namespace);
|
|
218
|
+
}
|
|
219
|
+
return namespace;
|
|
220
|
+
}
|
|
221
|
+
function markTraceDeleted(namespace, traceId, authoritative = true) {
|
|
222
|
+
const coordinator = getNamespaceCoordinator(namespace);
|
|
223
|
+
for (const reference of coordinator.stores) {
|
|
224
|
+
const target = reference.deref();
|
|
225
|
+
if (target)
|
|
226
|
+
target.markDeleted(traceId, authoritative);
|
|
227
|
+
else
|
|
228
|
+
coordinator.stores.delete(reference);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function releaseDeleteAuthority(namespace, traceId) {
|
|
232
|
+
const coordinator = getNamespaceCoordinator(namespace);
|
|
233
|
+
for (const reference of coordinator.stores) {
|
|
234
|
+
const target = reference.deref();
|
|
235
|
+
if (target)
|
|
236
|
+
target.releaseDeleteAuthority(traceId);
|
|
237
|
+
else
|
|
238
|
+
coordinator.stores.delete(reference);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async function evictDeletedTrace(namespace, traceId, fallback, requiredTarget) {
|
|
242
|
+
const coordinator = getNamespaceCoordinator(namespace);
|
|
243
|
+
const targets = new Set();
|
|
244
|
+
for (const reference of coordinator.stores) {
|
|
245
|
+
const target = reference.deref();
|
|
246
|
+
if (target)
|
|
247
|
+
targets.add(target);
|
|
248
|
+
else
|
|
249
|
+
coordinator.stores.delete(reference);
|
|
250
|
+
}
|
|
251
|
+
if (requiredTarget)
|
|
252
|
+
targets.add(requiredTarget);
|
|
253
|
+
const outcomes = await Promise.all([...targets].map((target) => target.evictDeleted(traceId, fallback)));
|
|
254
|
+
return outcomes.every(Boolean);
|
|
255
|
+
}
|
|
256
|
+
function hasDeletedTrace(namespace, traceId) {
|
|
257
|
+
const coordinator = getNamespaceCoordinator(namespace);
|
|
258
|
+
for (const reference of coordinator.stores) {
|
|
259
|
+
const target = reference.deref();
|
|
260
|
+
if (target?.isDeleted(traceId))
|
|
261
|
+
return true;
|
|
262
|
+
if (!target)
|
|
263
|
+
coordinator.stores.delete(reference);
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
function hasAuthoritativeDelete(namespace, traceId) {
|
|
268
|
+
const coordinator = getNamespaceCoordinator(namespace);
|
|
269
|
+
for (const reference of coordinator.stores) {
|
|
270
|
+
const target = reference.deref();
|
|
271
|
+
if (target?.isDeleteAuthoritative(traceId))
|
|
272
|
+
return true;
|
|
273
|
+
if (!target)
|
|
274
|
+
coordinator.stores.delete(reference);
|
|
275
|
+
}
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
async function reviveTrace(namespace, trace, embedding, source, sourceAlreadyWritten = true) {
|
|
279
|
+
const coordinator = getNamespaceCoordinator(namespace);
|
|
280
|
+
const targets = [];
|
|
281
|
+
for (const reference of coordinator.stores) {
|
|
282
|
+
const target = reference.deref();
|
|
283
|
+
if (target)
|
|
284
|
+
targets.push(target);
|
|
285
|
+
else
|
|
286
|
+
coordinator.stores.delete(reference);
|
|
287
|
+
}
|
|
288
|
+
// Fan-out is intentionally limited to deletion-to-active transitions.
|
|
289
|
+
// General active-update cache coherence has different latency and provider
|
|
290
|
+
// cost semantics and remains outside this deletion-safety coordinator.
|
|
291
|
+
if (!targets.some((target) => target.isDeleted(trace.id))) {
|
|
292
|
+
return source.revive(trace, embedding, sourceAlreadyWritten);
|
|
293
|
+
}
|
|
294
|
+
const outcomes = await Promise.all(targets.map((target) => target.revive(trace, embedding, sourceAlreadyWritten && target === source)));
|
|
295
|
+
return outcomes.every(Boolean);
|
|
296
|
+
}
|
|
297
|
+
function getDeleteEpoch(namespace) {
|
|
298
|
+
return getNamespaceCoordinator(namespace).deleteEpoch;
|
|
299
|
+
}
|
|
300
|
+
function isTraceDeletePending(namespace, traceId) {
|
|
301
|
+
const coordinator = namespaceCoordinators.get(namespace);
|
|
302
|
+
return (coordinator?.operations.get(traceId)?.pendingDeletes ?? 0) > 0;
|
|
303
|
+
}
|
|
304
|
+
async function withTraceOperation(namespace, traceId, kind, operation) {
|
|
305
|
+
const coordinator = getNamespaceCoordinator(namespace);
|
|
306
|
+
let state = coordinator.operations.get(traceId);
|
|
307
|
+
if (!state) {
|
|
308
|
+
state = { tail: Promise.resolve(), pending: 0, pendingDeletes: 0 };
|
|
309
|
+
coordinator.operations.set(traceId, state);
|
|
310
|
+
}
|
|
311
|
+
const operationState = state;
|
|
312
|
+
const blockedByDelete = kind === 'normal' && operationState.pendingDeletes > 0;
|
|
313
|
+
if (kind === 'delete') {
|
|
314
|
+
operationState.pendingDeletes += 1;
|
|
315
|
+
coordinator.deleteEpoch += 1;
|
|
316
|
+
markTraceDeleted(namespace, traceId);
|
|
317
|
+
}
|
|
318
|
+
const previous = operationState.tail;
|
|
319
|
+
let release;
|
|
320
|
+
const completion = new Promise((resolve) => {
|
|
321
|
+
release = resolve;
|
|
322
|
+
});
|
|
323
|
+
operationState.tail = previous.then(() => completion, () => completion);
|
|
324
|
+
operationState.pending += 1;
|
|
325
|
+
coordinator.pendingOperations += 1;
|
|
326
|
+
await previous.catch(() => undefined);
|
|
327
|
+
try {
|
|
328
|
+
return await operation({
|
|
329
|
+
blockedByDelete,
|
|
330
|
+
isDeletePending: () => kind === 'normal' && operationState.pendingDeletes > 0,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
finally {
|
|
334
|
+
if (kind === 'delete')
|
|
335
|
+
operationState.pendingDeletes -= 1;
|
|
336
|
+
operationState.pending -= 1;
|
|
337
|
+
coordinator.pendingOperations -= 1;
|
|
338
|
+
release?.();
|
|
339
|
+
if (operationState.pending === 0 &&
|
|
340
|
+
coordinator.operations.get(traceId) === operationState) {
|
|
341
|
+
coordinator.operations.delete(traceId);
|
|
342
|
+
}
|
|
343
|
+
compactNamespaceCoordinator(namespace, coordinator);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
147
346
|
// ---------------------------------------------------------------------------
|
|
148
347
|
// MemoryStore
|
|
149
348
|
// ---------------------------------------------------------------------------
|
|
@@ -155,6 +354,8 @@ export class MemoryStore {
|
|
|
155
354
|
this.embeddingCache = new Map();
|
|
156
355
|
/** Process-local deletion barrier for asynchronous vector refreshes and hydration. */
|
|
157
356
|
this.tombstonedTraceIds = new Set();
|
|
357
|
+
/** Tombstones whose local delete has not been durably confirmed. */
|
|
358
|
+
this.authoritativeTombstoneIds = new Set();
|
|
158
359
|
/** Track concrete scopes we have seen, so retrieval never falls back to a fake wildcard scope. */
|
|
159
360
|
this.knownScopes = new Map();
|
|
160
361
|
/**
|
|
@@ -166,19 +367,52 @@ export class MemoryStore {
|
|
|
166
367
|
this.brain = null;
|
|
167
368
|
/** Whether {@link MemoryStore.ensureHydratedFromBrain} has already run for this instance. */
|
|
168
369
|
this.brainHydrated = false;
|
|
370
|
+
this.disposed = false;
|
|
371
|
+
/** Brain attachment is immutable once any memory operation has started. */
|
|
372
|
+
this.brainAttachmentLocked = false;
|
|
169
373
|
this.config = config;
|
|
170
374
|
this.decay = config.decayConfig ?? DEFAULT_DECAY_CONFIG;
|
|
171
375
|
this.mechanismsEngine = config.mechanismsEngine;
|
|
376
|
+
this.coordinationTarget = {
|
|
377
|
+
isDeleted: (traceId) => this.tombstonedTraceIds.has(traceId),
|
|
378
|
+
isDeleteAuthoritative: (traceId) => this.authoritativeTombstoneIds.has(traceId),
|
|
379
|
+
markDeleted: (traceId, authoritative) => this.markDeletedLocally(traceId, authoritative),
|
|
380
|
+
releaseDeleteAuthority: (traceId) => this.authoritativeTombstoneIds.delete(traceId),
|
|
381
|
+
evictDeleted: (traceId, fallback) => this.evictDeletedLocally(traceId, fallback),
|
|
382
|
+
revive: (trace, embedding, source) => this.reviveLocally(trace, embedding, source),
|
|
383
|
+
};
|
|
384
|
+
this.coordinationNamespace = vectorStoreNamespace(config.vectorStore, config.collectionPrefix);
|
|
385
|
+
this.coordinationRegistration = registerCoordinationTarget(this.coordinationNamespace, this.coordinationTarget);
|
|
172
386
|
}
|
|
173
387
|
/**
|
|
174
388
|
* Attach a Brain for durable write-through persistence.
|
|
175
389
|
* Once attached, all store/softDelete/recordAccess operations also
|
|
176
|
-
* write to the brain's `memory_traces` table.
|
|
390
|
+
* write to the brain's `memory_traces` table. Initial attachment must happen
|
|
391
|
+
* before any memory operation; after that, only another handle with the same
|
|
392
|
+
* coordination token can be supplied.
|
|
177
393
|
*
|
|
178
394
|
* @param brain - Brain instance (already initialized with schema)
|
|
395
|
+
* @throws When attachment is late or would switch backing resources.
|
|
179
396
|
*/
|
|
180
397
|
setBrain(brain) {
|
|
398
|
+
this.assertNotDisposed();
|
|
399
|
+
if (this.brain) {
|
|
400
|
+
if (this.brain.coordinationToken !== brain.coordinationToken) {
|
|
401
|
+
throw new Error('MemoryStore.setBrain: cannot switch to a different backing resource');
|
|
402
|
+
}
|
|
403
|
+
this.brain = brain;
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (this.brainAttachmentLocked) {
|
|
407
|
+
throw new Error('MemoryStore.setBrain: attach a Brain before starting memory operations');
|
|
408
|
+
}
|
|
409
|
+
const previousRegistration = this.coordinationRegistration;
|
|
181
410
|
this.brain = brain;
|
|
411
|
+
this.coordinationNamespace = brain.coordinationToken;
|
|
412
|
+
this.coordinationRegistration = registerCoordinationTarget(this.coordinationNamespace, this.coordinationTarget);
|
|
413
|
+
if (previousRegistration) {
|
|
414
|
+
unregisterCoordinationTarget(previousRegistration);
|
|
415
|
+
}
|
|
182
416
|
}
|
|
183
417
|
/**
|
|
184
418
|
* Access the attached Brain for export/import operations.
|
|
@@ -187,6 +421,190 @@ export class MemoryStore {
|
|
|
187
421
|
getBrain() {
|
|
188
422
|
return this.brain;
|
|
189
423
|
}
|
|
424
|
+
/**
|
|
425
|
+
* Remove this store from sibling coordination. Recall and mutation methods
|
|
426
|
+
* reject after disposal; diagnostic cache accessors remain available.
|
|
427
|
+
*/
|
|
428
|
+
dispose() {
|
|
429
|
+
if (this.disposed)
|
|
430
|
+
return;
|
|
431
|
+
if (this.coordinationRegistration) {
|
|
432
|
+
unregisterCoordinationTarget(this.coordinationRegistration);
|
|
433
|
+
this.coordinationRegistration = null;
|
|
434
|
+
}
|
|
435
|
+
this.disposed = true;
|
|
436
|
+
}
|
|
437
|
+
assertNotDisposed() {
|
|
438
|
+
if (this.disposed) {
|
|
439
|
+
throw new Error('MemoryStore: operation attempted after dispose');
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
beginOperation() {
|
|
443
|
+
this.assertNotDisposed();
|
|
444
|
+
this.brainAttachmentLocked = true;
|
|
445
|
+
}
|
|
446
|
+
markDeletedLocally(traceId, authoritative = true) {
|
|
447
|
+
// Passive sibling coordination is still resource-specific state. Once it
|
|
448
|
+
// arrives, this store cannot safely migrate that state to another Brain.
|
|
449
|
+
this.brainAttachmentLocked = true;
|
|
450
|
+
this.tombstonedTraceIds.add(traceId);
|
|
451
|
+
if (authoritative)
|
|
452
|
+
this.authoritativeTombstoneIds.add(traceId);
|
|
453
|
+
this.embeddingCache.delete(traceId);
|
|
454
|
+
const trace = this.traceCache.get(traceId);
|
|
455
|
+
if (trace) {
|
|
456
|
+
trace.isActive = false;
|
|
457
|
+
trace.updatedAt = Date.now();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
async evictDeletedLocally(traceId, fallback) {
|
|
461
|
+
this.markDeletedLocally(traceId, false);
|
|
462
|
+
const trace = this.traceCache.get(traceId) ?? fallback;
|
|
463
|
+
this.embeddingCache.delete(traceId);
|
|
464
|
+
if (!trace)
|
|
465
|
+
return true;
|
|
466
|
+
const collection = collectionName(this.config.collectionPrefix, trace.scope, trace.scopeId);
|
|
467
|
+
try {
|
|
468
|
+
const result = await this.config.vectorStore.delete(collection, [traceId]);
|
|
469
|
+
return (result.failedCount ?? 0) === 0;
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
async reviveLocally(trace, embedding, source) {
|
|
476
|
+
this.brainAttachmentLocked = true;
|
|
477
|
+
if (source) {
|
|
478
|
+
const cached = this.traceCache.get(trace.id);
|
|
479
|
+
if (cached)
|
|
480
|
+
cached.isActive = true;
|
|
481
|
+
this.tombstonedTraceIds.delete(trace.id);
|
|
482
|
+
this.authoritativeTombstoneIds.delete(trace.id);
|
|
483
|
+
return true;
|
|
484
|
+
}
|
|
485
|
+
const collection = collectionName(this.config.collectionPrefix, trace.scope, trace.scopeId);
|
|
486
|
+
try {
|
|
487
|
+
const result = await this.config.vectorStore.upsert(collection, [{
|
|
488
|
+
id: trace.id,
|
|
489
|
+
textContent: trace.content,
|
|
490
|
+
embedding,
|
|
491
|
+
metadata: traceToMetadata(trace),
|
|
492
|
+
}]);
|
|
493
|
+
if ((result?.failedCount ?? 0) > 0)
|
|
494
|
+
return false;
|
|
495
|
+
const revivedTrace = {
|
|
496
|
+
...trace,
|
|
497
|
+
entities: [...trace.entities],
|
|
498
|
+
tags: [...trace.tags],
|
|
499
|
+
provenance: { ...trace.provenance },
|
|
500
|
+
emotionalContext: { ...trace.emotionalContext },
|
|
501
|
+
associatedTraceIds: [...trace.associatedTraceIds],
|
|
502
|
+
};
|
|
503
|
+
this.traceCache.set(trace.id, revivedTrace);
|
|
504
|
+
this.embeddingCache.set(trace.id, [...embedding]);
|
|
505
|
+
this.registerScope(trace.scope, trace.scopeId);
|
|
506
|
+
this.tombstonedTraceIds.delete(trace.id);
|
|
507
|
+
this.authoritativeTombstoneIds.delete(trace.id);
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
// Keep the sibling tombstone when its vector index cannot be revived.
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
async getDurableTraceRows(traceIds, brain = this.brain) {
|
|
516
|
+
const rowsById = new Map();
|
|
517
|
+
if (!brain || traceIds.length === 0)
|
|
518
|
+
return { ok: true, rows: rowsById };
|
|
519
|
+
try {
|
|
520
|
+
const uniqueIds = [...new Set(traceIds)];
|
|
521
|
+
for (let offset = 0; offset < uniqueIds.length; offset += 400) {
|
|
522
|
+
const chunk = uniqueIds.slice(offset, offset + 400);
|
|
523
|
+
const placeholders = chunk.map(() => '?').join(', ');
|
|
524
|
+
const rows = await brain.all(`SELECT id, type, scope, content, embedding, strength, created_at,
|
|
525
|
+
last_accessed, retrieval_count, tags, emotions, metadata, deleted
|
|
526
|
+
FROM memory_traces
|
|
527
|
+
WHERE brain_id = ? AND id IN (${placeholders})`, [brain.brainId, ...chunk]);
|
|
528
|
+
for (const row of rows)
|
|
529
|
+
rowsById.set(row.id, row);
|
|
530
|
+
}
|
|
531
|
+
return { ok: true, rows: rowsById };
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
return { ok: false, rows: new Map() };
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
async reconcileDurableTraceStates(traceIds, namespace, brain) {
|
|
538
|
+
if (!brain || traceIds.length === 0)
|
|
539
|
+
return true;
|
|
540
|
+
const initial = await this.getDurableTraceRows(traceIds, brain);
|
|
541
|
+
if (!initial.ok)
|
|
542
|
+
return false;
|
|
543
|
+
let validationAvailable = true;
|
|
544
|
+
const stateChanges = new Set();
|
|
545
|
+
for (const traceId of new Set(traceIds)) {
|
|
546
|
+
const row = initial.rows.get(traceId);
|
|
547
|
+
if (!row || row.deleted === 1 || hasDeletedTrace(namespace, traceId)) {
|
|
548
|
+
stateChanges.add(traceId);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
await Promise.all([...stateChanges].map((traceId) => withTraceOperation(namespace, traceId, 'normal', async ({ blockedByDelete }) => {
|
|
552
|
+
if (blockedByDelete)
|
|
553
|
+
return;
|
|
554
|
+
const confirmed = await this.getDurableTraceRows([traceId], brain);
|
|
555
|
+
if (!confirmed.ok) {
|
|
556
|
+
validationAvailable = false;
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const row = confirmed.rows.get(traceId);
|
|
560
|
+
if (!row) {
|
|
561
|
+
// Once a Brain is attached, provider-only documents are
|
|
562
|
+
// unvalidated. Re-read while holding the per-trace queue before
|
|
563
|
+
// fencing so a concurrent store cannot publish its durable row
|
|
564
|
+
// between validation and this local tombstone.
|
|
565
|
+
markTraceDeleted(namespace, traceId, false);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const trace = this.rowToTrace(row);
|
|
569
|
+
if (row.deleted === 1) {
|
|
570
|
+
markTraceDeleted(namespace, row.id, false);
|
|
571
|
+
releaseDeleteAuthority(namespace, row.id);
|
|
572
|
+
// Cross-process validation is deliberately non-destructive.
|
|
573
|
+
// A concurrent revival can write its new vector before its
|
|
574
|
+
// active SQL row; deleting here based on the older tombstone
|
|
575
|
+
// would erase that fresh vector. Explicit softDelete/retry owns
|
|
576
|
+
// provider cleanup, while this durable fence blocks recall.
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (!hasDeletedTrace(namespace, row.id) ||
|
|
580
|
+
hasAuthoritativeDelete(namespace, row.id)) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const embedding = blobToEmbedding(row.embedding);
|
|
584
|
+
if (!embedding || !isUsableEmbedding(embedding))
|
|
585
|
+
return;
|
|
586
|
+
await reviveTrace(namespace, trace, embedding, this.coordinationTarget, false);
|
|
587
|
+
})));
|
|
588
|
+
return validationAvailable;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Return the subset of cached trace IDs that remain recallable after a
|
|
592
|
+
* fail-closed durable-state check. Used by non-vector recall paths such as
|
|
593
|
+
* graph association injection so a tombstone cannot bypass vector filters.
|
|
594
|
+
*/
|
|
595
|
+
async filterRecallableTraceIds(traceIds) {
|
|
596
|
+
this.beginOperation();
|
|
597
|
+
const uniqueIds = [...new Set(traceIds)];
|
|
598
|
+
const validated = await this.reconcileDurableTraceStates(uniqueIds, this.coordinationNamespace, this.brain);
|
|
599
|
+
if (!validated)
|
|
600
|
+
return new Set();
|
|
601
|
+
return new Set(uniqueIds.filter((traceId) => {
|
|
602
|
+
const trace = this.traceCache.get(traceId);
|
|
603
|
+
return Boolean(trace?.isActive &&
|
|
604
|
+
!this.tombstonedTraceIds.has(traceId) &&
|
|
605
|
+
!isTraceDeletePending(this.coordinationNamespace, traceId));
|
|
606
|
+
}));
|
|
607
|
+
}
|
|
190
608
|
/**
|
|
191
609
|
* Hydrate the in-memory vector store + scope registry from the attached
|
|
192
610
|
* Brain on first use. The Brain is the durable backing store, but the vector
|
|
@@ -197,59 +615,81 @@ export class MemoryStore {
|
|
|
197
615
|
* best-effort: a SQL/schema error must never break the query path.
|
|
198
616
|
*/
|
|
199
617
|
async ensureHydratedFromBrain() {
|
|
200
|
-
if (this.brainHydrated
|
|
618
|
+
if (this.brainHydrated)
|
|
619
|
+
return;
|
|
620
|
+
const brain = this.brain;
|
|
621
|
+
if (!brain)
|
|
201
622
|
return;
|
|
202
623
|
this.brainHydrated = true; // set first: a failure must not retry on every query
|
|
624
|
+
const namespace = this.coordinationNamespace;
|
|
625
|
+
const hydrationDeleteEpoch = getDeleteEpoch(namespace);
|
|
203
626
|
try {
|
|
204
627
|
// Bound the per-instance load: a fresh facade (and on request-scoped
|
|
205
628
|
// hosts, every request) hydrates, so we cap at the most-recent traces to
|
|
206
629
|
// keep the SQL read + vector upserts bounded for very long-lived scopes.
|
|
207
630
|
// A process-level facade cache (so hydration runs once, not per request)
|
|
208
631
|
// is the follow-up optimisation; the cap keeps the un-cached path safe.
|
|
209
|
-
const rows = await
|
|
632
|
+
const rows = await brain.all(`SELECT id, type, scope, content, embedding, strength, created_at,
|
|
210
633
|
last_accessed, retrieval_count, tags, emotions, metadata
|
|
211
634
|
FROM memory_traces
|
|
212
635
|
WHERE brain_id = ? AND deleted = 0
|
|
213
636
|
ORDER BY created_at DESC
|
|
214
|
-
LIMIT ?`, [
|
|
637
|
+
LIMIT ?`, [brain.brainId, MemoryStore.HYDRATION_LIMIT]);
|
|
215
638
|
for (const row of rows) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (embedding == null || !isUsableEmbedding(embedding))
|
|
222
|
-
continue;
|
|
223
|
-
const trace = this.rowToTrace(row);
|
|
224
|
-
const collection = collectionName(this.config.collectionPrefix, trace.scope, trace.scopeId);
|
|
225
|
-
try {
|
|
226
|
-
const exists = this.config.vectorStore.collectionExists
|
|
227
|
-
? await this.config.vectorStore.collectionExists(collection)
|
|
228
|
-
: true;
|
|
229
|
-
if (!exists) {
|
|
230
|
-
await this.config.vectorStore.createCollection?.(collection, this.config.embeddingDimension ?? embedding.length, { overwriteIfExists: false });
|
|
639
|
+
await withTraceOperation(namespace, row.id, 'normal', async ({ blockedByDelete, isDeletePending }) => {
|
|
640
|
+
if (blockedByDelete ||
|
|
641
|
+
this.tombstonedTraceIds.has(row.id) ||
|
|
642
|
+
this.traceCache.has(row.id)) {
|
|
643
|
+
return;
|
|
231
644
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
645
|
+
// A delete may finish after the bulk SELECT but before this row's
|
|
646
|
+
// operation slot. Recheck only when deletion activity occurred.
|
|
647
|
+
if (getDeleteEpoch(namespace) !== hydrationDeleteEpoch) {
|
|
648
|
+
const stillActive = await brain.get('SELECT id FROM memory_traces WHERE brain_id = ? AND id = ? AND deleted = 0', [brain.brainId, row.id]);
|
|
649
|
+
if (!stillActive)
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const embedding = blobToEmbedding(row.embedding);
|
|
653
|
+
// Legacy rows persisted without embeddings become recallable once
|
|
654
|
+
// they are mentioned and stored again.
|
|
655
|
+
if (embedding == null || !isUsableEmbedding(embedding))
|
|
656
|
+
return;
|
|
657
|
+
const trace = this.rowToTrace(row);
|
|
658
|
+
const collection = collectionName(this.config.collectionPrefix, trace.scope, trace.scopeId);
|
|
242
659
|
try {
|
|
243
|
-
|
|
660
|
+
const exists = this.config.vectorStore.collectionExists
|
|
661
|
+
? await this.config.vectorStore.collectionExists(collection)
|
|
662
|
+
: true;
|
|
663
|
+
if (!exists) {
|
|
664
|
+
await this.config.vectorStore.createCollection?.(collection, this.config.embeddingDimension ?? embedding.length, { overwriteIfExists: false });
|
|
665
|
+
}
|
|
244
666
|
}
|
|
245
667
|
catch {
|
|
246
|
-
//
|
|
668
|
+
// Provider auto-creates the collection or omits existence checks.
|
|
247
669
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
670
|
+
const upsertResult = await this.config.vectorStore.upsert(collection, [
|
|
671
|
+
{
|
|
672
|
+
id: trace.id,
|
|
673
|
+
textContent: trace.content,
|
|
674
|
+
embedding,
|
|
675
|
+
metadata: traceToMetadata(trace),
|
|
676
|
+
},
|
|
677
|
+
]);
|
|
678
|
+
if ((upsertResult?.failedCount ?? 0) > 0)
|
|
679
|
+
return;
|
|
680
|
+
if (isDeletePending() || this.tombstonedTraceIds.has(row.id)) {
|
|
681
|
+
try {
|
|
682
|
+
await this.config.vectorStore.delete(collection, [row.id]);
|
|
683
|
+
}
|
|
684
|
+
catch {
|
|
685
|
+
// The queued delete retries after this operation releases.
|
|
686
|
+
}
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
this.traceCache.set(trace.id, trace);
|
|
690
|
+
this.embeddingCache.set(trace.id, embedding);
|
|
691
|
+
this.registerScope(trace.scope, trace.scopeId);
|
|
692
|
+
});
|
|
253
693
|
}
|
|
254
694
|
}
|
|
255
695
|
catch {
|
|
@@ -304,6 +744,29 @@ export class MemoryStore {
|
|
|
304
744
|
* and record as episodic memory in the knowledge graph.
|
|
305
745
|
*/
|
|
306
746
|
async store(trace) {
|
|
747
|
+
this.beginOperation();
|
|
748
|
+
const namespace = this.coordinationNamespace;
|
|
749
|
+
const brain = this.brain;
|
|
750
|
+
await withTraceOperation(namespace, trace.id, 'normal', async ({ blockedByDelete, isDeletePending }) => {
|
|
751
|
+
if (blockedByDelete) {
|
|
752
|
+
throw new Error('MemoryStore.store: trace deletion is already in progress');
|
|
753
|
+
}
|
|
754
|
+
const embedding = await this.storeWithTraceLock(trace, brain, namespace);
|
|
755
|
+
if (!isDeletePending()) {
|
|
756
|
+
await reviveTrace(namespace, trace, embedding, this.coordinationTarget);
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
async storeWithTraceLock(trace, brain, namespace) {
|
|
761
|
+
// Calling store with an existing ID is the explicit revival operation.
|
|
762
|
+
// Build active metadata without mutating a caller-provided cached
|
|
763
|
+
// tombstone until the vector provider has accepted the write.
|
|
764
|
+
const activeUpdatedAt = Date.now();
|
|
765
|
+
const activeTrace = {
|
|
766
|
+
...trace,
|
|
767
|
+
isActive: true,
|
|
768
|
+
updatedAt: activeUpdatedAt,
|
|
769
|
+
};
|
|
307
770
|
const collection = collectionName(this.config.collectionPrefix, trace.scope, trace.scopeId);
|
|
308
771
|
// Generate embedding
|
|
309
772
|
const embeddingResponse = await this.config.embeddingManager.generateEmbeddings({
|
|
@@ -332,9 +795,59 @@ export class MemoryStore {
|
|
|
332
795
|
id: trace.id,
|
|
333
796
|
textContent: trace.content,
|
|
334
797
|
embedding,
|
|
335
|
-
metadata: traceToMetadata(
|
|
798
|
+
metadata: traceToMetadata(activeTrace),
|
|
336
799
|
};
|
|
337
|
-
await this.config.vectorStore.upsert(collection, [doc]);
|
|
800
|
+
const upsertResult = await this.config.vectorStore.upsert(collection, [doc]);
|
|
801
|
+
if ((upsertResult?.failedCount ?? 0) > 0) {
|
|
802
|
+
throw new Error(`MemoryStore.store: vector provider rejected trace ${trace.id}; ` +
|
|
803
|
+
`${upsertResult.failedCount} document write failed`);
|
|
804
|
+
}
|
|
805
|
+
// The Brain is authoritative whenever attached. Persist before publishing
|
|
806
|
+
// the active trace to caches or secondary graph indexes; otherwise a
|
|
807
|
+
// failed write would look successful locally and then be fenced as a
|
|
808
|
+
// provider-only document on the next durable recall.
|
|
809
|
+
if (brain) {
|
|
810
|
+
try {
|
|
811
|
+
const { dialect } = brain.features;
|
|
812
|
+
const result = await brain.run(dialect.insertOrReplace('memory_traces', ['brain_id', 'id', 'type', 'scope', 'content', 'embedding', 'strength', 'created_at', 'last_accessed', 'retrieval_count', 'tags', 'emotions', 'metadata', 'deleted'], ['?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '0'], 'brain_id, id'), [
|
|
813
|
+
brain.brainId,
|
|
814
|
+
trace.id,
|
|
815
|
+
trace.type,
|
|
816
|
+
trace.scope,
|
|
817
|
+
trace.content,
|
|
818
|
+
embeddingToBlob(embedding),
|
|
819
|
+
trace.encodingStrength,
|
|
820
|
+
trace.createdAt,
|
|
821
|
+
trace.lastAccessedAt,
|
|
822
|
+
trace.retrievalCount,
|
|
823
|
+
JSON.stringify(trace.tags),
|
|
824
|
+
JSON.stringify(trace.emotionalContext),
|
|
825
|
+
JSON.stringify({
|
|
826
|
+
scopeId: trace.scopeId,
|
|
827
|
+
provenance: trace.provenance,
|
|
828
|
+
entities: trace.entities,
|
|
829
|
+
stability: trace.stability,
|
|
830
|
+
importance: trace.importance,
|
|
831
|
+
associatedTraceIds: trace.associatedTraceIds,
|
|
832
|
+
structuredData: trace.structuredData,
|
|
833
|
+
}),
|
|
834
|
+
]);
|
|
835
|
+
if (result.changes === 0) {
|
|
836
|
+
throw new Error('durable write affected no rows');
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
catch {
|
|
840
|
+
trace.isActive = false;
|
|
841
|
+
// The SQL error is ambiguous: another process may already have
|
|
842
|
+
// committed a successful store for the same trace. Fence this
|
|
843
|
+
// process non-authoritatively, but do not erase a shared provider
|
|
844
|
+
// vector that could belong to that successful writer.
|
|
845
|
+
markTraceDeleted(namespace, trace.id, false);
|
|
846
|
+
throw new Error('MemoryStore.store: durable trace write failed');
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
trace.isActive = true;
|
|
850
|
+
trace.updatedAt = activeUpdatedAt;
|
|
338
851
|
// Record in knowledge graph as episodic memory. Step 13: thread
|
|
339
852
|
// `trace.entities` through as `entityIds` (slugified for deterministic
|
|
340
853
|
// lookup). Previously hardcoded to `[]`, which silenced the sixth
|
|
@@ -375,42 +888,7 @@ export class MemoryStore {
|
|
|
375
888
|
this.traceCache.set(trace.id, trace);
|
|
376
889
|
this.embeddingCache.set(trace.id, embedding);
|
|
377
890
|
this.registerScope(trace.scope, trace.scopeId);
|
|
378
|
-
|
|
379
|
-
// the fresh trace is not filtered out of recall as a stale tombstone.
|
|
380
|
-
this.tombstonedTraceIds.delete(trace.id);
|
|
381
|
-
// Write-through to Brain for durability.
|
|
382
|
-
// The SQL row mirrors the in-memory cache so traces survive restart.
|
|
383
|
-
if (this.brain) {
|
|
384
|
-
try {
|
|
385
|
-
const { dialect } = this.brain.features;
|
|
386
|
-
await this.brain.run(dialect.insertOrReplace('memory_traces', ['brain_id', 'id', 'type', 'scope', 'content', 'embedding', 'strength', 'created_at', 'last_accessed', 'retrieval_count', 'tags', 'emotions', 'metadata', 'deleted'], ['?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '0'], 'brain_id, id'), [
|
|
387
|
-
this.brain.brainId,
|
|
388
|
-
trace.id,
|
|
389
|
-
trace.type,
|
|
390
|
-
trace.scope,
|
|
391
|
-
trace.content,
|
|
392
|
-
embeddingToBlob(embedding), // durable so recall survives a fresh instance (see ensureHydratedFromBrain)
|
|
393
|
-
trace.encodingStrength,
|
|
394
|
-
trace.createdAt,
|
|
395
|
-
trace.lastAccessedAt,
|
|
396
|
-
trace.retrievalCount,
|
|
397
|
-
JSON.stringify(trace.tags),
|
|
398
|
-
JSON.stringify(trace.emotionalContext),
|
|
399
|
-
JSON.stringify({
|
|
400
|
-
scopeId: trace.scopeId,
|
|
401
|
-
provenance: trace.provenance,
|
|
402
|
-
entities: trace.entities,
|
|
403
|
-
stability: trace.stability,
|
|
404
|
-
importance: trace.importance,
|
|
405
|
-
associatedTraceIds: trace.associatedTraceIds,
|
|
406
|
-
structuredData: trace.structuredData,
|
|
407
|
-
}),
|
|
408
|
-
]);
|
|
409
|
-
}
|
|
410
|
-
catch {
|
|
411
|
-
// Write-through is best-effort — in-memory store is primary
|
|
412
|
-
}
|
|
413
|
-
}
|
|
891
|
+
return embedding;
|
|
414
892
|
}
|
|
415
893
|
// =========================================================================
|
|
416
894
|
// Query
|
|
@@ -419,8 +897,11 @@ export class MemoryStore {
|
|
|
419
897
|
* Query memory traces with cognitive scoring.
|
|
420
898
|
*/
|
|
421
899
|
async query(queryText, currentMood, options = {}) {
|
|
900
|
+
this.beginOperation();
|
|
422
901
|
const now = Date.now();
|
|
423
902
|
const topK = options.topK ?? 20;
|
|
903
|
+
const namespace = this.coordinationNamespace;
|
|
904
|
+
const brain = this.brain;
|
|
424
905
|
// Cold-start durability: load durable traces from the attached Brain into
|
|
425
906
|
// the in-memory index (+ scope registry) before the first query, so recall
|
|
426
907
|
// survives a fresh MemoryStore instance (e.g. a new request-scoped facade).
|
|
@@ -499,6 +980,19 @@ export class MemoryStore {
|
|
|
499
980
|
// Search across scopes
|
|
500
981
|
const allCandidates = [];
|
|
501
982
|
const vectorSearchStart = Date.now();
|
|
983
|
+
if (this.tombstonedTraceIds.size > 0) {
|
|
984
|
+
const localStateAvailable = await this.reconcileDurableTraceStates([...this.tombstonedTraceIds], namespace, brain);
|
|
985
|
+
if (!localStateAvailable) {
|
|
986
|
+
return {
|
|
987
|
+
scored: [],
|
|
988
|
+
partial: [],
|
|
989
|
+
timings: {
|
|
990
|
+
vectorSearchMs: Date.now() - vectorSearchStart,
|
|
991
|
+
scoringMs: 0,
|
|
992
|
+
},
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
}
|
|
502
996
|
for (const { scope, scopeId } of scopes) {
|
|
503
997
|
const collection = collectionName(this.config.collectionPrefix, scope, scopeId);
|
|
504
998
|
try {
|
|
@@ -507,9 +1001,14 @@ export class MemoryStore {
|
|
|
507
1001
|
filter: metadataFilter,
|
|
508
1002
|
includeMetadata: true,
|
|
509
1003
|
});
|
|
1004
|
+
const durableStateAvailable = await this.reconcileDurableTraceStates(results.documents.map((document) => document.id), namespace, brain);
|
|
1005
|
+
if (!durableStateAvailable)
|
|
1006
|
+
continue;
|
|
510
1007
|
for (const result of results.documents) {
|
|
511
|
-
if (this.tombstonedTraceIds.has(result.id)
|
|
1008
|
+
if (this.tombstonedTraceIds.has(result.id) ||
|
|
1009
|
+
isTraceDeletePending(namespace, result.id)) {
|
|
512
1010
|
continue;
|
|
1011
|
+
}
|
|
513
1012
|
const tracePartial = metadataToTracePartial(result.metadata ?? {});
|
|
514
1013
|
const cached = this.traceCache.get(result.id);
|
|
515
1014
|
const trace = cached ??
|
|
@@ -574,13 +1073,16 @@ export class MemoryStore {
|
|
|
574
1073
|
weights: effectiveWeights,
|
|
575
1074
|
};
|
|
576
1075
|
const scoringStart = Date.now();
|
|
577
|
-
const
|
|
578
|
-
|
|
1076
|
+
const activeCandidates = allCandidates.filter((candidate) => candidate.trace.isActive &&
|
|
1077
|
+
!this.tombstonedTraceIds.has(candidate.trace.id) &&
|
|
1078
|
+
!isTraceDeletePending(namespace, candidate.trace.id));
|
|
1079
|
+
const scored = scoreAndRankTraces(activeCandidates, scoringContext).slice(0, topK);
|
|
1080
|
+
const partial = detectPartiallyRetrieved(activeCandidates, now, scoringContext);
|
|
579
1081
|
const scoringMs = Date.now() - scoringStart;
|
|
580
1082
|
// Cognitive mechanisms: RIF + FOK
|
|
581
1083
|
if (this.mechanismsEngine && scored.length > 0) {
|
|
582
1084
|
const cutoff = scored[scored.length - 1].retrievalScore;
|
|
583
|
-
this.mechanismsEngine.onRetrieval(scored,
|
|
1085
|
+
this.mechanismsEngine.onRetrieval(scored, activeCandidates, cutoff, []);
|
|
584
1086
|
}
|
|
585
1087
|
return { scored, partial, timings: { vectorSearchMs, scoringMs } };
|
|
586
1088
|
}
|
|
@@ -592,6 +1094,16 @@ export class MemoryStore {
|
|
|
592
1094
|
* Updates decay parameters via spaced repetition.
|
|
593
1095
|
*/
|
|
594
1096
|
async recordAccess(traceId) {
|
|
1097
|
+
this.beginOperation();
|
|
1098
|
+
const namespace = this.coordinationNamespace;
|
|
1099
|
+
const brain = this.brain;
|
|
1100
|
+
return withTraceOperation(namespace, traceId, 'normal', async ({ blockedByDelete, isDeletePending }) => {
|
|
1101
|
+
if (blockedByDelete)
|
|
1102
|
+
return null;
|
|
1103
|
+
return this.recordAccessWithTraceLock(traceId, isDeletePending, brain);
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
async recordAccessWithTraceLock(traceId, isDeletePending, brain) {
|
|
595
1107
|
const trace = this.traceCache.get(traceId);
|
|
596
1108
|
if (this.tombstonedTraceIds.has(traceId) || !trace?.isActive)
|
|
597
1109
|
return null;
|
|
@@ -648,7 +1160,7 @@ export class MemoryStore {
|
|
|
648
1160
|
}
|
|
649
1161
|
// Deletion can race the asynchronous embedding or vector refresh above.
|
|
650
1162
|
// If it won, remove any document the late refresh may have restored.
|
|
651
|
-
if (!trace.isActive) {
|
|
1163
|
+
if (isDeletePending() || !trace.isActive) {
|
|
652
1164
|
try {
|
|
653
1165
|
await this.config.vectorStore.delete(collection, [traceId]);
|
|
654
1166
|
}
|
|
@@ -659,9 +1171,9 @@ export class MemoryStore {
|
|
|
659
1171
|
return null;
|
|
660
1172
|
}
|
|
661
1173
|
// Write-through: update access metadata in the durable SQL store
|
|
662
|
-
if (
|
|
1174
|
+
if (brain) {
|
|
663
1175
|
try {
|
|
664
|
-
await
|
|
1176
|
+
await brain.run('UPDATE memory_traces SET last_accessed = ?, retrieval_count = ?, strength = ? WHERE brain_id = ? AND id = ? AND deleted = 0', [trace.lastAccessedAt, trace.retrievalCount, trace.encodingStrength, brain.brainId, traceId]);
|
|
665
1177
|
}
|
|
666
1178
|
catch {
|
|
667
1179
|
// Best-effort persistence
|
|
@@ -683,23 +1195,29 @@ export class MemoryStore {
|
|
|
683
1195
|
* topK) and does not guarantee completeness.
|
|
684
1196
|
*/
|
|
685
1197
|
async getByScope(scope, scopeId, type) {
|
|
1198
|
+
this.beginOperation();
|
|
1199
|
+
const namespace = this.coordinationNamespace;
|
|
1200
|
+
const brain = this.brain;
|
|
686
1201
|
await this.ensureHydratedFromBrain();
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
1202
|
+
const collectCachedResults = () => [...this.traceCache.values()].filter((trace) => !this.tombstonedTraceIds.has(trace.id) &&
|
|
1203
|
+
trace.isActive &&
|
|
1204
|
+
trace.scope === scope &&
|
|
1205
|
+
trace.scopeId === scopeId &&
|
|
1206
|
+
(!type || trace.type === type));
|
|
1207
|
+
let results = collectCachedResults();
|
|
1208
|
+
const collection = collectionName(this.config.collectionPrefix, scope, scopeId);
|
|
1209
|
+
const cachedStateAvailable = await this.reconcileDurableTraceStates([
|
|
1210
|
+
...results.map((trace) => trace.id),
|
|
1211
|
+
...this.tombstonedTraceIds,
|
|
1212
|
+
], namespace, brain);
|
|
1213
|
+
if (!cachedStateAvailable)
|
|
1214
|
+
return [];
|
|
1215
|
+
// Reconciliation can revive a trace while waiting behind an in-flight
|
|
1216
|
+
// store. Refresh the snapshot so that revival is visible in this call.
|
|
1217
|
+
results = collectCachedResults();
|
|
699
1218
|
// Fallback: if cache is empty for this scope, query the vector store.
|
|
700
1219
|
if (results.length === 0) {
|
|
701
1220
|
try {
|
|
702
|
-
const collection = collectionName(this.config.collectionPrefix, scope, scopeId);
|
|
703
1221
|
const dim = this.config.embeddingDimension ?? 1536;
|
|
704
1222
|
const zeroVector = new Array(dim).fill(0);
|
|
705
1223
|
const filter = { isActive: 1 };
|
|
@@ -712,9 +1230,14 @@ export class MemoryStore {
|
|
|
712
1230
|
includeMetadata: true,
|
|
713
1231
|
includeTextContent: true,
|
|
714
1232
|
});
|
|
1233
|
+
const providerStateAvailable = await this.reconcileDurableTraceStates(queryResult.documents.map((document) => document.id), namespace, brain);
|
|
1234
|
+
if (!providerStateAvailable)
|
|
1235
|
+
return [];
|
|
715
1236
|
for (const doc of queryResult.documents) {
|
|
716
|
-
if (this.tombstonedTraceIds.has(doc.id)
|
|
1237
|
+
if (this.tombstonedTraceIds.has(doc.id) ||
|
|
1238
|
+
isTraceDeletePending(namespace, doc.id)) {
|
|
717
1239
|
continue;
|
|
1240
|
+
}
|
|
718
1241
|
if (!doc.metadata)
|
|
719
1242
|
continue;
|
|
720
1243
|
const cached = this.traceCache.get(doc.id);
|
|
@@ -745,7 +1268,9 @@ export class MemoryStore {
|
|
|
745
1268
|
// Vector store query may fail (collection not found, etc.); return empty.
|
|
746
1269
|
}
|
|
747
1270
|
}
|
|
748
|
-
return results
|
|
1271
|
+
return results.filter((trace) => trace.isActive &&
|
|
1272
|
+
!this.tombstonedTraceIds.has(trace.id) &&
|
|
1273
|
+
!isTraceDeletePending(namespace, trace.id));
|
|
749
1274
|
}
|
|
750
1275
|
/**
|
|
751
1276
|
* Re-upsert a trace's metadata to the vector store using the cached
|
|
@@ -758,10 +1283,22 @@ export class MemoryStore {
|
|
|
758
1283
|
* not be durably updated.
|
|
759
1284
|
*/
|
|
760
1285
|
async persistTraceMetadata(traceId) {
|
|
1286
|
+
this.beginOperation();
|
|
1287
|
+
const namespace = this.coordinationNamespace;
|
|
1288
|
+
await withTraceOperation(namespace, traceId, 'normal', async ({ blockedByDelete, isDeletePending }) => {
|
|
1289
|
+
if (blockedByDelete)
|
|
1290
|
+
return;
|
|
1291
|
+
await this.persistTraceMetadataWithTraceLock(traceId, isDeletePending);
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
async persistTraceMetadataWithTraceLock(traceId, isDeletePending) {
|
|
761
1295
|
const trace = this.traceCache.get(traceId);
|
|
762
1296
|
const embedding = this.embeddingCache.get(traceId);
|
|
763
|
-
if (this.tombstonedTraceIds.has(traceId) ||
|
|
1297
|
+
if (this.tombstonedTraceIds.has(traceId) ||
|
|
1298
|
+
!trace?.isActive ||
|
|
1299
|
+
!embedding) {
|
|
764
1300
|
return;
|
|
1301
|
+
}
|
|
765
1302
|
const collection = collectionName(this.config.collectionPrefix, trace.scope, trace.scopeId);
|
|
766
1303
|
const doc = {
|
|
767
1304
|
id: trace.id,
|
|
@@ -771,6 +1308,9 @@ export class MemoryStore {
|
|
|
771
1308
|
};
|
|
772
1309
|
try {
|
|
773
1310
|
await this.config.vectorStore.upsert(collection, [doc]);
|
|
1311
|
+
if (isDeletePending() || !trace.isActive) {
|
|
1312
|
+
await this.config.vectorStore.delete(collection, [traceId]);
|
|
1313
|
+
}
|
|
774
1314
|
}
|
|
775
1315
|
catch {
|
|
776
1316
|
// Best-effort persistence — the in-memory mutation already
|
|
@@ -778,12 +1318,19 @@ export class MemoryStore {
|
|
|
778
1318
|
}
|
|
779
1319
|
}
|
|
780
1320
|
/**
|
|
781
|
-
* Soft-delete a trace
|
|
1321
|
+
* Soft-delete a trace and preserve the local barrier if durable persistence
|
|
1322
|
+
* cannot confirm the tombstone.
|
|
1323
|
+
*
|
|
1324
|
+
* @throws When an attached Brain cannot persist the tombstone or the trace
|
|
1325
|
+
* has no durable row to update.
|
|
782
1326
|
*/
|
|
783
1327
|
async softDelete(traceId) {
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
this.
|
|
1328
|
+
this.beginOperation();
|
|
1329
|
+
const namespace = this.coordinationNamespace;
|
|
1330
|
+
const brain = this.brain;
|
|
1331
|
+
await withTraceOperation(namespace, traceId, 'delete', async () => this.softDeleteWithTraceLock(traceId, namespace, brain));
|
|
1332
|
+
}
|
|
1333
|
+
async softDeleteWithTraceLock(traceId, namespace, brain) {
|
|
787
1334
|
let trace = this.traceCache.get(traceId);
|
|
788
1335
|
// A caller may delete before the first recall hydrates this process-local
|
|
789
1336
|
// cache. Load just the requested row so we can invalidate the correct
|
|
@@ -793,12 +1340,12 @@ export class MemoryStore {
|
|
|
793
1340
|
// retry — filtering them out made that failure permanent, because a
|
|
794
1341
|
// fresh instance found no row, skipped the vector delete entirely, and
|
|
795
1342
|
// the stale document stayed recallable forever.
|
|
796
|
-
if (!trace &&
|
|
1343
|
+
if (!trace && brain) {
|
|
797
1344
|
try {
|
|
798
|
-
const row = await
|
|
1345
|
+
const row = await brain.get(`SELECT id, type, scope, content, embedding, strength, created_at,
|
|
799
1346
|
last_accessed, retrieval_count, tags, emotions, metadata, deleted
|
|
800
1347
|
FROM memory_traces
|
|
801
|
-
WHERE brain_id = ? AND id = ?`, [
|
|
1348
|
+
WHERE brain_id = ? AND id = ?`, [brain.brainId, traceId]);
|
|
802
1349
|
if (row) {
|
|
803
1350
|
trace = this.rowToTrace(row);
|
|
804
1351
|
// A tombstoned row is loaded solely to name the collection for
|
|
@@ -821,38 +1368,36 @@ export class MemoryStore {
|
|
|
821
1368
|
trace.isActive = false;
|
|
822
1369
|
trace.updatedAt = Date.now();
|
|
823
1370
|
}
|
|
824
|
-
// Write-through: mark trace as deleted in the durable SQL store
|
|
825
|
-
|
|
1371
|
+
// Write-through: mark trace as deleted in the durable SQL store. Keep the
|
|
1372
|
+
// local delete authoritative until this succeeds so a transient SQL error
|
|
1373
|
+
// cannot be misread as an external revival on the next recall.
|
|
1374
|
+
let durableDeleteError = false;
|
|
1375
|
+
if (brain) {
|
|
826
1376
|
try {
|
|
827
|
-
await
|
|
1377
|
+
const result = await brain.run('UPDATE memory_traces SET deleted = 1 WHERE brain_id = ? AND id = ?', [brain.brainId, traceId]);
|
|
1378
|
+
if (result.changes > 0) {
|
|
1379
|
+
releaseDeleteAuthority(namespace, traceId);
|
|
1380
|
+
}
|
|
1381
|
+
else {
|
|
1382
|
+
durableDeleteError = true;
|
|
1383
|
+
}
|
|
828
1384
|
}
|
|
829
1385
|
catch {
|
|
830
|
-
|
|
1386
|
+
durableDeleteError = true;
|
|
831
1387
|
}
|
|
832
1388
|
}
|
|
833
|
-
//
|
|
834
|
-
//
|
|
835
|
-
|
|
836
|
-
if (
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
// the lookup above.
|
|
846
|
-
evicted = (result.failedCount ?? 0) === 0;
|
|
847
|
-
}
|
|
848
|
-
catch {
|
|
849
|
-
// Query and getByScope also reject cached tombstones defensively.
|
|
850
|
-
}
|
|
851
|
-
if (!evicted) {
|
|
852
|
-
console.warn(`[MemoryStore] vector eviction incomplete for trace ${traceId} in ${collection}; ` +
|
|
853
|
-
'the soft-deleted document may persist in a shared index until softDelete is retried');
|
|
854
|
-
}
|
|
855
|
-
this.embeddingCache.delete(traceId);
|
|
1389
|
+
// Every live store attached to this backing resource owns an independent
|
|
1390
|
+
// hot vector index. Evict all of them before the delete operation resolves.
|
|
1391
|
+
const evicted = await evictDeletedTrace(namespace, traceId, trace, this.coordinationTarget);
|
|
1392
|
+
if (!evicted) {
|
|
1393
|
+
const location = trace
|
|
1394
|
+
? collectionName(this.config.collectionPrefix, trace.scope, trace.scopeId)
|
|
1395
|
+
: 'an unresolved collection';
|
|
1396
|
+
console.warn(`[MemoryStore] vector eviction incomplete for trace ${traceId} in ${location}; ` +
|
|
1397
|
+
'the soft-deleted document may persist in a shared index until softDelete is retried');
|
|
1398
|
+
}
|
|
1399
|
+
if (durableDeleteError) {
|
|
1400
|
+
throw new Error('MemoryStore.softDelete: durable tombstone write failed');
|
|
856
1401
|
}
|
|
857
1402
|
}
|
|
858
1403
|
/**
|