Skip to content

Commit d2a03ec

Browse files
committed
perf: extract memory session sync state helpers
1 parent a690eaf commit d2a03ec

4 files changed

Lines changed: 104 additions & 104 deletions

File tree

extensions/memory-core/src/memory/index.test.ts

Lines changed: 0 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -391,100 +391,6 @@ describe("memory index", () => {
391391
await manager.close?.();
392392
});
393393

394-
it("targets explicit session files during post-compaction sync", async () => {
395-
const stateDir = path.join(fixtureRoot, `state-targeted-${randomUUID()}`);
396-
const sessionDir = path.join(stateDir, "agents", "main", "sessions");
397-
const firstSessionPath = path.join(sessionDir, "targeted-first.jsonl");
398-
const secondSessionPath = path.join(sessionDir, "targeted-second.jsonl");
399-
const storePath = path.join(workspaceDir, `index-targeted-${randomUUID()}.sqlite`);
400-
const previousStateDir = process.env.OPENCLAW_STATE_DIR;
401-
process.env.OPENCLAW_STATE_DIR = stateDir;
402-
403-
await fs.mkdir(sessionDir, { recursive: true });
404-
await fs.writeFile(
405-
firstSessionPath,
406-
`${JSON.stringify({
407-
type: "message",
408-
message: { role: "user", content: [{ type: "text", text: "first transcript v1" }] },
409-
})}\n`,
410-
);
411-
await fs.writeFile(
412-
secondSessionPath,
413-
`${JSON.stringify({
414-
type: "message",
415-
message: { role: "user", content: [{ type: "text", text: "second transcript v1" }] },
416-
})}\n`,
417-
);
418-
419-
try {
420-
const result = await getMemorySearchManager({
421-
cfg: createCfg({
422-
storePath,
423-
sources: ["sessions"],
424-
sessionMemory: true,
425-
}),
426-
agentId: "main",
427-
});
428-
const manager = requireManager(result);
429-
await manager.sync?.({ reason: "test" });
430-
431-
const db = (
432-
manager as unknown as {
433-
db: {
434-
prepare: (sql: string) => {
435-
get: (path: string, source: string) => { hash: string } | undefined;
436-
all?: (...args: unknown[]) => unknown;
437-
};
438-
};
439-
}
440-
).db;
441-
const getSessionHash = (sessionPath: string) =>
442-
db
443-
.prepare(`SELECT hash FROM files WHERE path = ? AND source = ?`)
444-
.get(sessionPath, "sessions")?.hash;
445-
446-
const firstOriginalHash = getSessionHash("sessions/targeted-first.jsonl");
447-
const secondOriginalHash = getSessionHash("sessions/targeted-second.jsonl");
448-
449-
await fs.writeFile(
450-
firstSessionPath,
451-
`${JSON.stringify({
452-
type: "message",
453-
message: {
454-
role: "user",
455-
content: [{ type: "text", text: "first transcript v2 after compaction" }],
456-
},
457-
})}\n`,
458-
);
459-
await fs.writeFile(
460-
secondSessionPath,
461-
`${JSON.stringify({
462-
type: "message",
463-
message: {
464-
role: "user",
465-
content: [{ type: "text", text: "second transcript v2 should stay untouched" }],
466-
},
467-
})}\n`,
468-
);
469-
470-
await manager.sync?.({
471-
reason: "post-compaction",
472-
sessionFiles: [firstSessionPath],
473-
});
474-
475-
expect(getSessionHash("sessions/targeted-first.jsonl")).not.toBe(firstOriginalHash);
476-
expect(getSessionHash("sessions/targeted-second.jsonl")).toBe(secondOriginalHash);
477-
await manager.close?.();
478-
} finally {
479-
if (previousStateDir === undefined) {
480-
delete process.env.OPENCLAW_STATE_DIR;
481-
} else {
482-
process.env.OPENCLAW_STATE_DIR = previousStateDir;
483-
}
484-
await fs.rm(stateDir, { recursive: true, force: true });
485-
}
486-
});
487-
488394
it.skip("finds keyword matches via hybrid search when query embedding is zero", async () => {
489395
await expectHybridKeywordSearchFindsMemory(
490396
createCfg({
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { describe, expect, it } from "vitest";
2+
import { resolveMemorySessionSyncPlan } from "./manager-session-sync-state.js";
3+
4+
describe("memory session sync state", () => {
5+
it("tracks active paths and bulk hashes for full scans", () => {
6+
const plan = resolveMemorySessionSyncPlan({
7+
needsFullReindex: false,
8+
files: ["/tmp/a.jsonl", "/tmp/b.jsonl"],
9+
targetSessionFiles: null,
10+
sessionsDirtyFiles: new Set(),
11+
existingRows: [
12+
{ path: "sessions/a.jsonl", hash: "hash-a" },
13+
{ path: "sessions/b.jsonl", hash: "hash-b" },
14+
],
15+
sessionPathForFile: (file) => `sessions/${file.split("/").at(-1)}`,
16+
});
17+
18+
expect(plan.indexAll).toBe(true);
19+
expect(plan.activePaths).toEqual(new Set(["sessions/a.jsonl", "sessions/b.jsonl"]));
20+
expect(plan.existingRows).toEqual([
21+
{ path: "sessions/a.jsonl", hash: "hash-a" },
22+
{ path: "sessions/b.jsonl", hash: "hash-b" },
23+
]);
24+
expect(plan.existingHashes).toEqual(
25+
new Map([
26+
["sessions/a.jsonl", "hash-a"],
27+
["sessions/b.jsonl", "hash-b"],
28+
]),
29+
);
30+
});
31+
32+
it("treats targeted session syncs as refresh-only and skips unrelated pruning", () => {
33+
const plan = resolveMemorySessionSyncPlan({
34+
needsFullReindex: false,
35+
files: ["/tmp/targeted-first.jsonl"],
36+
targetSessionFiles: new Set(["/tmp/targeted-first.jsonl"]),
37+
sessionsDirtyFiles: new Set(["/tmp/targeted-first.jsonl"]),
38+
existingRows: [
39+
{ path: "sessions/targeted-first.jsonl", hash: "hash-first" },
40+
{ path: "sessions/targeted-second.jsonl", hash: "hash-second" },
41+
],
42+
sessionPathForFile: (file) => `sessions/${file.split("/").at(-1)}`,
43+
});
44+
45+
expect(plan.indexAll).toBe(true);
46+
expect(plan.activePaths).toBeNull();
47+
expect(plan.existingRows).toBeNull();
48+
expect(plan.existingHashes).toBeNull();
49+
});
50+
51+
it("keeps dirty-only incremental mode when no targeted sync is requested", () => {
52+
const plan = resolveMemorySessionSyncPlan({
53+
needsFullReindex: false,
54+
files: ["/tmp/incremental.jsonl"],
55+
targetSessionFiles: null,
56+
sessionsDirtyFiles: new Set(["/tmp/incremental.jsonl"]),
57+
existingRows: [],
58+
sessionPathForFile: (file) => `sessions/${file.split("/").at(-1)}`,
59+
});
60+
61+
expect(plan.indexAll).toBe(false);
62+
expect(plan.activePaths).toEqual(new Set(["sessions/incremental.jsonl"]));
63+
});
64+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { type MemorySourceFileStateRow } from "./manager-source-state.js";
2+
3+
export function resolveMemorySessionSyncPlan(params: {
4+
needsFullReindex: boolean;
5+
files: string[];
6+
targetSessionFiles: Set<string> | null;
7+
sessionsDirtyFiles: Set<string>;
8+
existingRows?: MemorySourceFileStateRow[] | null;
9+
sessionPathForFile: (file: string) => string;
10+
}): {
11+
activePaths: Set<string> | null;
12+
existingRows: MemorySourceFileStateRow[] | null;
13+
existingHashes: Map<string, string> | null;
14+
indexAll: boolean;
15+
} {
16+
const activePaths = params.targetSessionFiles
17+
? null
18+
: new Set(params.files.map((file) => params.sessionPathForFile(file)));
19+
const existingRows = activePaths === null ? null : (params.existingRows ?? []);
20+
return {
21+
activePaths,
22+
existingRows,
23+
existingHashes: existingRows ? new Map(existingRows.map((row) => [row.path, row.hash])) : null,
24+
indexAll:
25+
params.needsFullReindex ||
26+
Boolean(params.targetSessionFiles) ||
27+
params.sessionsDirtyFiles.size === 0,
28+
};
29+
}

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

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import {
5555
type MemoryIndexMeta,
5656
} from "./manager-reindex-state.js";
5757
import { shouldSyncSessionsForReindex } from "./manager-session-reindex.js";
58+
import { resolveMemorySessionSyncPlan } from "./manager-session-sync-state.js";
5859
import {
5960
loadMemorySourceFileState,
6061
resolveMemorySourceExistingHash,
@@ -753,20 +754,20 @@ export abstract class MemoryManagerSyncOps {
753754
const files = targetSessionFiles
754755
? Array.from(targetSessionFiles)
755756
: await listSessionFilesForAgent(this.agentId);
756-
const activePaths = targetSessionFiles
757-
? null
758-
: new Set(files.map((file) => sessionPathForFile(file)));
759-
const existingRows =
760-
activePaths === null
757+
const sessionPlan = resolveMemorySessionSyncPlan({
758+
needsFullReindex: params.needsFullReindex,
759+
files,
760+
targetSessionFiles,
761+
sessionsDirtyFiles: this.sessionsDirtyFiles,
762+
existingRows: targetSessionFiles
761763
? null
762764
: loadMemorySourceFileState({
763765
db: this.db,
764766
source: "sessions",
765-
}).rows;
766-
const existingHashes =
767-
existingRows === null ? null : new Map(existingRows.map((row) => [row.path, row.hash]));
768-
const indexAll =
769-
params.needsFullReindex || Boolean(targetSessionFiles) || this.sessionsDirtyFiles.size === 0;
767+
}).rows,
768+
sessionPathForFile,
769+
});
770+
const { activePaths, existingRows, existingHashes, indexAll } = sessionPlan;
770771
log.debug("memory sync: indexing session files", {
771772
files: files.length,
772773
indexAll,

0 commit comments

Comments
 (0)