Skip to content

Commit e6e38d6

Browse files
committed
fix(memory): persist contradictedBy through vector-store upsert
The in-memory mutation in ConsolidationPipeline.resolveConflicts was already populating provenance.contradictedBy and provenance.lastVerifiedAt on both winner and loser before soft-deleting the loser, but the changes lived only in the cache and were lost on process restart. New MemoryStore.persistTraceMetadata(traceId) re-upserts the trace's metadata to the vector store using the cached embedding, so the audit trail survives without paying for a re-embedding pass. resolveConflicts calls this for both sides before the softDelete write so a future load of either trace sees the contradiction relationship that resolved the conflict in the original session.
1 parent adc2983 commit e6e38d6

2 files changed

Lines changed: 38 additions & 0 deletions

File tree

src/cognition/memory/pipeline/consolidation/ConsolidationPipeline.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,12 @@ export class ConsolidationPipeline {
432432
const loser = trace.createdAt > other.createdAt ? other : trace;
433433
const winner = loser === trace ? other : trace;
434434
recordContradiction(winner, loser);
435+
// Persist the winner's updated audit trail before the loser is
436+
// dropped. The loser keeps its symmetric record in-memory; the
437+
// softDelete also writes the deletion to SQL so future loads see
438+
// both sides of the contradiction.
439+
await this.config.store.persistTraceMetadata(winner.id);
440+
await this.config.store.persistTraceMetadata(loser.id);
435441
await this.config.store.softDelete(loser.id);
436442
resolved++;
437443
} else {
@@ -440,6 +446,8 @@ export class ConsolidationPipeline {
440446
const loser = trace.provenance.confidence < other.provenance.confidence ? trace : other;
441447
const winner = loser === trace ? other : trace;
442448
recordContradiction(winner, loser);
449+
await this.config.store.persistTraceMetadata(winner.id);
450+
await this.config.store.persistTraceMetadata(loser.id);
443451
await this.config.store.softDelete(loser.id);
444452
resolved++;
445453
}

src/cognition/memory/retrieval/store/MemoryStore.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,36 @@ export class MemoryStore {
671671
return results;
672672
}
673673

674+
/**
675+
* Re-upsert a trace's metadata to the vector store using the cached
676+
* embedding. Used by consolidation when a mutation to the in-memory
677+
* trace (e.g. `provenance.contradictedBy`, `provenance.lastVerifiedAt`)
678+
* needs to survive a process restart without paying for re-embedding.
679+
*
680+
* No-ops silently when the trace or its embedding is not cached; the
681+
* caller should `getTrace` first or accept that an uncached trace will
682+
* not be durably updated.
683+
*/
684+
async persistTraceMetadata(traceId: string): Promise<void> {
685+
const trace = this.traceCache.get(traceId);
686+
const embedding = this.embeddingCache.get(traceId);
687+
if (!trace || !embedding) return;
688+
689+
const collection = collectionName(this.config.collectionPrefix, trace.scope, trace.scopeId);
690+
const doc: VectorDocument = {
691+
id: trace.id,
692+
textContent: trace.content,
693+
embedding,
694+
metadata: traceToMetadata(trace),
695+
};
696+
try {
697+
await this.config.vectorStore.upsert(collection, [doc]);
698+
} catch {
699+
// Best-effort persistence — the in-memory mutation already
700+
// happened, so a vector-store failure should not kill the caller.
701+
}
702+
}
703+
674704
/**
675705
* Soft-delete a trace.
676706
*/

0 commit comments

Comments
 (0)