From 602d4ec6cc823dea09e5a103ed7e73910e63eaae Mon Sep 17 00:00:00 2001 From: yetone Date: Mon, 5 Oct 2026 16:24:23 +0800 Subject: [PATCH] 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..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 """). --- internal/agent/claude.go | 38 ++++++++++++++++- internal/agent/claude_compact_test.go | 59 +++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/internal/agent/claude.go b/internal/agent/claude.go index 671435c1..979f90b5 100644 --- a/internal/agent/claude.go +++ b/internal/agent/claude.go @@ -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 diff --git a/internal/agent/claude_compact_test.go b/internal/agent/claude_compact_test.go index 1c2a7ee7..35b539d7 100644 --- a/internal/agent/claude_compact_test.go +++ b/internal/agent/claude_compact_test.go @@ -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..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") +}