Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions internal/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -41,12 +45,26 @@ func Key(in KeyInputs) string {
in.BinariesFingerprint,
in.BackendIdentity,
in.PromptTemplateVersion,
in.ProjectRCFingerprint,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve baseline key when project directives are absent

Adding in.ProjectRCFingerprint to parts unconditionally changes the serialized key payload for every request (an extra field separator is always inserted), so repositories without .intentrc no longer reuse pre-existing cache entries. That defeats the stated goal of keeping baseline behavior unchanged and causes a broad cache miss wave unrelated to directive changes.

Useful? React with 👍 / 👎.

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))
Comment on lines +60 to +64
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Hash non-blank .intentrc content without trimming semantics

ProjectRCFingerprint hashes strings.TrimSpace(contents), but the engine injects the original opts.ProjectRC into the system prompt. If .intentrc changes only in leading/trailing whitespace, the prompt text changes while the cache key does not, which can return a cached response generated under different prompt bytes. Only whitespace-only files should collapse to empty; non-blank content should be fingerprinted from the raw bytes.

Useful? React with 👍 / 👎.

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
Expand Down
1 change: 1 addition & 0 deletions internal/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
61 changes: 61 additions & 0 deletions internal/engine/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading