diff --git a/docs/sequence-flows/schedules.md b/docs/sequence-flows/schedules.md index af8eca70..d675a0d1 100644 --- a/docs/sequence-flows/schedules.md +++ b/docs/sequence-flows/schedules.md @@ -56,13 +56,15 @@ the chat workers and SSE broadcast. │── RecordRun(running) ────────────▶│ │ │ │── SetLastRun ────────────────────▶│ │ │ │── CreateSessionFileWithSettings ───────────────────▶│ │ - │ (project dir or home; model/thinking as implicit entries) │ + │ (project dir or home; implicit model/thinking │ │ + │ entries are not restored on an empty session) │ │ │◀── filename ──────────────────────────────────────│ │ │── ResolveByID ────────────────────────────────────▶│ │ │◀── session UUID + path ───────────│ │ │ │── AttachSession(runID, uuid) ────▶│ │ │ │ │ │ │ │ │── EnsureWorker(uuid, path) ───────────────────────▶│ │ + │── SetModel / SetThinkingLevel (when configured) ──▶│ │ │── Send(uuid, path, {instructions}) ───────────────▶│─── pi runs ──▶│ │ │ │ │ │ │ (file watcher sees the new .jsonl → broadcasts `new-session`) │ diff --git a/internal/server/scheduler.go b/internal/server/scheduler.go index 43fb22ca..84036bfe 100644 --- a/internal/server/scheduler.go +++ b/internal/server/scheduler.go @@ -172,6 +172,19 @@ func (s *Server) fireScheduleContext(ctx context.Context, sc schedules.Schedule) _ = s.schedules.FailRun(runID, err.Error()) return sessionID, fmt.Errorf("ensure worker: %w", err) } + // Empty sessions skip history restore, so implicit file entries are ignored. + if sc.ModelProvider != "" && sc.ModelID != "" { + if err := s.chatSender.SetModel(ctx, sessionID, resolved.Path, sc.ModelProvider, sc.ModelID); err != nil { + _ = s.schedules.FailRun(runID, err.Error()) + return sessionID, fmt.Errorf("set model: %w", err) + } + } + if sc.ThinkingLevel != "" { + if err := s.chatSender.SetThinkingLevel(ctx, sessionID, resolved.Path, sc.ThinkingLevel); err != nil { + _ = s.schedules.FailRun(runID, err.Error()) + return sessionID, fmt.Errorf("set thinking level: %w", err) + } + } if err := s.chatSender.Send(ctx, sessionID, resolved.Path, chat.Request{Message: sc.Instructions}); err != nil { _ = s.schedules.FailRun(runID, err.Error()) return sessionID, fmt.Errorf("send: %w", err) diff --git a/internal/server/scheduler_test.go b/internal/server/scheduler_test.go index d0567185..1b8306fb 100644 --- a/internal/server/scheduler_test.go +++ b/internal/server/scheduler_test.go @@ -89,6 +89,79 @@ func TestFireScheduleCreatesSessionAndSends(t *testing.T) { } } +func TestFireScheduleAppliesConfiguredModel(t *testing.T) { + s, sender := newScheduleTestServer(t) + + sc, err := s.schedules.Create(schedules.Schedule{ + ID: "sched-model", + Name: "Model run", + Instructions: "go", + ModelProvider: "opencode-go", + ModelID: "deepseek-v4.1-flash", + ThinkingLevel: "high", + ProjectPath: t.TempDir(), + Enabled: true, + }) + if err != nil { + t.Fatalf("create schedule: %v", err) + } + + sessionID, err := s.fireSchedule(sc) + if err != nil { + t.Fatalf("fireSchedule: %v", err) + } + + sender.mu.Lock() + provider, modelID := sender.setModelProvider, sender.setModelID + modelSession := sender.setModelSessionID + thinking, thinkingSession := sender.setThinkingLevel, sender.setThinkingSessionID + sender.mu.Unlock() + + if provider != "opencode-go" || modelID != "deepseek-v4.1-flash" { + t.Errorf("SetModel = %s/%s, want opencode-go/deepseek-v4.1-flash", provider, modelID) + } + if modelSession != sessionID { + t.Errorf("SetModel session = %q, want %q", modelSession, sessionID) + } + if thinking != "high" { + t.Errorf("SetThinkingLevel = %q, want high", thinking) + } + if thinkingSession != sessionID { + t.Errorf("SetThinkingLevel session = %q, want %q", thinkingSession, sessionID) + } +} + +func TestFireScheduleKeepsDefaultsWhenUnset(t *testing.T) { + s, sender := newScheduleTestServer(t) + + sc, err := s.schedules.Create(schedules.Schedule{ + ID: "sched-default", + Name: "Default run", + Instructions: "go", + ProjectPath: t.TempDir(), + Enabled: true, + }) + if err != nil { + t.Fatalf("create schedule: %v", err) + } + + if _, err := s.fireSchedule(sc); err != nil { + t.Fatalf("fireSchedule: %v", err) + } + + sender.mu.Lock() + provider, modelID := sender.setModelProvider, sender.setModelID + thinking := sender.setThinkingLevel + sender.mu.Unlock() + + if provider != "" || modelID != "" { + t.Errorf("SetModel called with %s/%s, want no call", provider, modelID) + } + if thinking != "" { + t.Errorf("SetThinkingLevel called with %q, want no call", thinking) + } +} + func TestEvaluateSchedulesSkipsMissedRuns(t *testing.T) { s, sender := newScheduleTestServer(t) // A daily 09:00 schedule; "now" is 08:00. First evaluation must only arm the