From 9e059c2a4a65e79d361b08f1fe1c027c254c00de Mon Sep 17 00:00:00 2001 From: Corey Ryan Dean Date: Sun, 10 May 2026 09:05:20 -0500 Subject: [PATCH] fix(cache): key responses by project directives --- internal/cache/cache.go | 18 ++++++++++ internal/engine/engine.go | 1 + internal/engine/engine_test.go | 61 ++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/internal/cache/cache.go b/internal/cache/cache.go index a10ec19..faec7b5 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -26,6 +26,10 @@ type KeyInputs struct { BinariesFingerprint string BackendIdentity string PromptTemplateVersion string + // ProjectRCFingerprint captures .intentrc directives that are + // rendered into the system prompt. Whitespace-only content hashes + // to empty so users without a real project config keep today's key. + ProjectRCFingerprint string // UserContextFingerprint captures any --context values that are // injected into the system prompt. Order is preserved because the // model sees them in order; see UserContextFingerprint. @@ -41,12 +45,26 @@ func Key(in KeyInputs) string { in.BinariesFingerprint, in.BackendIdentity, in.PromptTemplateVersion, + in.ProjectRCFingerprint, in.UserContextFingerprint, } h := sha256.Sum256([]byte(strings.Join(parts, "\x1f"))) return hex.EncodeToString(h[:]) } +// ProjectRCFingerprint returns a stable fingerprint of .intentrc +// directives that are injected into the system prompt. Blank or +// whitespace-only content yields "" so the baseline cache key shape +// stays unchanged for repos without project directives. +func ProjectRCFingerprint(contents string) string { + trimmed := strings.TrimSpace(contents) + if trimmed == "" { + return "" + } + h := sha256.Sum256([]byte(trimmed)) + return hex.EncodeToString(h[:8]) +} + // UserContextFingerprint returns a stable fingerprint of the ordered // --context values forwarded into the system prompt. Order is preserved // because the model sees the entries in order, and two orderings may diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 0dcc525..50dd5b5 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -81,6 +81,7 @@ func (e *Engine) Run(ctx context.Context, prompt string, opts Options) (*Result, BinariesFingerprint: cache.BinariesFingerprint(pack.AvailableBins), BackendIdentity: backendCacheIdentity(opts.Backend), PromptTemplateVersion: model.PromptTemplateVersion, + ProjectRCFingerprint: cache.ProjectRCFingerprint(opts.ProjectRC), UserContextFingerprint: cache.UserContextFingerprint(opts.UserContext), }) res.CacheKey = key diff --git a/internal/engine/engine_test.go b/internal/engine/engine_test.go index 9c9b296..f332eca 100644 --- a/internal/engine/engine_test.go +++ b/internal/engine/engine_test.go @@ -151,3 +151,64 @@ func TestRunCacheKeyIncludesUserContext(t *testing.T) { t.Fatalf("cache key ignored --context order: %q", forward.CacheKey) } } + +func TestRunCacheKeyIncludesProjectRC(t *testing.T) { + eng := New(nil) + ctx := context.Background() + backend := &cacheIdentityBackend{ + name: "llamafile", + cacheIdentity: "llamafile|http://127.0.0.1:8080|qwen2.5-coder-3b", + } + + base, err := eng.Run(ctx, "list files", Options{Backend: backend}) + if err != nil { + t.Fatalf("run base: %v", err) + } + + withProjectRC, err := eng.Run(ctx, "list files", Options{ + Backend: backend, + ProjectRC: "prefer rg over grep", + }) + if err != nil { + t.Fatalf("run with project rc: %v", err) + } + if base.CacheKey == withProjectRC.CacheKey { + t.Fatalf("cache key collision when .intentrc directives were added: %q", base.CacheKey) + } + + otherProjectRC, err := eng.Run(ctx, "list files", Options{ + Backend: backend, + ProjectRC: "prefer fd over find", + }) + if err != nil { + t.Fatalf("run with other project rc: %v", err) + } + if withProjectRC.CacheKey == otherProjectRC.CacheKey { + t.Fatalf("cache key collision across distinct .intentrc directives: %q", withProjectRC.CacheKey) + } +} + +func TestRunCacheKeyTreatsBlankProjectRCAsAbsent(t *testing.T) { + eng := New(nil) + ctx := context.Background() + backend := &cacheIdentityBackend{ + name: "llamafile", + cacheIdentity: "llamafile|http://127.0.0.1:8080|qwen2.5-coder-3b", + } + + base, err := eng.Run(ctx, "list files", Options{Backend: backend}) + if err != nil { + t.Fatalf("run base: %v", err) + } + + blank, err := eng.Run(ctx, "list files", Options{ + Backend: backend, + ProjectRC: " \n\t ", + }) + if err != nil { + t.Fatalf("run blank project rc: %v", err) + } + if base.CacheKey != blank.CacheKey { + t.Fatalf("blank .intentrc content should preserve the baseline cache key: base=%q blank=%q", base.CacheKey, blank.CacheKey) + } +}