Skip to content

fix(server): apply a schedule's configured model to its run - #115

Open
setkyar wants to merge 1 commit into
ygncode:mainfrom
setkyar:fix/schedule-applies-configured-model
Open

setkyar wants to merge 1 commit into
ygncode:mainfrom
setkyar:fix/schedule-applies-configured-model

Conversation

@setkyar

@setkyar setkyar commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Problem

A schedule's configured model is ignored. Every run falls back to pi's global default model instead.

Reproduction with a schedule set to opencode-go/deepseek-v4.1-flash (+ thinking high), while pi's default is openai-codex/gpt-5.6-sol:

{"type":"model_change","provider":"opencode-go","modelId":"deepseek-v4.1-flash","implicit":true}
{"type":"thinking_level_change","thinkingLevel":"high","implicit":true}
{"type":"model_change","provider":"openai-codex","modelId":"gpt-5.6-sol"}
{"type":"thinking_level_change","thinkingLevel":"low"}

The run then dies on the default model's provider (in this case an expired openai-codex login), and the session shows Switched to model: openai-codex/gpt-5.6-sol instead of the configured model.

Root cause

fireScheduleContext writes the schedule's model/thinking into the new session file as implicit model_change / thinking_level_change entries and relies on pi picking them up from history. But pi only restores the session model when the session already has messages:

// dist/core/sdk.js (createAgentSession)
const existingSession = sessionManager.buildSessionContext();
const hasExistingSession = existingSession.messages.length > 0;
if (!model && hasExistingSession && existingSession.model) { /* restore */ }
if (!model) { /* settings default */ }

A freshly created schedule session has zero messages, so the implicit settings are skipped and the global default model wins.

Verified directly, independent of pi-web:

pi --mode rpc --session <session-with-only-model_change-deepseek>
# get_state -> model openai-codex/gpt-5.6-sol, thinkingLevel low

Fix

After the worker is ensured and before the instructions are sent, apply the schedule's explicit model and thinking level to the worker. When the schedule leaves them unset, pi's defaults are kept (no RPC calls).

internal/server/scheduler.go:

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

This makes the schedule independent of whether pi can restore settings from an empty session. The trade-off is a visible Switched to model: … entry on scheduled runs, which seems preferable to runs silently using the wrong model.

Tests

Two new table-driven-style tests in internal/server/scheduler_test.go using the existing fakeSender:

  • TestFireScheduleAppliesConfiguredModel — asserts SetModel/SetThinkingLevel are called with the schedule's values and the created session id.
  • TestFireScheduleKeepsDefaultsWhenUnset — asserts neither RPC is called when the schedule has no model/thinking set.

go test ./internal/server/ and go vet ./internal/server/ pass.

Notes

  • Schedules already configured will now actually use their model. Nothing to migrate.
  • Unrelated but observed along the way: handleNewSession + initializeNewSessionWorker rely on the same implicit-entry mechanism, so a session created from another session also starts on the global default rather than the source session's model. I left that out of this PR to keep it focused; happy to follow up if you want it addressed.

pi restores a session's model and thinking level from session history only
when the session already has at least one message. A freshly created schedule
session has none, so the implicit entries written by
CreateSessionFileWithSettings are ignored and pi falls back to its global
default model. A schedule configured for one model therefore silently ran on
another (and failed, for example, when the default model's auth had expired).

Apply the schedule's explicit model and thinking level to the worker after it
is ensured, before the instructions are sent. When the schedule leaves them
unset, keep pi's defaults.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant