src / cache.ts
import { promises as fs } from "fs";
import * as path from "path";
/**
* Stores one summary per chunk of conversation, keyed by a hash of the chunk's content. Survives
* across compactions so that only newly written material costs a model call.
*/
export interface SummaryCache {
get(key: string): Promise<string | undefined>;
set(key: string, value: string): Promise<void>;
}
export function createMemoryCache(): SummaryCache {
const entries = new Map<string, string>();
return {
async get(key) {
return entries.get(key);
},
async set(key, value) {
entries.set(key, value);
},
};
}
interface CacheFile {
[key: string]: {
summary: string;
lastUsed: string;
};
}
/**
* Chunk summaries are worth persisting: a restart would otherwise re-summarize the whole
* conversation on the next message, which is the exact cost the cache exists to avoid.
*/
export function createFileCache(vaultPath: string): SummaryCache {
const file = path.join(vaultPath, ".context-compactor-chunks.json");
let loaded: CacheFile | undefined;
const load = async (): Promise<CacheFile> => {
if (loaded === undefined) {
try {
loaded = JSON.parse(await fs.readFile(file, "utf8")) as CacheFile;
} catch {
loaded = {};
}
}
return loaded;
};
return {
async get(key) {
const entries = await load();
const entry = entries[key];
if (entry === undefined) {
return undefined;
}
entry.lastUsed = new Date().toISOString();
return entry.summary;
},
async set(key, value) {
const entries = await load();
entries[key] = { summary: value, lastUsed: new Date().toISOString() };
await fs.mkdir(vaultPath, { recursive: true });
await fs.writeFile(file, JSON.stringify(entries, null, 2), "utf8");
},
};
}
// Module-level, because a handler runs once per message: a cache built inside the handler would be
// thrown away before the next message could ever hit it.
const sharedMemoryCache = createMemoryCache();
export function createCache(vaultPath: string): SummaryCache {
return vaultPath.trim() === "" ? sharedMemoryCache : createFileCache(vaultPath);
}