perf(agent-codex): cache findCodexSessionFile to avoid double scan

Add a 30-second TTL cache for session file lookups so that
getActivityState, getSessionInfo, and getRestoreCommand called in the
same refresh cycle reuse the result instead of re-scanning
~/.codex/sessions/ independently.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
suraj-markup 2026-02-25 16:25:24 +05:30
parent c307ae30e0
commit 433189a79b
2 changed files with 29 additions and 4 deletions

View File

@ -62,7 +62,7 @@ vi.mock("node:os", () => ({
}));
import { Readable } from "node:stream";
import { create, manifest, default as defaultExport, resolveCodexBinary } from "./index.js";
import { create, manifest, default as defaultExport, resolveCodexBinary, _resetSessionFileCache } from "./index.js";
// ---------------------------------------------------------------------------
// Test helpers
@ -166,6 +166,7 @@ function setupMockStream(content: string) {
beforeEach(() => {
vi.clearAllMocks();
_resetSessionFileCache();
mockHomedir.mockReturnValue("/mock/home");
// Default: open() returns a handle with empty content (no session_meta match).
// Session tests call setupMockOpen(content) to override.

View File

@ -538,6 +538,25 @@ function appendModelFlags(parts: string[], model: string | undefined): void {
}
}
/** TTL for session file path cache (ms). Prevents redundant filesystem scans
* when getActivityState and getSessionInfo are called in the same refresh cycle. */
const SESSION_FILE_CACHE_TTL_MS = 30_000;
/** Module-level session file cache shared across the agent instance lifetime.
* Keyed by workspace path, stores the resolved file path and an expiry timestamp. */
const sessionFileCache = new Map<string, { path: string | null; expiry: number }>();
/** Find session file with caching to avoid double scans per refresh cycle */
async function findCodexSessionFileCached(workspacePath: string): Promise<string | null> {
const cached = sessionFileCache.get(workspacePath);
if (cached && Date.now() < cached.expiry) {
return cached.path;
}
const result = await findCodexSessionFile(workspacePath);
sessionFileCache.set(workspacePath, { path: result, expiry: Date.now() + SESSION_FILE_CACHE_TTL_MS });
return result;
}
function createCodexAgent(): Agent {
/** Cached resolved binary path (populated by init or first getLaunchCommand) */
let resolvedBinary: string | null = null;
@ -621,7 +640,7 @@ function createCodexAgent(): Agent {
// modified file means the agent is active.
if (!session.workspacePath) return null;
const sessionFile = await findCodexSessionFile(session.workspacePath);
const sessionFile = await findCodexSessionFileCached(session.workspacePath);
if (!sessionFile) return null;
try {
@ -695,7 +714,7 @@ function createCodexAgent(): Agent {
async getSessionInfo(session: Session): Promise<AgentSessionInfo | null> {
if (!session.workspacePath) return null;
const sessionFile = await findCodexSessionFile(session.workspacePath);
const sessionFile = await findCodexSessionFileCached(session.workspacePath);
if (!sessionFile) return null;
// Stream the file line-by-line to avoid loading potentially huge
@ -727,7 +746,7 @@ function createCodexAgent(): Agent {
if (!session.workspacePath) return null;
// Find the Codex session file for this workspace
const sessionFile = await findCodexSessionFile(session.workspacePath);
const sessionFile = await findCodexSessionFileCached(session.workspacePath);
if (!sessionFile) return null;
// Stream the file line-by-line to avoid loading potentially huge
@ -782,6 +801,11 @@ export function create(): Agent {
return createCodexAgent();
}
/** @internal Clear the session file cache. Exported for testing only. */
export function _resetSessionFileCache(): void {
sessionFileCache.clear();
}
export { CodexAppServerClient } from "./app-server-client.js";
export type {
AppServerClientOptions,