refactor: unify cache invalidation into invalidateAllCaches() (#545)
Three independent caches (state, path, parse) required manual coordination on every dispatch cycle. Forgetting any one caused stale reads (#431). Add a single invalidateAllCaches() in cache.ts that clears all three, and replace grouped call sites in auto.ts and tests. Individual clear functions are preserved for callers that legitimately only need to clear one cache. Closes #527 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4afcc81382
commit
4184be251f
4 changed files with 45 additions and 27 deletions
|
|
@ -18,7 +18,7 @@ import type {
|
|||
|
||||
import { deriveState, invalidateStateCache } from "./state.js";
|
||||
import type { BudgetEnforcementMode, GSDState } from "./types.js";
|
||||
import { loadFile, parseRoadmap, getManifestStatus, clearParseCache } from "./files.js";
|
||||
import { loadFile, parseRoadmap, getManifestStatus } from "./files.js";
|
||||
export { inlinePriorMilestoneSummary } from "./files.js";
|
||||
import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
|
||||
import {
|
||||
|
|
@ -27,8 +27,8 @@ import {
|
|||
relMilestoneFile, relSliceFile, relSlicePath, relMilestonePath,
|
||||
milestonesDir,
|
||||
buildMilestoneFileName, buildSliceFileName, buildTaskFileName,
|
||||
clearPathCache,
|
||||
} from "./paths.js";
|
||||
import { invalidateAllCaches } from "./cache.js";
|
||||
import { saveActivityLog } from "./activity-log.js";
|
||||
import { synthesizeCrashRecovery, getDeepDiagnostic } from "./session-forensics.js";
|
||||
import { writeLock, clearLock, readCrashLock, formatCrashInfo, isLockProcessAlive } from "./crash-recovery.js";
|
||||
|
|
@ -492,9 +492,7 @@ export async function startAuto(
|
|||
} catch { /* non-fatal */ }
|
||||
// Self-heal: clear stale runtime records where artifacts already exist
|
||||
await selfHealRuntimeRecords(base, ctx, completedKeySet);
|
||||
invalidateStateCache();
|
||||
clearParseCache();
|
||||
clearPathCache();
|
||||
invalidateAllCaches();
|
||||
await dispatchNextUnit(ctx, pi);
|
||||
return;
|
||||
}
|
||||
|
|
@ -763,11 +761,9 @@ export async function handleAgentEnd(
|
|||
// Unit completed — clear its timeout
|
||||
clearUnitTimeout();
|
||||
|
||||
// Invalidate deriveState() cache — the unit just completed and may have
|
||||
// Invalidate all caches — the unit just completed and may have
|
||||
// written planning files (task summaries, roadmap checkboxes, etc.)
|
||||
invalidateStateCache();
|
||||
clearParseCache();
|
||||
clearPathCache();
|
||||
invalidateAllCaches();
|
||||
|
||||
// Small delay to let files settle (git commits, file writes)
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
|
|
@ -1120,11 +1116,10 @@ async function dispatchNextUnit(
|
|||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
|
||||
// Clear stale directory listing cache so deriveState sees fresh disk state (#431)
|
||||
clearPathCache();
|
||||
// Clear parsed roadmap/plan cache — doctor may have re-populated it with
|
||||
// Clear all caches so deriveState sees fresh disk state (#431).
|
||||
// Parse cache is also cleared — doctor may have re-populated it with
|
||||
// stale data between handleAgentEnd and this dispatch call (Path B fix).
|
||||
clearParseCache();
|
||||
invalidateAllCaches();
|
||||
|
||||
let state = await deriveState(basePath);
|
||||
let mid = state.activeMilestone?.id;
|
||||
|
|
@ -1171,9 +1166,7 @@ async function dispatchNextUnit(
|
|||
|
||||
// ── Mid-merge safety check: detect leftover merge state from a prior session ──
|
||||
if (reconcileMergeState(basePath, ctx)) {
|
||||
invalidateStateCache();
|
||||
clearParseCache();
|
||||
clearPathCache();
|
||||
invalidateAllCaches();
|
||||
state = await deriveState(basePath);
|
||||
mid = state.activeMilestone?.id;
|
||||
midTitle = state.activeMilestone?.title;
|
||||
|
|
|
|||
27
src/resources/extensions/gsd/cache.ts
Normal file
27
src/resources/extensions/gsd/cache.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// GSD Extension — Unified Cache Invalidation
|
||||
//
|
||||
// Three module-scoped caches exist across the GSD extension:
|
||||
// 1. State cache (state.ts) — memoized deriveState() result
|
||||
// 2. Path cache (paths.ts) — directory listing results (readdirSync)
|
||||
// 3. Parse cache (files.ts) — parsed markdown file results
|
||||
//
|
||||
// After any file write that changes .gsd/ contents, all three must be
|
||||
// invalidated together to prevent stale reads. This module provides a
|
||||
// single function that clears all three atomically.
|
||||
|
||||
import { invalidateStateCache } from './state.js';
|
||||
import { clearPathCache } from './paths.js';
|
||||
import { clearParseCache } from './files.js';
|
||||
|
||||
/**
|
||||
* Invalidate all GSD runtime caches in one call.
|
||||
*
|
||||
* Call this after file writes, milestone transitions, merge reconciliation,
|
||||
* or any operation that changes .gsd/ contents on disk. Forgetting to clear
|
||||
* any single cache causes stale reads (see #431).
|
||||
*/
|
||||
export function invalidateAllCaches(): void {
|
||||
invalidateStateCache();
|
||||
clearPathCache();
|
||||
clearParseCache();
|
||||
}
|
||||
|
|
@ -3,8 +3,7 @@ import { join, dirname } from "node:path";
|
|||
import { tmpdir } from "node:os";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createTestContext } from './test-helpers.ts';
|
||||
import { clearPathCache } from '../paths.ts';
|
||||
import { invalidateStateCache } from '../state.ts';
|
||||
import { invalidateAllCaches } from '../cache.ts';
|
||||
|
||||
// loadPrompt reads from ~/.gsd/agent/extensions/gsd/prompts/ (main checkout).
|
||||
// In a worktree the file may not exist there yet, so we resolve prompts
|
||||
|
|
@ -148,7 +147,8 @@ async function main(): Promise<void> {
|
|||
// ─── deriveState integration: completing-milestone dispatches correctly ─
|
||||
console.log("\n=== deriveState completing-milestone integration ===");
|
||||
{
|
||||
const { deriveState, isMilestoneComplete, invalidateStateCache } = await import("../state.ts");
|
||||
const { deriveState, isMilestoneComplete } = await import("../state.ts");
|
||||
const { invalidateAllCaches: invalidateAllCachesDynamic } = await import("../cache.ts");
|
||||
const { parseRoadmap } = await import("../files.ts");
|
||||
|
||||
const base = createFixtureBase();
|
||||
|
|
@ -181,8 +181,7 @@ async function main(): Promise<void> {
|
|||
|
||||
// Now add the summary and verify it transitions to complete
|
||||
writeMilestoneSummary(base, "M001", "# M001 Summary\n\nDone.");
|
||||
clearPathCache();
|
||||
invalidateStateCache();
|
||||
invalidateAllCachesDynamic();
|
||||
const stateAfter = await deriveState(base);
|
||||
assertEq(stateAfter.phase, "complete", "deriveState returns complete after summary exists");
|
||||
assertEq(stateAfter.registry[0]?.status, "complete", "registry shows complete status");
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync } from "node:
|
|||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
import { deriveState, invalidateStateCache } from "../state.js";
|
||||
import { resolveMilestoneFile, clearPathCache } from "../paths.js";
|
||||
import { deriveState } from "../state.js";
|
||||
import { resolveMilestoneFile } from "../paths.js";
|
||||
import { invalidateAllCaches } from "../cache.js";
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
|
@ -40,8 +41,7 @@ assert(
|
|||
const contextPath = join(gsd, "milestones", "M001", "M001-CONTEXT.md");
|
||||
writeFileSync(contextPath, "# M001: Full Context\n\nDeep discussion output.\n");
|
||||
|
||||
clearPathCache();
|
||||
invalidateStateCache();
|
||||
invalidateAllCaches();
|
||||
const state2 = await deriveState(tmpBase);
|
||||
assert(
|
||||
state2.phase === "pre-planning",
|
||||
|
|
@ -67,8 +67,7 @@ assert(
|
|||
);
|
||||
|
||||
// Step 4: After cleanup, state is still pre-planning (CONTEXT.md exists)
|
||||
clearPathCache();
|
||||
invalidateStateCache();
|
||||
invalidateAllCaches();
|
||||
const state3 = await deriveState(tmpBase);
|
||||
assert(
|
||||
state3.phase === "pre-planning",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue