Skip to content

Commit b66449d

Browse files
committed
mason: D20 key-files cache key projectPath and tokenBudget
Extend readVersionedKeyFiles session cache with projectPath and tokenBudget discriminators, use BoundedSessionMap(100), and add tests for cross-project, budget-change miss, and unchanged hit path.
1 parent 4fac105 commit b66449d

2 files changed

Lines changed: 171 additions & 3 deletions

File tree

packages/plugin/src/features/magic-context/key-files/project-key-files.test.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,4 +511,156 @@ describe("versioned key-files injection", () => {
511511
closeQuietly(db);
512512
}
513513
});
514+
515+
it("does not reuse cache across different projects at the same version", () => {
516+
setAftAvailabilityOverride(true);
517+
const db = makeDb();
518+
const sessionId = "shared-session";
519+
const sessionMeta = {
520+
sessionId,
521+
isSubagent: false,
522+
} as import("../types").SessionMeta;
523+
try {
524+
const projectA = tempDir("kf-proj-a-");
525+
const projectB = tempDir("kf-proj-b-");
526+
writeFileSync(join(projectA, "a.ts"), "a");
527+
writeFileSync(join(projectB, "b.ts"), "b");
528+
replaceProjectKeyFiles(db, projectA, [
529+
{
530+
path: "a.ts",
531+
content: "project-a-block",
532+
localTokenEstimate: 10,
533+
generationConfigHash: "cfg",
534+
},
535+
]);
536+
replaceProjectKeyFiles(db, projectB, [
537+
{
538+
path: "b.ts",
539+
content: "project-b-block",
540+
localTokenEstimate: 10,
541+
generationConfigHash: "cfg",
542+
},
543+
]);
544+
expect(getKeyFilesVersion(db, projectA)).toBe(1);
545+
expect(getKeyFilesVersion(db, projectB)).toBe(1);
546+
547+
const fromA = readVersionedKeyFiles({
548+
db,
549+
sessionId,
550+
sessionMeta,
551+
directory: projectA,
552+
isCacheBusting: false,
553+
config: { enabled: true, tokenBudget: 2000 },
554+
});
555+
const fromB = readVersionedKeyFiles({
556+
db,
557+
sessionId,
558+
sessionMeta,
559+
directory: projectB,
560+
isCacheBusting: false,
561+
config: { enabled: true, tokenBudget: 2000 },
562+
});
563+
expect(fromA).toContain("project-a-block");
564+
expect(fromB).toContain("project-b-block");
565+
expect(fromB).not.toContain("project-a-block");
566+
} finally {
567+
clearKeyFilesCacheForSession(sessionId);
568+
closeQuietly(db);
569+
}
570+
});
571+
572+
it("invalidates cache when tokenBudget changes without a version bump", () => {
573+
setAftAvailabilityOverride(true);
574+
const db = makeDb();
575+
const sessionId = "budget-session";
576+
const sessionMeta = {
577+
sessionId,
578+
isSubagent: false,
579+
} as import("../types").SessionMeta;
580+
try {
581+
const project = tempDir("kf-budget-");
582+
writeFileSync(join(project, "big.ts"), "x");
583+
writeFileSync(join(project, "small.ts"), "y");
584+
replaceProjectKeyFiles(db, project, [
585+
{
586+
path: "big.ts",
587+
content: "BIG-FILE-CONTENT",
588+
localTokenEstimate: 100,
589+
generationConfigHash: "cfg",
590+
},
591+
{
592+
path: "small.ts",
593+
content: "SMALL",
594+
localTokenEstimate: 5,
595+
generationConfigHash: "cfg",
596+
},
597+
]);
598+
599+
const tightBudget = readVersionedKeyFiles({
600+
db,
601+
sessionId,
602+
sessionMeta,
603+
directory: project,
604+
isCacheBusting: false,
605+
config: { enabled: true, tokenBudget: 10 },
606+
});
607+
const looseBudget = readVersionedKeyFiles({
608+
db,
609+
sessionId,
610+
sessionMeta,
611+
directory: project,
612+
isCacheBusting: false,
613+
config: { enabled: true, tokenBudget: 2000 },
614+
});
615+
expect(tightBudget).not.toContain("BIG-FILE-CONTENT");
616+
expect(looseBudget).toContain("BIG-FILE-CONTENT");
617+
expect(looseBudget).not.toBe(tightBudget);
618+
} finally {
619+
clearKeyFilesCacheForSession(sessionId);
620+
closeQuietly(db);
621+
}
622+
});
623+
624+
it("cache hit when session, project, budget, and version are unchanged", () => {
625+
setAftAvailabilityOverride(true);
626+
const db = makeDb();
627+
const sessionId = "hit-session";
628+
const sessionMeta = {
629+
sessionId,
630+
isSubagent: false,
631+
} as import("../types").SessionMeta;
632+
const config = { enabled: true, tokenBudget: 2000 };
633+
try {
634+
const project = tempDir("kf-hit-");
635+
writeFileSync(join(project, "a.ts"), "a");
636+
replaceProjectKeyFiles(db, project, [
637+
{
638+
path: "a.ts",
639+
content: "stable-hit",
640+
localTokenEstimate: 10,
641+
generationConfigHash: "cfg",
642+
},
643+
]);
644+
const first = readVersionedKeyFiles({
645+
db,
646+
sessionId,
647+
sessionMeta,
648+
directory: project,
649+
isCacheBusting: false,
650+
config,
651+
});
652+
const second = readVersionedKeyFiles({
653+
db,
654+
sessionId,
655+
sessionMeta,
656+
directory: project,
657+
isCacheBusting: false,
658+
config,
659+
});
660+
expect(second).toBe(first);
661+
} finally {
662+
clearKeyFilesCacheForSession(sessionId);
663+
closeQuietly(db);
664+
}
665+
});
514666
});

packages/plugin/src/hooks/magic-context/key-files-block.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
sha256,
1111
} from "../../features/magic-context/key-files/project-key-files";
1212
import type { SessionMeta } from "../../features/magic-context/types";
13+
import { BoundedSessionMap } from "../../shared/bounded-session-map";
1314
import { log, sessionLog } from "../../shared/logger";
1415
import type { Database } from "../../shared/sqlite";
1516

@@ -21,6 +22,8 @@ export interface KeyFilesConfigForRender {
2122
interface CacheEntry {
2223
value: string | null;
2324
version: number;
25+
projectPath: string;
26+
tokenBudget: number;
2427
}
2528

2629
interface StaleUpdate {
@@ -30,7 +33,9 @@ interface StaleUpdate {
3033
staleReason: KeyFileStaleReason;
3134
}
3235

33-
export const cachedKeyFilesBySession = new Map<string, CacheEntry>();
36+
const KEY_FILES_CACHE_MAX = 100;
37+
38+
export const cachedKeyFilesBySession = new BoundedSessionMap<CacheEntry>(KEY_FILES_CACHE_MAX);
3439

3540
const staleUpdates = new Map<string, StaleUpdate>();
3641

@@ -188,14 +193,25 @@ export function readVersionedKeyFiles(args: {
188193
const currentVersion = getKeyFilesVersion(args.db, projectPath);
189194
if (args.sessionId) {
190195
const cached = cachedKeyFilesBySession.get(args.sessionId);
191-
if (cached && !args.isCacheBusting && cached.version === currentVersion) {
196+
if (
197+
cached &&
198+
!args.isCacheBusting &&
199+
cached.version === currentVersion &&
200+
cached.projectPath === projectPath &&
201+
cached.tokenBudget === config.tokenBudget
202+
) {
192203
return cached.value;
193204
}
194205
}
195206

196207
const value = buildKeyFilesBlock(args.db, projectPath, config);
197208
if (args.sessionId) {
198-
cachedKeyFilesBySession.set(args.sessionId, { value, version: currentVersion });
209+
cachedKeyFilesBySession.set(args.sessionId, {
210+
value,
211+
version: currentVersion,
212+
projectPath,
213+
tokenBudget: config.tokenBudget,
214+
});
199215
if (value)
200216
sessionLog(
201217
args.sessionId,

0 commit comments

Comments
 (0)