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 <noreply@anthropic.com>
This commit is contained in:
Levi Neely 2026-04-28 17:41:43 +02:00
parent d350c9f412
commit 9ce1e63788
3 changed files with 10 additions and 10 deletions

View File

@ -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]))
},

View File

@ -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(),

View File

@ -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)
}
}