can1357/oh-my-pi · error · Error
No model selected for handoff
Error message
No model selected for handoff
What it means
SessionHandoff.generateDocument needs an active model to run the handoff generation turn; if session.model() returns undefined it throws immediately before any LLM call. A handoff cannot proceed without a selected model.
Source
Thrown at packages/coding-agent/src/session/session-handoff.ts:119
const sourceSignal = options?.signal;
const onSourceAbort = () => {
if (!handoffSignal.aborted) {
handoffAbortController.abort(sourceSignal?.reason);
}
};
if (sourceSignal) {
sourceSignal.addEventListener("abort", onSourceAbort, { once: true });
if (sourceSignal.aborted) {
onSourceAbort();
}
}
try {
throwIfHandoffAborted(handoffSignal);
const model = this.#host.model();
if (!model) {
throw new Error("No model selected for handoff");
}
const apiKey = await this.#host.modelRegistry.getApiKey(model, this.#host.sessionId());
if (!apiKey) {
throw new Error(`No API key for ${model.provider}`);
}
// Build the handoff request through the SAME pipeline a live turn uses
// (`runEphemeralTurn` / `/btw` share it) so the oneshot reads the
// provider prompt cache the main turn populated instead of cold-missing
// the whole prefix: identical system prompt, normalized tools, and
// transform-/obfuscation-matched message history via
// `convertMessagesToLlm` + `buildSideRequestContext`, plus the live turn's
// effective provider cache key with a unique side `sessionId` so
// OpenAI/Codex append-only state never mixes with the live turn.
const cacheSessionId = this.#host.sessionId();
// The loop sends `promptCacheKey` (providerPromptCacheKey) and falls back to
// the provider session id; providers route on `promptCacheKey ?? sessionId`.
// Both can diverge from this.#host.sessionId() (tan/subagent/shared sessions), soView on GitHub (pinned to 9690622007)
Solutions
- Set a model on the session (e.g. via setModel) before invoking handoff generation
- Guard: if (!session.model()) select a default model first
- Restore/initialize session model state properly after reset or deserialization
- Catch and surface 'select a model first' to the user instead of attempting handoff
Example fix
// before
await handoff.generateDocument(signal); // throws if no model
// after
if (!session.model()) {
await controls.setModel({ model: defaultModel });
}
await handoff.generateDocument(signal); Defensive patterns
Strategy: validation
Validate before calling
if (!session.model()) {
await controls.setModel({ model: defaultModel }); // or surface a picker
}
await handoff.generateDocument(signal); Try / catch
try {
await handoff.generateDocument(signal);
} catch (err) {
if (err instanceof Error && err.message === "No model selected for handoff") {
// prompt user to select a model, then retry
} else throw err;
} Prevention
- Always select a model when initializing/restoring a session
- Check session.model() before any model-dependent operation
- Default to a configured model instead of leaving model unset
- Handle session resets by re-establishing model state
When it happens
Trigger: Calling generateDocument on a session whose model is not set (e.g. before initial model selection, or after a model was cleared/reset).
Common situations: Triggering compaction/handoff on a fresh or reset session before a model was ever chosen; model state lost after session restore; programmatic use of the handoff API without configuring a model first.
Related errors
- No model selected
- Handoff cancelled
- No API key for ${model.provider}
- Handoff generation produced no content
- Compaction already in progress
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/52de72e0ec90fa11.
Report an issue: GitHub.