From 9ce1e63788a8580a8f3fbcd35a3ead7464ba4c61 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Tue, 28 Apr 2026 17:41:43 +0200 Subject: [PATCH] core: allow /model switch while running; detach turnError hook context - /model command no longer rejects switches while agent is running; needed for turnError hook recovery (freeloader switches model mid-error) - TurnError hook uses context.Background()-derived context so hook can safely issue ctl commands without cancelling its own execution - update TestCommand_Model_WhileRunning to expect success Co-Authored-By: Claude Sonnet 4.6 --- pkg/agent/commands.go | 4 ---- pkg/agent/core.go | 8 ++++++-- pkg/agent/core_test.go | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/agent/commands.go b/pkg/agent/commands.go index c785108..a7b2f82 100644 --- a/pkg/agent/commands.go +++ b/pkg/agent/commands.go @@ -126,10 +126,6 @@ func (s *agent) handleCommand(ctx context.Context, input string, handler EventHa handler(infoEvent(s.cfg.Backend.Model())) return } - if s.IsRunning() { - handler(infoEvent("error: cannot switch model while agent is running")) - return - } s.cfg.Backend.SetModel(args[0]) handler(infoEvent("switched model to: " + args[0])) }, diff --git a/pkg/agent/core.go b/pkg/agent/core.go index 16b0ce0..e375a52 100644 --- a/pkg/agent/core.go +++ b/pkg/agent/core.go @@ -310,8 +310,12 @@ func NewAgentCore(cfg AgentCoreConfig) Core { a.changeCond = sync.NewCond(&a.changeMu) a.pushSessionEnv() a.pushLockDir() - a.cfg.TurnError = func(ctx context.Context, errType, errMsg string) HookResult { - return a.hooks.Run(ctx, HookTurnError, map[string]string{ + a.cfg.TurnError = func(_ context.Context, errType, errMsg string) HookResult { + // Use a detached context so the hook can safely write "stop" to ctl + // without cancelling its own execution via the action context. + hookCtx, cancel := context.WithTimeout(context.Background(), time.Duration(hookTimeout)*time.Second) + defer cancel() + return a.hooks.Run(hookCtx, HookTurnError, map[string]string{ "session_id": a.sessionID, "cwd": a.CWD(), "model": a.cfg.Backend.Model(), diff --git a/pkg/agent/core_test.go b/pkg/agent/core_test.go index a62d55c..3aa212d 100644 --- a/pkg/agent/core_test.go +++ b/pkg/agent/core_test.go @@ -1071,19 +1071,19 @@ func TestCommand_Model_WhileRunning(t *testing.T) { }() waitState(t, c, "thinking") + // /model is now allowed while running (needed for turnError hook recovery). evs := collectEvents(context.Background(), c, "/model new-model") close(unblock) <-done - found := false for _, s := range byRole(evs, "info") { if strings.Contains(s, "error") { - found = true + t.Errorf("/model while running: unexpected error: %s", s) } } - if !found { - t.Errorf("/model while running: expected error event, got: %v", byRole(evs, "info")) + if got := be.Model(); got != "new-model" { + t.Errorf("model = %q; want new-model", got) } }