Skip to content
Merged
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
4 changes: 3 additions & 1 deletion docs/sequence-flows/schedules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`) │
Expand Down
13 changes: 13 additions & 0 deletions internal/server/scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
73 changes: 73 additions & 0 deletions internal/server/scheduler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down