perf: avoid full session snapshots for entry reads

This commit is contained in:
Peter Steinberger
2026-05-29 03:12:36 +01:00
parent c36ba9ea7a
commit d5bbf3033c
6 changed files with 42 additions and 5 deletions

View File

@@ -59,6 +59,9 @@ describe("agent runner helpers", () => {
});
expect(shouldEmitResult()).toBe(true);
expect(shouldEmitOutput()).toBe(true);
expect(hoisted.loadSessionStoreMock).toHaveBeenCalledWith("/tmp/store.json", {
clone: false,
});
});
it("caches session verbose reads briefly while still refreshing live changes", () => {

View File

@@ -27,7 +27,7 @@ function readCurrentVerboseLevel(params: VerboseGateParams): VerboseLevel | unde
return undefined;
}
try {
const store = loadSessionStore(params.storePath);
const store = loadSessionStore(params.storePath, { clone: false });
const entry = store[params.sessionKey];
return typeof entry?.verboseLevel === "string"
? normalizeVerboseLevel(entry.verboseLevel)

View File

@@ -444,6 +444,7 @@ describe("Session Store Cache", () => {
expect(readSessionUpdatedAt({ storePath, sessionKey: "agent:main:main" })).toBe(updatedAt);
expect(parseSpy).not.toHaveBeenCalled();
expect(getSessionStoreSnapshotCacheStatsForTest().entries).toBe(1);
parseSpy.mockRestore();
});
@@ -507,6 +508,33 @@ describe("Session Store Cache", () => {
expect(readSessionStoreSnapshot(storePath)["session:1"].origin?.provider).toBe("openai");
});
it("reads immutable single entries without populating whole-store snapshots", async () => {
await saveSessionStore(storePath, {
"session:1": createSessionEntry({
sessionId: "id-1",
skillsSnapshot: {
prompt: "single entry prompt ".repeat(200),
skills: [{ name: "alpha" }],
},
}),
"session:2": createSessionEntry({ sessionId: "id-2" }),
});
clearSessionStoreCacheForTest();
const entry = readSessionEntry(storePath, "session:1");
expect(entry?.sessionId).toBe("id-1");
expect(Object.isFrozen(entry)).toBe(true);
expect(Object.isFrozen(entry?.skillsSnapshot?.skills)).toBe(true);
expect(getSessionStoreSnapshotCacheStatsForTest().entries).toBe(0);
const cached = loadSessionStore(storePath, { clone: false });
expect(() => {
(entry as SessionEntry).displayName = "mutated returned entry";
}).toThrow(TypeError);
expect(cached["session:1"].displayName).toBe("Test Session 1");
});
it("does not tag snapshots with stats from writes racing after a disk read", async () => {
await saveSessionStore(
storePath,

View File

@@ -243,6 +243,10 @@ export function cloneSessionStoreSnapshot(
return deepFreeze(cloned);
}
export function cloneSessionStoreSnapshotEntry(entry: SessionEntry): SessionStoreSnapshotEntry {
return deepFreeze(cloneSessionStoreRecord({ entry }).entry);
}
export function getSessionStoreTtl(): number {
return resolveCacheTtlMs({
envValue: process.env.OPENCLAW_SESSION_CACHE_TTL_MS,

View File

@@ -13,6 +13,7 @@ import { getFileStatSnapshot } from "../cache-utils.js";
import { hydrateSessionStoreSkillPromptRefs } from "./skill-prompt-blobs.js";
import {
cloneSessionStoreRecord,
cloneSessionStoreSnapshotEntry,
cloneSessionStoreSnapshot,
internSessionEntryLargeStrings,
isSessionStoreCacheEnabled,
@@ -488,12 +489,12 @@ export function readSessionEntry(
storePath: string,
sessionKey: string,
): SessionStoreSnapshotEntry | undefined {
const snapshot = readSessionStoreSnapshot(storePath);
const store = loadSessionStore(storePath, { clone: false });
const resolved = resolveSessionStoreEntry({
store: snapshot as Record<string, SessionEntry>,
store,
sessionKey,
});
return resolved.existing as SessionStoreSnapshotEntry | undefined;
return resolved.existing ? cloneSessionStoreSnapshotEntry(resolved.existing) : undefined;
}
export function readSessionEntries(storePath: string): SessionStoreSnapshotEntries {

View File

@@ -113,7 +113,8 @@ export function readSessionUpdatedAt(params: {
sessionKey: string;
}): number | undefined {
try {
return readSessionEntry(params.storePath, params.sessionKey)?.updatedAt;
const store = loadSessionStore(params.storePath, { clone: false });
return resolveSessionStoreEntry({ store, sessionKey: params.sessionKey }).existing?.updatedAt;
} catch {
return undefined;
}