mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-21 16:41:56 +00:00
fix: refresh session-store cache when file size changes within same mtime tick
The session-store cache used only mtime for invalidation. In fast CI runs (especially under bun), test writes to the session store can complete within the same filesystem mtime granularity (~1s on HFS+/ext4), so the cache returns stale data. This caused non-deterministic failures in model precedence tests where a session override written to disk was not observed by the next loadSessionStore() call. Fix: add file size as a secondary cache invalidation signal. The cache now checks both mtimeMs and sizeBytes — if either differs from the cached values, it reloads from disk. Changes: - cache-utils.ts: add getFileSizeBytes() helper - sessions/store.ts: extend SessionStoreCacheEntry with sizeBytes field, check size in cache-hit path, populate size on cache writes - sessions.cache.test.ts: add regression test for same-mtime rewrite
This commit is contained in:
committed by
Peter Steinberger
parent
f9025c3f55
commit
1212328c8d
@@ -25,3 +25,11 @@ export function getFileMtimeMs(filePath: string): number | undefined {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function getFileSizeBytes(filePath: string): number | undefined {
|
||||
try {
|
||||
return fs.statSync(filePath).size;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,4 +198,38 @@ describe("Session Store Cache", () => {
|
||||
const loaded = loadSessionStore(storePath);
|
||||
expect(loaded).toEqual({});
|
||||
});
|
||||
|
||||
it("should refresh cache when file is rewritten within the same mtime tick", async () => {
|
||||
// This reproduces the CI flake where fast test writes complete within the
|
||||
// same mtime granularity (typically 1s on HFS+/ext4), so mtime-only
|
||||
// invalidation returns stale cached data.
|
||||
const store1: Record<string, SessionEntry> = {
|
||||
"session:1": createSessionEntry({ sessionId: "id-1", displayName: "Original" }),
|
||||
};
|
||||
|
||||
await saveSessionStore(storePath, store1);
|
||||
|
||||
// Warm the cache
|
||||
const loaded1 = loadSessionStore(storePath);
|
||||
expect(loaded1["session:1"].displayName).toBe("Original");
|
||||
|
||||
// Rewrite the file directly (bypassing saveSessionStore's write-through
|
||||
// cache) with different content but preserve the same mtime so only size
|
||||
// changes.
|
||||
const store2: Record<string, SessionEntry> = {
|
||||
"session:1": createSessionEntry({ sessionId: "id-1", displayName: "Original" }),
|
||||
"session:2": createSessionEntry({ sessionId: "id-2", displayName: "Added" }),
|
||||
};
|
||||
const json2 = JSON.stringify(store2, null, 2);
|
||||
fs.writeFileSync(storePath, json2);
|
||||
|
||||
// Force mtime to match the cached value so only size differs
|
||||
const stat = fs.statSync(storePath);
|
||||
fs.utimesSync(storePath, stat.atime, stat.mtime);
|
||||
|
||||
// The cache should detect the size change and reload from disk
|
||||
const loaded2 = loadSessionStore(storePath);
|
||||
expect(loaded2["session:2"]).toBeDefined();
|
||||
expect(loaded2["session:2"].displayName).toBe("Added");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,7 +17,12 @@ import {
|
||||
normalizeSessionDeliveryFields,
|
||||
type DeliveryContext,
|
||||
} from "../../utils/delivery-context.js";
|
||||
import { getFileMtimeMs, isCacheEnabled, resolveCacheTtlMs } from "../cache-utils.js";
|
||||
import {
|
||||
getFileMtimeMs,
|
||||
getFileSizeBytes,
|
||||
isCacheEnabled,
|
||||
resolveCacheTtlMs,
|
||||
} from "../cache-utils.js";
|
||||
import { loadConfig } from "../config.js";
|
||||
import type { SessionMaintenanceConfig, SessionMaintenanceMode } from "../types.base.js";
|
||||
import { enforceSessionDiskBudget, type SessionDiskBudgetSweepResult } from "./disk-budget.js";
|
||||
@@ -39,6 +44,7 @@ type SessionStoreCacheEntry = {
|
||||
loadedAt: number;
|
||||
storePath: string;
|
||||
mtimeMs?: number;
|
||||
sizeBytes?: number;
|
||||
serialized?: string;
|
||||
};
|
||||
|
||||
@@ -208,7 +214,8 @@ export function loadSessionStore(
|
||||
const cached = SESSION_STORE_CACHE.get(storePath);
|
||||
if (cached && isSessionStoreCacheValid(cached)) {
|
||||
const currentMtimeMs = getFileMtimeMs(storePath);
|
||||
if (currentMtimeMs === cached.mtimeMs) {
|
||||
const currentSizeBytes = getFileSizeBytes(storePath);
|
||||
if (currentMtimeMs === cached.mtimeMs && currentSizeBytes === cached.sizeBytes) {
|
||||
// Return a deep copy to prevent external mutations affecting cache
|
||||
return structuredClone(cached.store);
|
||||
}
|
||||
@@ -288,6 +295,7 @@ export function loadSessionStore(
|
||||
loadedAt: Date.now(),
|
||||
storePath,
|
||||
mtimeMs,
|
||||
sizeBytes: getFileSizeBytes(storePath),
|
||||
serialized: serializedFromDisk,
|
||||
});
|
||||
}
|
||||
@@ -667,6 +675,7 @@ function updateSessionStoreWriteCaches(params: {
|
||||
loadedAt: Date.now(),
|
||||
storePath: params.storePath,
|
||||
mtimeMs,
|
||||
sizeBytes: getFileSizeBytes(params.storePath),
|
||||
serialized: params.serialized,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user