Skip to content

Commit 6fa4ca0

Browse files
committed
fix(memory): complete reindex retry recovery
1 parent 4b24143 commit 6fa4ca0

5 files changed

Lines changed: 173 additions & 28 deletions

File tree

extensions/memory-core/src/memory/manager-embedding-ops.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -241,14 +241,20 @@ export abstract class MemoryManagerEmbeddingOps extends MemoryManagerSyncOps {
241241
tableName: EMBEDDING_CACHE_TABLE,
242242
});
243243
if (this.embeddingCacheMirrorDb && this.embeddingCacheMirrorDb !== this.db) {
244-
upsertMemoryEmbeddingCache({
245-
db: this.embeddingCacheMirrorDb,
246-
enabled: this.cache.enabled,
247-
provider,
248-
providerKey: this.providerKey,
249-
entries,
250-
tableName: EMBEDDING_CACHE_TABLE,
251-
});
244+
try {
245+
upsertMemoryEmbeddingCache({
246+
db: this.embeddingCacheMirrorDb,
247+
enabled: this.cache.enabled,
248+
provider,
249+
providerKey: this.providerKey,
250+
entries,
251+
tableName: EMBEDDING_CACHE_TABLE,
252+
});
253+
} catch (err) {
254+
log.warn("memory embeddings: failed to mirror safe-reindex cache batch", {
255+
error: formatErrorMessage(err),
256+
});
257+
}
252258
}
253259
}
254260

extensions/memory-core/src/memory/manager-sync-ops.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ type MemorySessionDeltaState = { lastSize: number; pendingBytes: number; pending
122122

123123
type MemoryReindexRetryState = {
124124
dirty: boolean;
125+
memoryFullRetryDirty: boolean;
125126
sessionsDirty: boolean;
126127
sessionsFullRetryDirty: boolean;
127128
sessionsDirtyFiles: Set<string>;
@@ -275,6 +276,9 @@ export abstract class MemoryManagerSyncOps {
275276
protected memoryWatchPressureStartupTimer: NodeJS.Timeout | null = null;
276277
protected closed = false;
277278
protected dirty = false;
279+
// Failed full memory reindexes must retry as full rebuilds, not incremental
280+
// dirty syncs that can skip unchanged files against the still-live index.
281+
protected memoryFullRetryDirty = false;
278282
protected pendingWatchPaths: MemoryWatchSettleQueue = new Map();
279283
protected sessionsDirty = false;
280284
// Failed full reindexes can start with no per-file dirty set. Keep a
@@ -324,6 +328,7 @@ export abstract class MemoryManagerSyncOps {
324328
private snapshotReindexRetryState(): MemoryReindexRetryState {
325329
return {
326330
dirty: this.dirty,
331+
memoryFullRetryDirty: this.memoryFullRetryDirty,
327332
sessionsDirty: this.sessionsDirty,
328333
sessionsFullRetryDirty: this.sessionsFullRetryDirty,
329334
sessionsDirtyFiles: new Set(this.sessionsDirtyFiles),
@@ -335,6 +340,7 @@ export abstract class MemoryManagerSyncOps {
335340

336341
private restoreReindexRetryState(snapshot: MemoryReindexRetryState): void {
337342
this.dirty = snapshot.dirty || this.dirty;
343+
this.memoryFullRetryDirty = snapshot.memoryFullRetryDirty || this.memoryFullRetryDirty;
338344
this.sessionsFullRetryDirty = snapshot.sessionsFullRetryDirty || this.sessionsFullRetryDirty;
339345
this.sessionsDirtyFiles = new Set([...snapshot.sessionsDirtyFiles, ...this.sessionsDirtyFiles]);
340346
const currentDeltas = this.sessionDeltas;
@@ -354,6 +360,7 @@ export abstract class MemoryManagerSyncOps {
354360
private markFailedFullReindexRetry(params: { memory: boolean; sessions: boolean }): void {
355361
if (params.memory) {
356362
this.dirty = true;
363+
this.memoryFullRetryDirty = true;
357364
}
358365
if (params.sessions) {
359366
this.sessionsDirty = true;
@@ -367,6 +374,11 @@ export abstract class MemoryManagerSyncOps {
367374
this.sessionsDirtyFiles.clear();
368375
}
369376

377+
private clearMemoryRetryState(): void {
378+
this.dirty = false;
379+
this.memoryFullRetryDirty = false;
380+
}
381+
370382
private refreshSessionDirtyFlag(): void {
371383
this.sessionsDirty = this.sessionsFullRetryDirty || this.sessionsDirtyFiles.size > 0;
372384
}
@@ -428,6 +440,7 @@ export abstract class MemoryManagerSyncOps {
428440
shouldSyncMemory: boolean;
429441
shouldSyncSessions: boolean;
430442
needsFullReindex: boolean;
443+
needsFullSessionReindex?: boolean;
431444
targetSessionFiles?: string[];
432445
progress?: MemorySyncProgressState;
433446
}): Promise<void> {
@@ -440,7 +453,7 @@ export abstract class MemoryManagerSyncOps {
440453
: this.emptySourceSyncPlan();
441454
if (params.shouldSyncSessions) {
442455
await this.syncSessionFiles({
443-
needsFullReindex: params.needsFullReindex,
456+
needsFullReindex: params.needsFullSessionReindex ?? params.needsFullReindex,
444457
targetSessionFiles: params.targetSessionFiles,
445458
progress: params.progress,
446459
deferIndex: true,
@@ -2169,7 +2182,9 @@ export abstract class MemoryManagerSyncOps {
21692182
(params?.force && !hasTargetSessionFiles) ||
21702183
needsInitialIndex ||
21712184
needsMissingIdentityReindex ||
2172-
needsExplicitIdentityReindex;
2185+
needsExplicitIdentityReindex ||
2186+
this.memoryFullRetryDirty;
2187+
const needsFullSessionReindex = needsFullReindex || this.sessionsFullRetryDirty;
21732188
if (indexIdentity.status !== "valid" && !needsFullReindex) {
21742189
this.dirty = true;
21752190
const sessionsDirty = markMemoryTargetSessionFilesDirty({
@@ -2181,12 +2196,13 @@ export abstract class MemoryManagerSyncOps {
21812196
}
21822197
return;
21832198
}
2184-
if (!needsFullReindex) {
2199+
if (!needsFullSessionReindex) {
21852200
const targetedSessionSync = await runMemoryTargetedSessionSync({
21862201
hasSessionSource: this.sources.has("sessions"),
21872202
targetSessionFiles,
21882203
reason: params?.reason,
21892204
progress: progress ?? undefined,
2205+
sessionsFullRetryDirty: this.sessionsFullRetryDirty,
21902206
sessionsDirtyFiles: this.sessionsDirtyFiles,
21912207
syncSessionFiles: async (targetedParams) => {
21922208
await this.syncSessionFiles(targetedParams);
@@ -2230,11 +2246,12 @@ export abstract class MemoryManagerSyncOps {
22302246
shouldSyncMemory,
22312247
shouldSyncSessions,
22322248
needsFullReindex,
2249+
needsFullSessionReindex,
22332250
targetSessionFiles: targetSessionFiles ? Array.from(targetSessionFiles) : undefined,
22342251
progress: progress ?? undefined,
22352252
});
22362253
if (shouldSyncMemory) {
2237-
this.dirty = false;
2254+
this.clearMemoryRetryState();
22382255
}
22392256
if (shouldSyncSessions) {
22402257
this.clearSessionRetryState();
@@ -2244,12 +2261,12 @@ export abstract class MemoryManagerSyncOps {
22442261
} else {
22452262
if (shouldSyncMemory) {
22462263
await this.syncMemoryFiles({ needsFullReindex, progress: progress ?? undefined });
2247-
this.dirty = false;
2264+
this.clearMemoryRetryState();
22482265
}
22492266

22502267
if (shouldSyncSessions) {
22512268
await this.syncSessionFiles({
2252-
needsFullReindex,
2269+
needsFullReindex: needsFullSessionReindex,
22532270
targetSessionFiles: targetSessionFiles ? Array.from(targetSessionFiles) : undefined,
22542271
progress: progress ?? undefined,
22552272
});
@@ -2430,7 +2447,7 @@ export abstract class MemoryManagerSyncOps {
24302447
progress: params.progress,
24312448
});
24322449
if (shouldSyncMemory) {
2433-
this.dirty = false;
2450+
this.clearMemoryRetryState();
24342451
}
24352452
if (shouldSyncSessions) {
24362453
this.clearSessionRetryState();
@@ -2440,7 +2457,7 @@ export abstract class MemoryManagerSyncOps {
24402457
} else {
24412458
if (shouldSyncMemory) {
24422459
await this.syncMemoryFiles({ needsFullReindex: true, progress: params.progress });
2443-
this.dirty = false;
2460+
this.clearMemoryRetryState();
24442461
}
24452462

24462463
if (shouldSyncSessions) {
@@ -2538,7 +2555,7 @@ export abstract class MemoryManagerSyncOps {
25382555
progress: params.progress,
25392556
});
25402557
if (shouldSyncMemory) {
2541-
this.dirty = false;
2558+
this.clearMemoryRetryState();
25422559
}
25432560
if (shouldSyncSessions) {
25442561
this.clearSessionRetryState();
@@ -2548,7 +2565,7 @@ export abstract class MemoryManagerSyncOps {
25482565
} else {
25492566
if (shouldSyncMemory) {
25502567
await this.syncMemoryFiles({ needsFullReindex: true, progress: params.progress });
2551-
this.dirty = false;
2568+
this.clearMemoryRetryState();
25522569
}
25532570

25542571
if (shouldSyncSessions) {

extensions/memory-core/src/memory/manager-targeted-sync.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,24 @@ describe("memory targeted session sync", () => {
6464
expect(sessionsDirtyFiles.has("/tmp/targeted-fallback.jsonl")).toBe(true);
6565
expect(sessionsDirtyFiles.has("/tmp/other-dirty.jsonl")).toBe(true);
6666
});
67+
68+
it("preserves the full-retry dirty marker after targeted cleanup", async () => {
69+
const syncSessionFiles = vi.fn(async () => undefined);
70+
const sessionsDirtyFiles = new Set(["/tmp/targeted-full-retry.jsonl"]);
71+
72+
const result = await runMemoryTargetedSessionSync({
73+
hasSessionSource: true,
74+
targetSessionFiles: new Set(["/tmp/targeted-full-retry.jsonl"]),
75+
reason: "post-compaction",
76+
progress: undefined,
77+
sessionsFullRetryDirty: true,
78+
sessionsDirtyFiles,
79+
syncSessionFiles,
80+
shouldFallbackOnError: () => false,
81+
activateFallbackProvider: async () => false,
82+
});
83+
84+
expect(result).toEqual({ handled: true, sessionsDirty: true });
85+
expect(sessionsDirtyFiles.size).toBe(0);
86+
});
6787
});

extensions/memory-core/src/memory/manager-targeted-sync.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export async function runMemoryTargetedSessionSync(params: {
4040
targetSessionFiles: Set<string> | null;
4141
reason?: string;
4242
progress?: TargetedSyncProgress;
43+
sessionsFullRetryDirty?: boolean;
4344
sessionsDirtyFiles: Set<string>;
4445
syncSessionFiles: (params: {
4546
needsFullReindex: boolean;
@@ -52,7 +53,7 @@ export async function runMemoryTargetedSessionSync(params: {
5253
if (!params.hasSessionSource || !params.targetSessionFiles) {
5354
return {
5455
handled: false,
55-
sessionsDirty: params.sessionsDirtyFiles.size > 0,
56+
sessionsDirty: Boolean(params.sessionsFullRetryDirty) || params.sessionsDirtyFiles.size > 0,
5657
};
5758
}
5859

@@ -62,12 +63,13 @@ export async function runMemoryTargetedSessionSync(params: {
6263
targetSessionFiles: Array.from(params.targetSessionFiles),
6364
progress: params.progress,
6465
});
66+
const remainingSessionsDirty = clearMemorySyncedSessionFiles({
67+
sessionsDirtyFiles: params.sessionsDirtyFiles,
68+
targetSessionFiles: params.targetSessionFiles,
69+
});
6570
return {
6671
handled: true,
67-
sessionsDirty: clearMemorySyncedSessionFiles({
68-
sessionsDirtyFiles: params.sessionsDirtyFiles,
69-
targetSessionFiles: params.targetSessionFiles,
70-
}),
72+
sessionsDirty: Boolean(params.sessionsFullRetryDirty) || remainingSessionsDirty,
7173
};
7274
} catch (err) {
7375
const reason = formatErrorMessage(err);
@@ -76,12 +78,13 @@ export async function runMemoryTargetedSessionSync(params: {
7678
if (!activated) {
7779
throw err;
7880
}
81+
const remainingSessionsDirty = markMemoryTargetSessionFilesDirty({
82+
sessionsDirtyFiles: params.sessionsDirtyFiles,
83+
targetSessionFiles: params.targetSessionFiles,
84+
});
7985
return {
8086
handled: true,
81-
sessionsDirty: markMemoryTargetSessionFilesDirty({
82-
sessionsDirtyFiles: params.sessionsDirtyFiles,
83-
targetSessionFiles: params.targetSessionFiles,
84-
}),
87+
sessionsDirty: Boolean(params.sessionsFullRetryDirty) || remainingSessionsDirty,
8588
};
8689
}
8790
}

0 commit comments

Comments
 (0)