diff --git a/src/cache-keys.ts b/src/cache-keys.ts
index a8d22fd4..ac29270a 100644
--- a/src/cache-keys.ts
+++ b/src/cache-keys.ts
@@ -14,7 +14,10 @@ export function normalizeContentForHash(content: string): string {
return content.replaceAll("\r\n", "\n").trim();
}
-export function extractTaggedBlock(prompt: string, tag: "instructions" | "content"): string | null {
+export function extractTaggedBlock(
+ prompt: string,
+ tag: "instructions" | "content" | "context",
+): string | null {
const open = `<${tag}>`;
const close = `${tag}>`;
const start = prompt.indexOf(open);
@@ -25,8 +28,18 @@ export function extractTaggedBlock(prompt: string, tag: "instructions" | "conten
}
export function buildPromptHash(prompt: string): string {
- const instructions = extractTaggedBlock(prompt, "instructions") ?? prompt;
- return hashString(instructions.trim());
+ const instructionsContent = extractTaggedBlock(prompt, "instructions");
+ const contextContent = extractTaggedBlock(prompt, "context");
+
+ // If at least one of the tags is present (even if empty), hash their contents.
+ if (instructionsContent !== null || contextContent !== null) {
+ const instructions = instructionsContent ?? "";
+ const context = contextContent ?? "";
+ return hashString(`${instructions}\n${context}`.trim());
+ }
+
+ // Fallback for prompts without any tags.
+ return hashString(prompt.trim());
}
export function buildPromptContentHash({
diff --git a/src/cache.ts b/src/cache.ts
index 7442c86e..a4ea9b77 100644
--- a/src/cache.ts
+++ b/src/cache.ts
@@ -40,7 +40,7 @@ export type CacheConfig = {
path?: string;
};
-export const CACHE_FORMAT_VERSION = 1;
+export const CACHE_FORMAT_VERSION = 2;
export const DEFAULT_CACHE_MAX_MB = 512;
export const DEFAULT_CACHE_TTL_DAYS = 30;
diff --git a/tests/cache.keys.test.ts b/tests/cache.keys.test.ts
index 21c47adb..1303f30e 100644
--- a/tests/cache.keys.test.ts
+++ b/tests/cache.keys.test.ts
@@ -2,8 +2,10 @@ import { describe, expect, it } from "vitest";
import {
buildExtractCacheKey,
buildPromptContentHash,
+ buildPromptHash,
buildSummaryCacheKey,
extractTaggedBlock,
+ hashString,
} from "../src/cache.js";
describe("cache keys and tags", () => {
@@ -11,9 +13,53 @@ describe("cache keys and tags", () => {
const prompt = "Do the thing.\nBody";
expect(extractTaggedBlock(prompt, "instructions")).toBe("Do the thing.");
expect(extractTaggedBlock(prompt, "content")).toBe("Body");
+ expect(extractTaggedBlock(prompt, "context")).toBeNull();
+ expect(extractTaggedBlock("Site", "context")).toBe("Site");
expect(extractTaggedBlock("no tags here", "instructions")).toBeNull();
});
+ it("changes prompt hashes when context changes", () => {
+ const instructions = "Summarize it.";
+ const contextA = "URL: https://a.com";
+ const contextB = "URL: https://b.com";
+ const prompt1 = `${instructions}\n${contextA}\n`;
+ const prompt2 = `${instructions}\n${contextB}\n`;
+
+ const hash1 = buildPromptHash(prompt1);
+ const hash2 = buildPromptHash(prompt2);
+
+ expect(hash1).not.toBe(hash2);
+ });
+
+ it("hashes instructions-only prompt consistently", () => {
+ const promptWithEmptyContext = "Summarize.\n\nBody";
+ const promptWithNoContextTag = "Summarize.\nBody";
+
+ const hash1 = buildPromptHash(promptWithEmptyContext);
+ const hash2 = buildPromptHash(promptWithNoContextTag);
+
+ // Both should hash just the instructions since context is empty/missing
+ expect(hash1).toBe(hash2);
+ });
+
+ it("treats multiple empty tags consistently", () => {
+ const p1 = "";
+ const p2 = "";
+ const p3 = "";
+ const p4 = " ";
+
+ const h1 = buildPromptHash(p1);
+ const h2 = buildPromptHash(p2);
+ const h3 = buildPromptHash(p3);
+ const h4 = buildPromptHash(p4);
+
+ expect(h1).toBe(h2);
+ expect(h2).toBe(h3);
+ expect(h3).toBe(h4);
+ // They should all hash to an empty string's hash (after trim)
+ expect(h1).toBe(hashString(""));
+ });
+
it("changes summary keys when inputs change", () => {
const base = buildSummaryCacheKey({
contentHash: "content",