Merge ff804bc14630563011a8a5147b081dc52fdeb6eb into 5e417b44e1540f528d2ae63e3e20229a902d1db2
This commit is contained in:
commit
1af68eecc9
@ -1123,4 +1123,87 @@ describe("memory index", () => {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("shouldSyncSessions returns true for needsFullReindex even when reason is session-start or watch", async () => {
|
||||
const cfg = createCfg({ storePath: indexMainPath });
|
||||
const manager = await getPersistentManager(cfg);
|
||||
// Inject the sessions source so shouldSyncSessions passes the source guard
|
||||
const sources = (manager as unknown as { sources: Set<string> }).sources;
|
||||
sources.add("sessions");
|
||||
try {
|
||||
const shouldSync = manager as unknown as {
|
||||
shouldSyncSessions: (
|
||||
params?: { reason?: string; force?: boolean },
|
||||
needsFullReindex?: boolean,
|
||||
) => boolean;
|
||||
};
|
||||
// Core bug: reason gate must not block when needsFullReindex is true
|
||||
expect(shouldSync.shouldSyncSessions({ reason: "session-start" }, true)).toBe(true);
|
||||
expect(shouldSync.shouldSyncSessions({ reason: "watch" }, true)).toBe(true);
|
||||
// Sanity: without needsFullReindex, these reasons should still block
|
||||
expect(shouldSync.shouldSyncSessions({ reason: "session-start" }, false)).toBe(false);
|
||||
expect(shouldSync.shouldSyncSessions({ reason: "watch" }, false)).toBe(false);
|
||||
} finally {
|
||||
sources.delete("sessions");
|
||||
}
|
||||
});
|
||||
|
||||
it("restores sessionsDirty from persisted meta on manager construction", async () => {
|
||||
const storePath = path.join(workspaceDir, `index-sessions-dirty-${Date.now()}.sqlite`);
|
||||
const cfg: TestCfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: workspaceDir,
|
||||
memorySearch: {
|
||||
provider: "openai",
|
||||
model: "mock-embed",
|
||||
store: { path: storePath, vector: { enabled: false } },
|
||||
chunking: { tokens: 4000, overlap: 0 },
|
||||
sync: { watch: false, onSessionStart: false, onSearch: false },
|
||||
query: { minScore: 0, hybrid: { enabled: false } },
|
||||
sources: ["memory", "sessions"],
|
||||
experimental: { sessionMemory: true },
|
||||
},
|
||||
},
|
||||
list: [{ id: "main", default: true }],
|
||||
},
|
||||
};
|
||||
|
||||
// First manager: write meta with sessionsDirty=true directly
|
||||
const first = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(first.manager).not.toBeNull();
|
||||
if (!first.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
const firstManager = first.manager as MemoryIndexManager;
|
||||
managersForCleanup.add(firstManager);
|
||||
(firstManager as unknown as { sessionsDirty: boolean }).sessionsDirty = true;
|
||||
// Write meta that includes sessionsDirty=true
|
||||
(
|
||||
firstManager as unknown as {
|
||||
writeMeta: (meta: Record<string, unknown>) => void;
|
||||
}
|
||||
).writeMeta({
|
||||
model: "mock-embed",
|
||||
provider: "openai",
|
||||
chunkTokens: 4000,
|
||||
chunkOverlap: 0,
|
||||
sessionsDirty: true,
|
||||
});
|
||||
await firstManager.close?.();
|
||||
managersForCleanup.delete(firstManager);
|
||||
|
||||
// Second manager: should restore sessionsDirty from meta
|
||||
const second = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(second.manager).not.toBeNull();
|
||||
if (!second.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
const secondManager = second.manager as MemoryIndexManager;
|
||||
managersForCleanup.add(secondManager);
|
||||
const restoredDirty = (secondManager as unknown as { sessionsDirty: boolean }).sessionsDirty;
|
||||
expect(restoredDirty).toBe(true);
|
||||
await secondManager.close?.();
|
||||
managersForCleanup.delete(secondManager);
|
||||
});
|
||||
});
|
||||
|
||||
@ -60,6 +60,7 @@ type MemoryIndexMeta = {
|
||||
chunkTokens: number;
|
||||
chunkOverlap: number;
|
||||
vectorDims?: number;
|
||||
sessionsDirty?: boolean;
|
||||
};
|
||||
|
||||
type MemorySyncProgressState = {
|
||||
@ -683,13 +684,13 @@ export abstract class MemoryManagerSyncOps {
|
||||
if (params?.force) {
|
||||
return true;
|
||||
}
|
||||
if (needsFullReindex) {
|
||||
return true;
|
||||
}
|
||||
const reason = params?.reason;
|
||||
if (reason === "session-start" || reason === "watch") {
|
||||
return false;
|
||||
}
|
||||
if (needsFullReindex) {
|
||||
return true;
|
||||
}
|
||||
return this.sessionsDirty && this.sessionsDirtyFiles.size > 0;
|
||||
}
|
||||
|
||||
@ -1227,6 +1228,10 @@ export abstract class MemoryManagerSyncOps {
|
||||
nextMeta.vectorDims = this.vector.dims;
|
||||
}
|
||||
|
||||
if (this.sources.has("sessions")) {
|
||||
nextMeta.sessionsDirty = this.sessionsDirty;
|
||||
}
|
||||
|
||||
this.writeMeta(nextMeta);
|
||||
this.pruneEmbeddingCacheIfNeeded?.();
|
||||
|
||||
@ -1295,6 +1300,10 @@ export abstract class MemoryManagerSyncOps {
|
||||
nextMeta.vectorDims = this.vector.dims;
|
||||
}
|
||||
|
||||
if (this.sources.has("sessions")) {
|
||||
nextMeta.sessionsDirty = this.sessionsDirty;
|
||||
}
|
||||
|
||||
this.writeMeta(nextMeta);
|
||||
this.pruneEmbeddingCacheIfNeeded?.();
|
||||
}
|
||||
|
||||
@ -255,6 +255,10 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
|
||||
this.ensureIntervalSync();
|
||||
const statusOnly = params.purpose === "status";
|
||||
this.dirty = this.sources.has("memory") && (statusOnly ? !meta : true);
|
||||
if (this.sources.has("sessions") && meta?.sessionsDirty) {
|
||||
this.sessionsDirty = true;
|
||||
void this.rebuildSessionsDirtyFiles();
|
||||
}
|
||||
this.batch = this.resolveBatchConfig();
|
||||
}
|
||||
|
||||
@ -821,6 +825,30 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
|
||||
}
|
||||
}
|
||||
|
||||
private async rebuildSessionsDirtyFiles(): Promise<void> {
|
||||
try {
|
||||
const { listSessionFilesForAgent } = await import("./session-files.js");
|
||||
const files = await listSessionFilesForAgent(this.agentId);
|
||||
|
||||
for (const absPath of files) {
|
||||
const record = this.db
|
||||
.prepare(`SELECT path FROM files WHERE path = ? AND source = ?`)
|
||||
.get(absPath, "sessions") as { path: string } | undefined;
|
||||
|
||||
if (!record) {
|
||||
this.sessionsDirtyFiles.add(absPath);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("Rebuilt sessionsDirtyFiles on startup", {
|
||||
totalFiles: files.length,
|
||||
dirtyFiles: this.sessionsDirtyFiles.size,
|
||||
});
|
||||
} catch (err) {
|
||||
log.warn(`Failed to rebuild sessionsDirtyFiles: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
if (this.closed) {
|
||||
return;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user