@framers/agentos 0.10.9 → 0.10.11
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/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 +105 -9
- 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 +49 -2
- package/dist/cognition/memory/retrieval/store/MemoryStore.d.ts.map +1 -1
- package/dist/cognition/memory/retrieval/store/MemoryStore.js +702 -124
- package/dist/cognition/memory/retrieval/store/MemoryStore.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,39 +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 SQL row mirrors the in-memory cache so traces survive restart.
|
|
380
|
-
if (this.brain) {
|
|
381
|
-
try {
|
|
382
|
-
const { dialect } = this.brain.features;
|
|
383
|
-
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'), [
|
|
384
|
-
this.brain.brainId,
|
|
385
|
-
trace.id,
|
|
386
|
-
trace.type,
|
|
387
|
-
trace.scope,
|
|
388
|
-
trace.content,
|
|
389
|
-
embeddingToBlob(embedding), // durable so recall survives a fresh instance (see ensureHydratedFromBrain)
|
|
390
|
-
trace.encodingStrength,
|
|
391
|
-
trace.createdAt,
|
|
392
|
-
trace.lastAccessedAt,
|
|
393
|
-
trace.retrievalCount,
|
|
394
|
-
JSON.stringify(trace.tags),
|
|
395
|
-
JSON.stringify(trace.emotionalContext),
|
|
396
|
-
JSON.stringify({
|
|
397
|
-
scopeId: trace.scopeId,
|
|
398
|
-
provenance: trace.provenance,
|
|
399
|
-
entities: trace.entities,
|
|
400
|
-
stability: trace.stability,
|
|
401
|
-
importance: trace.importance,
|
|
402
|
-
associatedTraceIds: trace.associatedTraceIds,
|
|
403
|
-
structuredData: trace.structuredData,
|
|
404
|
-
}),
|
|
405
|
-
]);
|
|
406
|
-
}
|
|
407
|
-
catch {
|
|
408
|
-
// Write-through is best-effort — in-memory store is primary
|
|
409
|
-
}
|
|
410
|
-
}
|
|
891
|
+
return embedding;
|
|
411
892
|
}
|
|
412
893
|
// =========================================================================
|
|
413
894
|
// Query
|
|
@@ -416,8 +897,11 @@ export class MemoryStore {
|
|
|
416
897
|
* Query memory traces with cognitive scoring.
|
|
417
898
|
*/
|
|
418
899
|
async query(queryText, currentMood, options = {}) {
|
|
900
|
+
this.beginOperation();
|
|
419
901
|
const now = Date.now();
|
|
420
902
|
const topK = options.topK ?? 20;
|
|
903
|
+
const namespace = this.coordinationNamespace;
|
|
904
|
+
const brain = this.brain;
|
|
421
905
|
// Cold-start durability: load durable traces from the attached Brain into
|
|
422
906
|
// the in-memory index (+ scope registry) before the first query, so recall
|
|
423
907
|
// survives a fresh MemoryStore instance (e.g. a new request-scoped facade).
|
|
@@ -496,6 +980,19 @@ export class MemoryStore {
|
|
|
496
980
|
// Search across scopes
|
|
497
981
|
const allCandidates = [];
|
|
498
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
|
+
}
|
|
499
996
|
for (const { scope, scopeId } of scopes) {
|
|
500
997
|
const collection = collectionName(this.config.collectionPrefix, scope, scopeId);
|
|
501
998
|
try {
|
|
@@ -504,9 +1001,14 @@ export class MemoryStore {
|
|
|
504
1001
|
filter: metadataFilter,
|
|
505
1002
|
includeMetadata: true,
|
|
506
1003
|
});
|
|
1004
|
+
const durableStateAvailable = await this.reconcileDurableTraceStates(results.documents.map((document) => document.id), namespace, brain);
|
|
1005
|
+
if (!durableStateAvailable)
|
|
1006
|
+
continue;
|
|
507
1007
|
for (const result of results.documents) {
|
|
508
|
-
if (this.tombstonedTraceIds.has(result.id)
|
|
1008
|
+
if (this.tombstonedTraceIds.has(result.id) ||
|
|
1009
|
+
isTraceDeletePending(namespace, result.id)) {
|
|
509
1010
|
continue;
|
|
1011
|
+
}
|
|
510
1012
|
const tracePartial = metadataToTracePartial(result.metadata ?? {});
|
|
511
1013
|
const cached = this.traceCache.get(result.id);
|
|
512
1014
|
const trace = cached ??
|
|
@@ -571,13 +1073,16 @@ export class MemoryStore {
|
|
|
571
1073
|
weights: effectiveWeights,
|
|
572
1074
|
};
|
|
573
1075
|
const scoringStart = Date.now();
|
|
574
|
-
const
|
|
575
|
-
|
|
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);
|
|
576
1081
|
const scoringMs = Date.now() - scoringStart;
|
|
577
1082
|
// Cognitive mechanisms: RIF + FOK
|
|
578
1083
|
if (this.mechanismsEngine && scored.length > 0) {
|
|
579
1084
|
const cutoff = scored[scored.length - 1].retrievalScore;
|
|
580
|
-
this.mechanismsEngine.onRetrieval(scored,
|
|
1085
|
+
this.mechanismsEngine.onRetrieval(scored, activeCandidates, cutoff, []);
|
|
581
1086
|
}
|
|
582
1087
|
return { scored, partial, timings: { vectorSearchMs, scoringMs } };
|
|
583
1088
|
}
|
|
@@ -589,6 +1094,16 @@ export class MemoryStore {
|
|
|
589
1094
|
* Updates decay parameters via spaced repetition.
|
|
590
1095
|
*/
|
|
591
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) {
|
|
592
1107
|
const trace = this.traceCache.get(traceId);
|
|
593
1108
|
if (this.tombstonedTraceIds.has(traceId) || !trace?.isActive)
|
|
594
1109
|
return null;
|
|
@@ -645,7 +1160,7 @@ export class MemoryStore {
|
|
|
645
1160
|
}
|
|
646
1161
|
// Deletion can race the asynchronous embedding or vector refresh above.
|
|
647
1162
|
// If it won, remove any document the late refresh may have restored.
|
|
648
|
-
if (!trace.isActive) {
|
|
1163
|
+
if (isDeletePending() || !trace.isActive) {
|
|
649
1164
|
try {
|
|
650
1165
|
await this.config.vectorStore.delete(collection, [traceId]);
|
|
651
1166
|
}
|
|
@@ -656,9 +1171,9 @@ export class MemoryStore {
|
|
|
656
1171
|
return null;
|
|
657
1172
|
}
|
|
658
1173
|
// Write-through: update access metadata in the durable SQL store
|
|
659
|
-
if (
|
|
1174
|
+
if (brain) {
|
|
660
1175
|
try {
|
|
661
|
-
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]);
|
|
662
1177
|
}
|
|
663
1178
|
catch {
|
|
664
1179
|
// Best-effort persistence
|
|
@@ -680,23 +1195,29 @@ export class MemoryStore {
|
|
|
680
1195
|
* topK) and does not guarantee completeness.
|
|
681
1196
|
*/
|
|
682
1197
|
async getByScope(scope, scopeId, type) {
|
|
1198
|
+
this.beginOperation();
|
|
1199
|
+
const namespace = this.coordinationNamespace;
|
|
1200
|
+
const brain = this.brain;
|
|
683
1201
|
await this.ensureHydratedFromBrain();
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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();
|
|
696
1218
|
// Fallback: if cache is empty for this scope, query the vector store.
|
|
697
1219
|
if (results.length === 0) {
|
|
698
1220
|
try {
|
|
699
|
-
const collection = collectionName(this.config.collectionPrefix, scope, scopeId);
|
|
700
1221
|
const dim = this.config.embeddingDimension ?? 1536;
|
|
701
1222
|
const zeroVector = new Array(dim).fill(0);
|
|
702
1223
|
const filter = { isActive: 1 };
|
|
@@ -709,9 +1230,14 @@ export class MemoryStore {
|
|
|
709
1230
|
includeMetadata: true,
|
|
710
1231
|
includeTextContent: true,
|
|
711
1232
|
});
|
|
1233
|
+
const providerStateAvailable = await this.reconcileDurableTraceStates(queryResult.documents.map((document) => document.id), namespace, brain);
|
|
1234
|
+
if (!providerStateAvailable)
|
|
1235
|
+
return [];
|
|
712
1236
|
for (const doc of queryResult.documents) {
|
|
713
|
-
if (this.tombstonedTraceIds.has(doc.id)
|
|
1237
|
+
if (this.tombstonedTraceIds.has(doc.id) ||
|
|
1238
|
+
isTraceDeletePending(namespace, doc.id)) {
|
|
714
1239
|
continue;
|
|
1240
|
+
}
|
|
715
1241
|
if (!doc.metadata)
|
|
716
1242
|
continue;
|
|
717
1243
|
const cached = this.traceCache.get(doc.id);
|
|
@@ -742,7 +1268,9 @@ export class MemoryStore {
|
|
|
742
1268
|
// Vector store query may fail (collection not found, etc.); return empty.
|
|
743
1269
|
}
|
|
744
1270
|
}
|
|
745
|
-
return results
|
|
1271
|
+
return results.filter((trace) => trace.isActive &&
|
|
1272
|
+
!this.tombstonedTraceIds.has(trace.id) &&
|
|
1273
|
+
!isTraceDeletePending(namespace, trace.id));
|
|
746
1274
|
}
|
|
747
1275
|
/**
|
|
748
1276
|
* Re-upsert a trace's metadata to the vector store using the cached
|
|
@@ -755,10 +1283,22 @@ export class MemoryStore {
|
|
|
755
1283
|
* not be durably updated.
|
|
756
1284
|
*/
|
|
757
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) {
|
|
758
1295
|
const trace = this.traceCache.get(traceId);
|
|
759
1296
|
const embedding = this.embeddingCache.get(traceId);
|
|
760
|
-
if (this.tombstonedTraceIds.has(traceId) ||
|
|
1297
|
+
if (this.tombstonedTraceIds.has(traceId) ||
|
|
1298
|
+
!trace?.isActive ||
|
|
1299
|
+
!embedding) {
|
|
761
1300
|
return;
|
|
1301
|
+
}
|
|
762
1302
|
const collection = collectionName(this.config.collectionPrefix, trace.scope, trace.scopeId);
|
|
763
1303
|
const doc = {
|
|
764
1304
|
id: trace.id,
|
|
@@ -768,6 +1308,9 @@ export class MemoryStore {
|
|
|
768
1308
|
};
|
|
769
1309
|
try {
|
|
770
1310
|
await this.config.vectorStore.upsert(collection, [doc]);
|
|
1311
|
+
if (isDeletePending() || !trace.isActive) {
|
|
1312
|
+
await this.config.vectorStore.delete(collection, [traceId]);
|
|
1313
|
+
}
|
|
771
1314
|
}
|
|
772
1315
|
catch {
|
|
773
1316
|
// Best-effort persistence — the in-memory mutation already
|
|
@@ -775,30 +1318,46 @@ export class MemoryStore {
|
|
|
775
1318
|
}
|
|
776
1319
|
}
|
|
777
1320
|
/**
|
|
778
|
-
* 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.
|
|
779
1326
|
*/
|
|
780
1327
|
async softDelete(traceId) {
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
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) {
|
|
784
1334
|
let trace = this.traceCache.get(traceId);
|
|
785
1335
|
// A caller may delete before the first recall hydrates this process-local
|
|
786
1336
|
// cache. Load just the requested row so we can invalidate the correct
|
|
787
|
-
// vector collection without paying the full hydration cost.
|
|
788
|
-
|
|
1337
|
+
// vector collection without paying the full hydration cost. Tombstoned
|
|
1338
|
+
// rows are included deliberately: a prior delete whose vector eviction
|
|
1339
|
+
// failed (see below) must still be able to re-derive the collection and
|
|
1340
|
+
// retry — filtering them out made that failure permanent, because a
|
|
1341
|
+
// fresh instance found no row, skipped the vector delete entirely, and
|
|
1342
|
+
// the stale document stayed recallable forever.
|
|
1343
|
+
if (!trace && brain) {
|
|
789
1344
|
try {
|
|
790
|
-
const row = await
|
|
791
|
-
last_accessed, retrieval_count, tags, emotions, metadata
|
|
1345
|
+
const row = await brain.get(`SELECT id, type, scope, content, embedding, strength, created_at,
|
|
1346
|
+
last_accessed, retrieval_count, tags, emotions, metadata, deleted
|
|
792
1347
|
FROM memory_traces
|
|
793
|
-
WHERE brain_id = ? AND id =
|
|
1348
|
+
WHERE brain_id = ? AND id = ?`, [brain.brainId, traceId]);
|
|
794
1349
|
if (row) {
|
|
795
1350
|
trace = this.rowToTrace(row);
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
if (
|
|
799
|
-
this.
|
|
1351
|
+
// A tombstoned row is loaded solely to name the collection for
|
|
1352
|
+
// (re-)eviction — never recached, so it cannot re-enter recall.
|
|
1353
|
+
if (row.deleted !== 1) {
|
|
1354
|
+
this.traceCache.set(trace.id, trace);
|
|
1355
|
+
const embedding = blobToEmbedding(row.embedding);
|
|
1356
|
+
if (embedding && isUsableEmbedding(embedding)) {
|
|
1357
|
+
this.embeddingCache.set(trace.id, embedding);
|
|
1358
|
+
}
|
|
1359
|
+
this.registerScope(trace.scope, trace.scopeId);
|
|
800
1360
|
}
|
|
801
|
-
this.registerScope(trace.scope, trace.scopeId);
|
|
802
1361
|
}
|
|
803
1362
|
}
|
|
804
1363
|
catch {
|
|
@@ -809,27 +1368,36 @@ export class MemoryStore {
|
|
|
809
1368
|
trace.isActive = false;
|
|
810
1369
|
trace.updatedAt = Date.now();
|
|
811
1370
|
}
|
|
812
|
-
// Write-through: mark trace as deleted in the durable SQL store
|
|
813
|
-
|
|
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) {
|
|
814
1376
|
try {
|
|
815
|
-
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
|
+
}
|
|
816
1384
|
}
|
|
817
1385
|
catch {
|
|
818
|
-
|
|
1386
|
+
durableDeleteError = true;
|
|
819
1387
|
}
|
|
820
1388
|
}
|
|
821
|
-
//
|
|
822
|
-
//
|
|
823
|
-
|
|
824
|
-
if (
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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');
|
|
833
1401
|
}
|
|
834
1402
|
}
|
|
835
1403
|
/**
|
|
@@ -838,6 +1406,16 @@ export class MemoryStore {
|
|
|
838
1406
|
getTrace(traceId) {
|
|
839
1407
|
return this.traceCache.get(traceId);
|
|
840
1408
|
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Whether this store has soft-deleted the trace in this process. Lifecycle
|
|
1411
|
+
* hooks that re-activate traces leaving working memory MUST consult this so
|
|
1412
|
+
* a tombstone's `isActive` flag is never resurrected — a resurrected flag
|
|
1413
|
+
* let the spaced-repetition sweep re-embed and re-upsert deleted memories
|
|
1414
|
+
* back into shared vector recall.
|
|
1415
|
+
*/
|
|
1416
|
+
isDeleted(traceId) {
|
|
1417
|
+
return this.tombstonedTraceIds.has(traceId);
|
|
1418
|
+
}
|
|
841
1419
|
/**
|
|
842
1420
|
* Get trace count.
|
|
843
1421
|
*/
|