Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions internal/server/scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,24 @@ func (s *Server) fireScheduleContext(ctx context.Context, sc schedules.Schedule)
_ = s.schedules.FailRun(runID, err.Error())
return sessionID, fmt.Errorf("ensure worker: %w", err)
}
// pi restores a session's model and thinking level from its history only
// once the session has at least one message. A freshly created schedule
// session has none, so the implicit settings written by
// CreateSessionFileWithSettings are ignored and pi would run with its global
// default model. Apply the schedule's explicit settings to the worker here
// so the run uses the model the user configured.
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)
Expand Down
78 changes: 78 additions & 0 deletions internal/server/scheduler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,84 @@ func TestFireScheduleCreatesSessionAndSends(t *testing.T) {
}
}

// A scheduled session has no messages yet, so pi cannot restore the model from
// its history. The runner must apply the schedule's configured model and
// thinking level to the worker explicitly.
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)
}
}

// Without explicit settings the schedule must keep pi's defaults, so the runner
// must not call SetModel/SetThinkingLevel.
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
Expand Down