agent: magpie's default compaction window gives way to one the user set in Claude Code (#876, hisiling)

On a non-Claude model magpie wrote CLAUDE_CODE_AUTO_COMPACT_WINDOW=272000 into ~/.claude/settings.json's env even when the user had set autoCompactWindow there, or modelSettings.<model>.autoCompactWindow (where /autocompact saves it). Claude Code takes the env first, so the user's own window was overridden: in a sandbox, autoCompactWindow 900000 showed 900k in /context, and 272k once the env was added. magpie now leaves the env out when the user set either one for the main model. A size the user typed in magpie, for every model or for a model or its provider, is still written, because the user chose it there.

Codex is unaffected. Codex's model_info.rs applies model_context_window and model_auto_compact_token_limit from config.toml over the catalog, and magpie never writes or removes either key.

TestClaudeCompactYieldsToOwnSetting fails without the change ("autoCompactWindow: "272000"; want """).
This commit is contained in:
yetone committed 2026-10-05 16:24:23 +08:00
1 parent 9139b19224
commit 602d4ec6cc
2 files changed
+95 -2

No files matched your search

+36 -2
View File
@@ -2,6 +2,7 @@ package agent
import (
"cmp"
"encoding/json"
"fmt"
"os"
"path/filepath"
@@ -427,8 +428,14 @@ func claudeIn(at place) *Agent {
// model (settings.Compact), none under Full window
main := mainModel()
n := provider.CompactSet(main)
if n == 0 && !claudeModel(main) {
n = settings.Load().Compact()
if s := settings.Load(); n == 0 && !claudeModel(main) {
n = s.Compact()
// magpie's default gives way to an autoCompactWindow the user
// set in Claude Code (/autocompact, its settings), which the env
// would take precedence over; one typed in magpie doesn't (#876)
if s.CompactAt == 0 && claudeOwnCompact(path, main) {
n = 0
}
}
if n == 0 {
return dropCompact()
@@ -1290,6 +1297,33 @@ func claudeModel(ref string) bool {
return strings.HasPrefix(strings.ToLower(m), "claude")
}
// claudeOwnCompact says the settings.json at path has an auto-compact
// window of the user's for model: autoCompactWindow, or one under
// modelSettings for it (as /autocompact saves it), a number or "auto".
// Claude Code takes CLAUDE_CODE_AUTO_COMPACT_WINDOW before either.
func claudeOwnCompact(path, model string) bool {
if _, ok := edit.GetJSON(path, "autoCompactWindow"); ok {
return true
}
raw, ok := edit.GetJSON(path, "modelSettings")
if !ok {
return false
}
var per map[string]struct {
Window any `json:"autoCompactWindow"`
}
if json.Unmarshal([]byte(raw), &per) != nil {
return false
}
model = strings.ToLower(strings.TrimSuffix(strings.TrimPrefix(model, "magpie/"), "[1m]"))
for k, v := range per {
if v.Window != nil && strings.ToLower(strings.TrimSuffix(k, "[1m]")) == model {
return true
}
}
return false
}
// claudeWindow is the context window to tell Claude Code for the models it
// runs on, 0 when magpie doesn't know it. One value serves every model not
// marked [1m]: the main model's, or when that one is marked, the smallest of
+59
View File
@@ -138,3 +138,62 @@ func TestClaudeCompactAt(t *testing.T) {
must(a.Field("model").Set("v/big"))
expect("full window", "")
}
// An auto-compact window the user set in Claude Code (autoCompactWindow,
// or modelSettings.<model>.autoCompactWindow as /autocompact saves it) is
// theirs: magpie's default doesn't write the env that would take
// precedence over it; a size typed in magpie, or on the model or its
// provider, still does (#876, hisiling).
func TestClaudeCompactYieldsToOwnSetting(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
if err := provider.Save(provider.Provider{ID: "v", Name: "V", Chat: "https://example.test/v1", Key: "k",
Models: []string{"big", "flash"}}); err != nil {
t.Fatal(err)
}
if err := catalog.SaveLive("v", "https://example.test/v1", []catalog.Model{{ID: "big", Context: 1000000}, {ID: "flash", Context: 1000000}}); err != nil {
t.Fatal(err)
}
path := filepath.Join(home, ".claude", "settings.json")
writeFile(t, path, `{"autoCompactWindow": 900000}`)
a := claude(home)
compact := func() string { v, _ := edit.GetJSON(path, "env."+claudeCompactEnv); return v }
must := func(err error) {
t.Helper()
if err != nil {
t.Fatal(err)
}
}
expect := func(what, want string) {
t.Helper()
if got := compact(); got != want {
t.Fatalf("%s: %q; want %q", what, got, want)
}
}
must(a.Field("model").Set("v/big[1m]"))
expect("autoCompactWindow", "")
if v, _ := edit.GetJSON(path, "autoCompactWindow"); v != "900000" {
t.Fatalf("autoCompactWindow changed: %q", v)
}
must(edit.DelJSON(path, "autoCompactWindow"))
must(a.Sync())
expect("none set", "272000")
// one for this model in modelSettings, not another's
must(edit.SetJSON(path, edit.KV{Path: "modelSettings", Value: map[string]any{"v/flash": map[string]any{"autoCompactWindow": 800000}}}))
must(a.Sync())
expect("another model's", "272000")
must(a.Field("model").Set("v/flash[1m]"))
expect("this model's", "")
// a size typed in magpie, or the model's own there, comes first
must(provider.SetModelCompacts("v", map[string]int{"flash": 200000}))
must(a.Sync())
expect("the model's own in magpie", "200000")
must(provider.SetModelCompacts("v", nil))
must(provider.SetCompactAt(400000))
must(a.Sync())
expect("typed in magpie", "400000")
}