4.4 KiB
4.4 KiB
Plan: No Silent Model Fallback
Spec: docs/specs/agents/requirements/no-silent-model-fallback.md
Status: implemented (all tasks done, gate green; PR pending)
Date: 2026-08-04
Summary
Eliminate implicit model fallback. Three per-profile modes (strict / fallback-model / auto-fallback toggle) control every fallback point: session-start model application (agentctl runtime), boot reconciliation (never heal away a gone model), office post-start failure re-dispatch, and all model/profile pickers in the UI (grey out gone models; block profiles whose start model is gone unless a fallback is configured).
Design Decisions
auto_fallbackwins overfallback_model— the toggle restores legacy behavior wholesale; the fallback-model field is hidden/disabled when it is ON.- Strict is the default for existing profiles (
auto_fallback=0, empty fallback). Existing users who relied on automatic fallback must opt in via the toggle. - Gone-ness is computed on the frontend:
configured ∉ advertised. The backend only stops causing silent switches (reconciler, runtime best-effort, post-start requeue). - Session-start detection uses the session's advertised model list
(
execution.GetModelState().ModelsafterInitializeSession) plus theSetModelresult;-32601/MethodNone("no model selection support") is never treated as availability failure. - One-shot office retry with the fallback model uses a new nullable
runs.fallback_modeloverride consumed (and cleared) by the next dispatch — one extra attempt, then terminal explicit failure. Bypasses the resolver; counts againstMaxAttemptsPerRunlike any attempt.
Waves (dependency order)
Wave 1 — Backend foundation (profile fields + strict start)
task-01-profile-fields.md—fallback_model+auto_fallbackon agent_profiles: migration, model struct, store, DTO, create/update controller, frontend type/normalize round-trip.task-02-reconciler-keep-gone.md—healProfilekeeps a gone start model (and gone fallback model); seed-default branch unchanged.task-03-session-start-strict.md— agentctl runtime applies the start-model policy at session start (+ context-reset path): strict fail-explicit, fallback-model apply, auto-fallback legacy. Actionable error messages.
Wave 2 — Office post-start gating
task-04-post-start-gating.md—HandlePostStartFailuregates on profile mode: strict → escalate (no requeue); auto-fallback → requeue; fallback-model → one-shot forced retry viaruns.fallback_model(Run model + repo + dispatcher), then terminal failure. Error-code → actionable message mapping.
Wave 3 — Frontend
task-05-picker-gone-support.md—ModelConfigSelectordisabled options; session toolbar keeps + greys the stale active model (session-models.ts,model-selector.tsx).task-06-profile-editor.md— start model red/disabled when gone; new "Agent fallback" row; "Fallback automatically to next model" toggle (hides fallback row);cli-profile-editorparity; i18n keys.task-07-profile-picker-gating.md— task-create dialog + office setup profile pickers block strict-gone profiles (reason tooltip) and warn on fallback-mode profiles;AgentProfileOptiongains model fields.
Wave 4 — E2E
task-08-e2e.md— Playwright: gone start model red/disabled in editor, blocked profile in task-create picker, fallback warning + toggle behavior, on the mock backend.
Verification Commands
- Backend:
make -C apps/backend test ./internal/agent/settings/... ./internal/agent/runtime/lifecycle/... ./internal/office/scheduler/...(task-specificgo testtargets are listed per task file). - Frontend unit:
cd apps/web && pnpm vitest run <file>per task. - Typecheck:
cd apps/web && pnpm run typecheck. - E2E:
cd apps/web && pnpm e2e(gated project or targeted spec). - Full gate:
make fmt && make typecheck test lintper AGENTS.md.
Risks
- Behavior change for strict profiles (launch now fails when start model gone). Mitigated by actionable error mapping.
- The one-shot office retry must respect
MaxAttemptsPerRun/ route-cycle semantics (counts as a normal attempt row). - Probe cache staleness: acceptable; agent
validateAvailableModelfails fast and the runtime treats that as unavailable. - i18n ratchet: every added/edited user-facing line must use
t().