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
26 changes: 20 additions & 6 deletions cli/compress/layer.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,26 @@ import (
// prefix). Its output must never be compressed — see CompressHinted.
const recallToolName = "recall"

// isRecallTool reports whether toolName refers to the @recall builtin. Tool
// names reach the layer either bare ("recall") or '@'-prefixed ("@recall")
// since GetPlugin accepts both forms, so we normalize before comparing.
func isRecallTool(toolName string) bool {
// verbatimToolNames are the tools whose output is the evidence itself: saved
// conversations and stored memories. The compressors detect by content, and a
// conversation that talks about errors, warnings or file paths reads like a
// log or a grep result, so the middle of a session the model opened to answer
// from would be dropped behind "[N lines omitted]". These tools page and cap
// their own output, and the per-result budget still bounds the rest.
var verbatimToolNames = []string{recallToolName, "session", "memory"}

// isVerbatimTool reports whether toolName is a tool whose output must reach
// the model exactly as the tool produced it. Tool names reach the layer either
// bare ("recall") or '@'-prefixed ("@recall") since GetPlugin accepts both
// forms, so the name is normalized before comparing.
func isVerbatimTool(toolName string) bool {
name := strings.TrimPrefix(strings.TrimSpace(toolName), "@")
return strings.EqualFold(name, recallToolName)
for _, v := range verbatimToolNames {
if strings.EqualFold(name, v) {
return true
}
}
return false
}

// Layer is the high-level facade the rest of ChatCLI talks to. It bundles a
Expand Down Expand Up @@ -263,7 +277,7 @@ func (l *Layer) CompressHinted(h Hint, content string) (string, Result) {
// so without this guard recall's output would be re-compressed: the model
// would receive a truncated view with a fresh <<ccr:KEY>> marker instead of
// the full original it explicitly asked for — defeating recall entirely.
if isRecallTool(h.ToolName) {
if isVerbatimTool(h.ToolName) {
return content, passthrough(content)
}
// Idempotent: content that already carries a CCR marker was compressed
Expand Down
25 changes: 25 additions & 0 deletions cli/compress/layer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,31 @@ func TestLayerNeverCompressesRecallOutput(t *testing.T) {
}
}

func TestLayerNeverCompressesMemoryToolOutput(t *testing.T) {
// A saved conversation that mentions errors and file paths reads like a
// log to the content detectors; @session and @memory return the evidence
// a question is answered from, so their output must reach the model whole.
l := NewLayer(Config{Mode: ModeLossyWithCCR, Store: NewMemoryStore(), Threshold: 100})
var sb strings.Builder
for ln := 1; ln <= 300; ln++ {
level := "INFO"
if ln%50 == 0 {
level = "ERROR"
}
fmt.Fprintf(&sb, "[%d] user: %s step %d of the build, I used Trello in my previous role\n", ln, level, ln)
}
original := sb.String()
if out, _ := l.CompressToolOutput("build", original); len(out) >= len(original) {
t.Fatal("test payload should compress as a log; otherwise the guard is untested")
}
for _, name := range []string{"@session", "session", "@memory", "Memory"} {
out, res := l.CompressToolOutput(name, original)
if out != original || res.Strategy != "passthrough" {
t.Fatalf("%s output must pass through verbatim, got %d bytes strategy=%q", name, len(out), res.Strategy)
}
}
}

func TestNewLayerFromEnvOff(t *testing.T) {
t.Setenv("CHATCLI_COMPRESSION", "off")
l := NewLayerFromEnv(t.TempDir())
Expand Down
Loading