mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 23:31:24 +00:00
fix(agents): cache bootstrap snapshots per session key
Co-authored-by: Isis Anisoptera <github@lotuswind.net>
This commit is contained in:
100
src/agents/bootstrap-cache.test.ts
Normal file
100
src/agents/bootstrap-cache.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
clearAllBootstrapSnapshots,
|
||||
clearBootstrapSnapshot,
|
||||
getOrLoadBootstrapFiles,
|
||||
} from "./bootstrap-cache.js";
|
||||
import type { WorkspaceBootstrapFile } from "./workspace.js";
|
||||
|
||||
vi.mock("./workspace.js", () => ({
|
||||
loadWorkspaceBootstrapFiles: vi.fn(),
|
||||
}));
|
||||
|
||||
import { loadWorkspaceBootstrapFiles } from "./workspace.js";
|
||||
|
||||
const mockLoad = vi.mocked(loadWorkspaceBootstrapFiles);
|
||||
|
||||
function makeFile(name: string, content: string): WorkspaceBootstrapFile {
|
||||
return {
|
||||
name: name as WorkspaceBootstrapFile["name"],
|
||||
path: `/ws/${name}`,
|
||||
content,
|
||||
missing: false,
|
||||
};
|
||||
}
|
||||
|
||||
describe("getOrLoadBootstrapFiles", () => {
|
||||
const files = [makeFile("AGENTS.md", "# Agent"), makeFile("SOUL.md", "# Soul")];
|
||||
|
||||
beforeEach(() => {
|
||||
clearAllBootstrapSnapshots();
|
||||
mockLoad.mockResolvedValue(files);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearAllBootstrapSnapshots();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("loads from disk on first call and caches", async () => {
|
||||
const result = await getOrLoadBootstrapFiles({
|
||||
workspaceDir: "/ws",
|
||||
sessionKey: "session-1",
|
||||
});
|
||||
|
||||
expect(result).toBe(files);
|
||||
expect(mockLoad).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("returns cached result on second call", async () => {
|
||||
await getOrLoadBootstrapFiles({ workspaceDir: "/ws", sessionKey: "session-1" });
|
||||
const result = await getOrLoadBootstrapFiles({ workspaceDir: "/ws", sessionKey: "session-1" });
|
||||
|
||||
expect(result).toBe(files);
|
||||
expect(mockLoad).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("different session keys get independent caches", async () => {
|
||||
const files2 = [makeFile("AGENTS.md", "# Agent v2")];
|
||||
mockLoad.mockResolvedValueOnce(files).mockResolvedValueOnce(files2);
|
||||
|
||||
const r1 = await getOrLoadBootstrapFiles({ workspaceDir: "/ws", sessionKey: "session-1" });
|
||||
const r2 = await getOrLoadBootstrapFiles({ workspaceDir: "/ws", sessionKey: "session-2" });
|
||||
|
||||
expect(r1).toBe(files);
|
||||
expect(r2).toBe(files2);
|
||||
expect(mockLoad).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearBootstrapSnapshot", () => {
|
||||
beforeEach(() => {
|
||||
clearAllBootstrapSnapshots();
|
||||
mockLoad.mockResolvedValue([makeFile("AGENTS.md", "content")]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearAllBootstrapSnapshots();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("clears a single session entry", async () => {
|
||||
await getOrLoadBootstrapFiles({ workspaceDir: "/ws", sessionKey: "sk" });
|
||||
clearBootstrapSnapshot("sk");
|
||||
|
||||
// Next call should hit disk again.
|
||||
await getOrLoadBootstrapFiles({ workspaceDir: "/ws", sessionKey: "sk" });
|
||||
expect(mockLoad).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("does not affect other sessions", async () => {
|
||||
await getOrLoadBootstrapFiles({ workspaceDir: "/ws", sessionKey: "sk1" });
|
||||
await getOrLoadBootstrapFiles({ workspaceDir: "/ws", sessionKey: "sk2" });
|
||||
|
||||
clearBootstrapSnapshot("sk1");
|
||||
|
||||
// sk2 should still be cached.
|
||||
await getOrLoadBootstrapFiles({ workspaceDir: "/ws", sessionKey: "sk2" });
|
||||
expect(mockLoad).toHaveBeenCalledTimes(2); // sk1 x1, sk2 x1
|
||||
});
|
||||
});
|
||||
25
src/agents/bootstrap-cache.ts
Normal file
25
src/agents/bootstrap-cache.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { loadWorkspaceBootstrapFiles, type WorkspaceBootstrapFile } from "./workspace.js";
|
||||
|
||||
const cache = new Map<string, WorkspaceBootstrapFile[]>();
|
||||
|
||||
export async function getOrLoadBootstrapFiles(params: {
|
||||
workspaceDir: string;
|
||||
sessionKey: string;
|
||||
}): Promise<WorkspaceBootstrapFile[]> {
|
||||
const existing = cache.get(params.sessionKey);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const files = await loadWorkspaceBootstrapFiles(params.workspaceDir);
|
||||
cache.set(params.sessionKey, files);
|
||||
return files;
|
||||
}
|
||||
|
||||
export function clearBootstrapSnapshot(sessionKey: string): void {
|
||||
cache.delete(sessionKey);
|
||||
}
|
||||
|
||||
export function clearAllBootstrapSnapshots(): void {
|
||||
cache.clear();
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { getOrLoadBootstrapFiles } from "./bootstrap-cache.js";
|
||||
import { applyBootstrapHookOverrides } from "./bootstrap-hooks.js";
|
||||
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
|
||||
import {
|
||||
@@ -49,10 +50,13 @@ export async function resolveBootstrapFilesForRun(params: {
|
||||
warn?: (message: string) => void;
|
||||
}): Promise<WorkspaceBootstrapFile[]> {
|
||||
const sessionKey = params.sessionKey ?? params.sessionId;
|
||||
const bootstrapFiles = filterBootstrapFilesForSession(
|
||||
await loadWorkspaceBootstrapFiles(params.workspaceDir),
|
||||
sessionKey,
|
||||
);
|
||||
const rawFiles = params.sessionKey
|
||||
? await getOrLoadBootstrapFiles({
|
||||
workspaceDir: params.workspaceDir,
|
||||
sessionKey: params.sessionKey,
|
||||
})
|
||||
: await loadWorkspaceBootstrapFiles(params.workspaceDir);
|
||||
const bootstrapFiles = filterBootstrapFilesForSession(rawFiles, sessionKey);
|
||||
|
||||
const updated = await applyBootstrapHookOverrides({
|
||||
files: bootstrapFiles,
|
||||
|
||||
Reference in New Issue
Block a user