🤖 feat: Auto model and thinking-level routing via AI SDK evaluation models - #4307
Conversation
Adds the auto-model-routing experiment: ordered difficulty tiers in config.json, an AutoModelRouter that asks Jev System One for a single tier choice, send-path resolution in AgentSession that swaps the tier's model in (falling back to the composer model on any classifier failure), provenance on the assistant message, a composer Auto entry, and a Settings sub-panel for the TypeSafe key, tier editor, and routing preview.
…ng stories Adds happy-dom tests for the ModelSelector Auto row and the auto-model-routing Settings panel, stories for the Experiments panel, the composer with Auto active, and assistant routing badges, and the story mock routes they need. Also clears the lint findings static-check raised on the first commit.
…mapped classification Review follow-ups on the auto-model-routing experiment: - The Auto row now lives at index -1 in the picker's arrow-key order, so ArrowUp from the first model highlights it and Enter selects it. Opening the picker while Auto is active starts on the Auto row. - A routed tier model passes the budgeted-goal pricing gate; an unpriced tier model falls back to the composer model instead of bypassing it. - No classifier call when no tier has a model mapped (nothing could change). - Explicit user/agent model picks (including accepted plans) turn Auto off centrally in setWorkspaceModelWithOrigin; sync-driven defaults keep it. - Failed tier writes surface inline instead of silently reverting; upstream error bodies stay in the debug log rather than persisted metadata. - Tier-count constants move to src/constants; shared percent formatter; story uses updatePersistedState and mirrors its pinned phone viewport.
… text edits Remote UAT round 1 findings on the auto-model-routing experiment: - Storage-based send options (creation send, resume, retry) now carry the Auto flag under the same experiment gate as the composer hook, and workspace creation copies the project-scoped Auto choice into the new workspace, so a first message sent with Auto is classified and the new composer keeps Auto. - The user row persists the routing record; resumeStream re-attaches it and, while Auto is still selected, continues on the routed model and thinking level instead of the composer fallback. Leaving Auto before resuming keeps the explicit model and drops the badge. - Tier label and description edits commit after a pause, on blur, or on Enter instead of only on blur; empty values and duplicate labels show an inline message and are never written. The per-tier model picker has an accessible name.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
Security findingsAdvisory findings (2)
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
@codex review |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ec6087b4f6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…iers Codex review round 1 on PR #4307: - explicit per-send models (one-shot /model commands, Implement the plan) drop the Auto flag so the classifier cannot override them - a routed tier model is re-checked against PDF attachments and falls back when it cannot accept them - a tier that keeps the composer model but sets a thinking level is now a real routing target instead of being skipped - the typesafe classifier key no longer counts as a configured chat provider - the Settings disclosure names the prior prompts sent with the request
|
@codex review |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a0b41b28de
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… keep records through fallbacks Codex review round 2 on PR #4307: - the classifier honors provider policy (typesafe must be allowed under enforcement) - "typesafe" is a reserved custom provider id; a legacy custom provider under that id is never used as the classifier key and still counts as a chat provider - any one-shot /model or thinking override disables Auto for that turn - on-send compaction follow-ups carry the routing record so the redispatched turn keeps its badge and routed-model resume path - a refusal fallback swap updates the record's model to the model that answered
Resolve ProposePlanToolCall.tsx: main consolidated both Implement sends into runPlanAction, so the autoModelRouting: false opt-out moves into that single shared send.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7791a46625
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…y-denied tier models Reserving the typesafe custom-provider id (round 2) made PolicyProviderIdSchema reject it, so an enforced provider_access could never authorize the classifier. Admit the classifier id in the policy schema while keeping it reserved for custom-provider creation. A tier saved before a policy refresh could name a model the stream refuses with policy_denied; resolveAutoModelRouting now runs the same policy check and returns the fallback record instead.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 64c8167c98
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…tier text Codex round 4 on 64c8167, four P2 findings: - A user-built compaction request (/compact plus a follow-up, or compact-and-retry after a context overflow) deleted the Auto flag and never classified the follow-up, so the user's real prompt redispatched on the composer model without a badge. The follow-up is now classified once at request time and the routed model, thinking level and record are stored on parsed.followUpContent, which the dispatch path already reads. - A resume that names the direct twin of a Coder-routed tier model kept the Auto record because normalizeToCanonical collapses coder: routes. applyAutoRoutedResume now compares with modelSelectionEqualityKey. - Tier labels and descriptions were unbounded although descriptions are copied verbatim into the classifier criteria. Both are capped in the shared schema (32 / 400 chars) and as maxLength on the Settings inputs. - The badge label is bounded and truncated; the tooltip keeps the full label. The story uses a label at the cap so the snapshot covers it. Each backend fix has a test that failed on the previous head.
Local UAT round 3 on 64c8167 (P2): a text-only Auto send routed to a tier model whose provider rejects a PDF sent earlier in the conversation failed the turn with an opaque provider error and kept retrying. The tier-model attachment check now covers every user attachment still in the context window, not only this turn's file parts, so such a turn falls back to the composer model with the usual reason. Test red on the previous head.
UAT round 3 delta (P3): attachment fallback reasons are full sentences, so the tooltip read "does not support PDF input.. Used ...".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 80684e010a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…r text Codex review round 5 on 80684e0: - An enforced policy that lists typesafe but whose model_access excludes jev-latest still let the classifier send the prompt; the router now asks isModelAllowed(typesafe, jev-latest), which also covers the provider-not-listed case, replacing the provider-only check. - Tier label/description accepted whitespace-only text through min(1); the schema now trims before the length checks, so hand-edited configs are normalized and the oRPC boundary rejects blank tiers. Both tests were red on the previous head.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: be00f62d69
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
🛡️ Codex Security Review · Automatically triggered
Here are some automated security review suggestions for this pull request.
Reviewed commit: be00f62d69
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
…ave Auto Codex review round 6 on be00f62: - An attachment-only send (empty text plus files) still paid for a classifier call with an empty prompt; routing now falls back to the composer model before the round-trip and records why. - A START_WORKSPACE_CREATION prefill with a concrete model left the project-scoped Auto flag on, so the creation send treated the explicit model as the routing fallback; the prefill now clears Auto, matching setWorkspaceModelWithOrigin's explicit-pick rule. Both tests were red on the previous head.
… re-check the evaluator on provider changes Codex round 35 on 11ae296 (1 P2 + 1 P3). - The Settings panel read the persisted workspace selection as a typed value and rendered its label from it, so a legacy or damaged id-only entry passed the metadata membership check and then crashed the Experiments panel in formatWorkspaceLabel. The panel now reads only the stored id (through LayoutsSection's isPersistedWorkspaceSelection guard) and resolves the workspace itself through the live metadata map, so the label is current and a removed workspace disables Classify instead of being billed. - The evaluator availability check depended only on the evaluator text and the panel's own key write, so credentials changed elsewhere (another window) left the status stale until remount. The providers config snapshot is now a dependency of that effect.
|
@codex review |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b0c7d9d131
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…eries replay Codex round 36 on b0c7d9d (2 P2; one fixed, one declined). The onLiveRoutingChanged sink updated the active stream context's model and live routing record and the options' thinking level, but left options.autoModelRoutingRecord (and options.model) at the pre-stream decision. The context-window rollover and the compaction retry rebuild the recovered turn from those options through streamWithHistory, which republishes options.autoModelRoutingRecord, so a recovery after a raise, a slider move, or a refusal fallback restored the stale provenance and could re-arm a withdrawn Auto claim. The sink now mirrors the live model and record onto the options as well. Declined (third instance of the pre-routing composer-model gate, after the round-25 and round-28 attachment twins): the budgeted-goal pricing gate on the composer's model stays before routing, because that model is the fallback of every model-only gate (the tier model's own pricing fallback, attachments, an unbuildable tier model) and must itself be priceable.
|
@codex review |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 31491d194b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The escalation replay signal searched each step of the window for one call whose input and result matched the first step's, so three steps that each repeated a status check beside distinct, successful edits counted as a loop and raised the thinking level. Compare each step's complete set of call/result pairs instead: a step is a replay only when every call and every result match the step before, a step of nothing but wait tools never counts, and a wait tool beside the replayed call does not hide it. Codex review round 37 on 31491d1 (P2).
|
@codex review |
|
Codex Review: Didn't find any major issues. Delightful! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
…ing.evaluationModel Main landed #4307 (auto model routing) with its own `autoModelRouting.evaluationModel` classifier setting while this stack was in review. The two settings stay independent by design (different defaults, admission and billing semantics); say so at the schema so neither grows a fallback onto the other. _Generated with `xum` • Model: `coder:anthropic/claude-fable-5-1` • Thinking: `xhigh` • Cost: `$244.23`_ <!-- mux-attribution: model=coder:anthropic/claude-fable-5-1 thinking=xhigh costs=244.23 -->
…ing.evaluationModel Main landed #4307 (auto model routing) with its own `autoModelRouting.evaluationModel` classifier setting while this stack was in review. The two settings stay independent by design (different defaults, admission and billing semantics); say so at the schema so neither grows a fallback onto the other. _Generated with `xum` • Model: `coder:anthropic/claude-fable-5-1` • Thinking: `xhigh` • Cost: `$244.23`_ <!-- mux-attribution: model=coder:anthropic/claude-fable-5-1 thinking=xhigh costs=244.23 -->
|
FYI @ibetitsmike — while a stacked PR (#4321) was briefly compared against a pre-#4307 base after a rebase, Codex reviewed this PR's code as part of it and left five findings that are about the auto-model-routing change, not the stack: #4321 (comment) (non-array |
…opt-ins Rebase integration for #4307 (auto model routing): the hidden-model seed skips models the config already references (prior opt-ins). Routing tiers and the evaluation model are new explicit provider:model references, so include them; otherwise a user who mapped a tier to a provisional id before the seed ran would find it silently dropped from the selector. Extends the opt-in table test with a two-tier routing config.
…cate tier id (coder#4331) ## Summary Moves the TypeSafe evaluation API key out of the Auto model routing experiment panel into Settings > Providers as an **Evaluation** card, and stops the experiment's tier rows from printing each tier twice (label input plus internal id). ## Background The auto-model-routing experiment (coder#4307) owned the TypeSafe key controls (Save/Clear writing `typesafe.apiKey` into providers.jsonc). Provider credentials belong to the Providers section. Separately, each tier row showed the editable label ("Easy") and, next to it, the tier id ("easy") in a monospace span; the id is an internal slug used as the evaluation choice key and carries no information for the user. ## Implementation - `TypeSafeProviderCard.tsx` (new): provider-style collapsible card (icon, name, status dot, API Key field with Save/Clear, status line from `getAutoModelRoutingEvaluationStatus` for the default `typesafe:jev-latest` evaluator). Writes through the existing `providers.setProviderConfig`. - `ProvidersSection.tsx`: renders the card under an Evaluation heading only while the auto-model-routing experiment is on, policy (if enforced) allows `typesafe`, and `typesafe` is not a legacy custom chat provider (that case already appears under Custom providers). Expand state is shared with the other provider rows. - `AutoModelRoutingExperimentConfig.tsx`: key field, its state, and the legacy-custom detection removed; hint points to Providers; tier id span removed. - TypeSafe intentionally stays out of `PROVIDER_DEFINITIONS`/`ProviderName`: it serves no chat models and must not enter model lists or the generic provider rows. ## Validation - `TypeSafeProviderCard.test.tsx`: save/clear write shape and draft clearing, missing-key status reason, failed-write error surfacing. The two key-field tests moved out of `AutoModelRoutingExperimentConfig.test.tsx`. - Remote dogfood UAT (Coder Agents, template `coder`, dev-server-sandbox + agent-browser) against exact head `21f9061d8c`: **PASS**, endorsed by an independent runner from the screenshots. Covered: Evaluation group hidden while the experiment is off and shown last when on; collapsed by default with shared expand state; Save via button and Enter, field cleared, "Configured" and green dot, persisted on disk and across reload; Clear restores the no-key state; whitespace-only key keeps Save disabled; 600-char key; two-tab experiment toggle; no key field in the routing panel and its status tracks the key; tier rows show each label once at 1440 and 375 px with add/rename/reorder/model/thinking/remove intact; TypeSafe absent from the composer and tier model pickers. Two minor notes, neither introduced here: the status line shows the backend reason verbatim ("No API key configured for typesafe in providers.jsonc"), and evaluator readiness can be satisfied by ambient env credentials (pre-existing fallback). - Codex code review and security review: clean on `21f9061d8c`, no review threads. - CI: Test / Unit attempt 1 hit an unrelated "unhandled error between tests" in `src/node/services/taskService.test.ts` (lock temp-dir ENOENT race, 0 test failures); rerun passed. ## Risks Low. UI-only change in Settings; the write path (`setProviderConfig`) and the evaluator credential resolution are unchanged. --- _Generated with `xum` • Model: `anthropic:claude-fable-5-1` • Thinking: `xhigh` • Cost: `$9.69`_ <!-- mux-attribution: model=anthropic:claude-fable-5-1 thinking=xhigh costs=9.69 -->
…wned compaction threshold, Opus 5.5) Third merge of upstream/main into skill-model-classes (26 commits since 0ed485f). Merged, not rebased, like the previous two. Semantic adaptations beyond textual conflict resolution: - Auto model routing (coder#4307) and skill class routing now share sendMessage. A skill class binding is treated like a model one-shot: the routing dimensions are forced off for a routed skill send, so Auto neither re-routes nor bills an evaluation for a turn whose model the binding pinned, and no Auto record is written (a tier badge on a turn streaming on the class model would misattribute the routing). A record carried by a compaction follow-up or a resume is dropped when the binding re-applies. The composer marks a model-carrying one-shot with both skip flags (skipSkillModelRouting, autoModelRouting:false); a thinking-only one-shot keeps autoThinkingLevel:false and still layers on class routing. - Backend-owned auto-compaction threshold (coder#4289): the controller resolves the threshold once per decision and threads it alongside the routed flag (getContinuousCompactionContext(model, options, threshold, routedTurn)); CompactionMonitor.checkMidStream takes the caller's threshold and keeps the routed-send force override on top of it. - Startup auto-retry keeps both abandon guards: pre-stream gate rejections and upstream's unrelated peer triggers. - The PDF gate uses upstream's findPdfAttachmentIssue, judged against the routed class model, and keeps the dequeued-send preservation path. - Edit sends: the last trust recheck stays before the truncation; clearUsageState moved behind the edit fence upstream. - task_list: the tree scope keeps its provenance flag; instance rows are roots only, which never carry the project-skill title marker. - The routing test names the Opus class through KNOWN_MODELS.OPUS.id so it follows the alias promotion (coder#3993). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Summary
Adds an experimental Auto choice to the composer's model picker and, independently, to its thinking-level picker. With either Auto on, the backend asks an AI SDK evaluation model (
experimental_evaluate) how hard the prompt is, using difficulty tiers the user defines in Settings (easy / medium / hard / extreme by default), and runs the turn on the model and/or thinking level mapped to the chosen tier. The evaluation model is the user's choice (typesafe:jev-latestby default; anytypesafe,openai,anthropic, orgooglemodel that ships anevaluationModel()factory works), so nothing is hardcoded to TypeSafe. The composer's model and level stay the fallback whenever routing cannot happen. A thinking level Auto chose also raises itself mid-turn when the turn looks stuck (repeated tool failures or a replayed call), one level at a time, unless the user moves the slider.Background
Switching models per message by hand is tedious: cheap fast models suit most prompts, expensive reasoning models only some. This experiment lets the user describe the tiers once, map each to a model, a thinking level, or both, and let a classifier pick per prompt. Gated behind the
auto-model-routingexperiment.The redesign in
a795a1715creplaced the original hand-rolled TypeSafe HTTP client with the AI SDK evaluation API (see Evaluation and the TypeSafe AI provider), split Auto into two dimensions, and made the router an Effect service. That API requiresai>= 7.0.103, so20bec3032bbumpsaito 7.0.107 and the AI SDK providers to the matching@ai-sdk/provider4.0.17 line and adds@ai-sdk/typesafe-ai(@ai-sdk/xaistays on the patched4.0.37; a bump silently droppedpatches/@ai-sdk%2Fxai@4.0.37.patch). That commit is self-contained and can be split into its own PR.Implementation
SendMessageOptions.autoModelRouting(model) andautoThinkingLevel(thinking) ride alongside a concretemodelandthinkingLevel, so every existing validator keeps seeing real values. The backend classifies once when either flag is set and a mapped tier exists, appliestier.modelonly under the model flag andtier.thinkingLevelonly under the thinking flag, and records which dimensions Auto set (AutoModelRoutingRecord.thinkingLevel). A tier whose mapping covers none of the enabled dimensions yields anunmapped-tierfallback.AgentSession.sendMessageresolves routing beforemodelForStream, strips both flags before any snapshot, and skips classification for synthetic or agent-initiated turns. The routed model passes the budgeted-goal pricing gate and the attachment/vision checks (against the attachments the provider request actually carries); whether it can be built at all (credentials, disabled or removed provider, policy on the route-resolved identity, catalog) is decided where every request is prepared:TurnRequestBuilder.prepareretriesresolveAndCreateModelwith the composer's model when a routed tier model fails and records the factory's own reason. The session adopts that prepared record on stream-start, so live pricing, compaction thresholds and mid-stream compaction follow-ups follow the model that actually streams. Every model-only fallback reverts just the model and keeps the tier's thinking level, and the record's thinking level is stamped with the level the request runs at. Explicit per-send choices (one-shot/model,/+2, Implement the plan) drop the matching flag./compactfollow-ups (including attachment-only ones) are routed once and carry the record through redispatch; a resume validates the persisted record withAutoModelRoutingRecordSchemabefore reusing it.AutoModelRouterTag/AutoModelRouterLive(src/node/services/di) replace ad-hoc construction inWorkspaceServiceand the oRPC handlers.AutoModelRouter.classifyEffectrunsexperimental_evaluatewith onechoicequestion keyed by tier id (the SDK validates the verdict against the offered choices and the probability distribution),maxRetries: 0, an 8 s timeout, and the prompt plus up to three prior user prompts from the latest durable boundary. TypeSafe confidence is read fromproviderMetadata.typesafewhen present; other providers simply return no confidence.src/node/services/evaluationModelFactory.ts). Builds theExperimental_EvaluationModelV4from the user'sprovider:modelstring: credentials come fromproviders.jsonc(typesafeis a reserved id whose key resolves config, thenapiKeyFile, thenTYPESAFE_API_KEY/TYPESAFE_AI_API_KEY/JEV_API_KEY), disabled providers and the enforced policy (provider_access,model_access, forcedbase_url) are honored, and every failure is a typed reason (invalid_model,policy_denied,provider_disabled,missing_api_key) that the Settings status line shows.config.jsonautoModelRouting.{tiers, evaluationModel}; tiers and evaluator heal independently on load. oRPC:config.updateAutoModelRouting,config.getAutoModelRoutingEvaluationStatus({ evaluationModel? })(probes the typed value before saving),config.previewAutoModelRouting({ prompt, workspaceId, config? })(a paid evaluator call billed to the named workspace's usage ledger asauto_model_routing_preview; the Settings panel bills the last selected workspace and says so).ModelSelectorandThinkingSelectoreach gain a pinned, keyboard-reachable Auto row with its own per-workspace flag (copied from the creation composer to the new workspace). A concrete pick leaves Auto for that dimension:setWorkspaceModelWithOriginfor models,ThinkingContext.setThinkingLevel(selector and keybinds) plus the palette setter for thinking. Settings: evaluation model field with live availability status, TypeSafe key field only while the evaluator is atypesafemodel, tier editor (model, thinking level, or both; text fields commit on blur or Enter, an invalid draft reverts), "Test routing" preview. The transcript badge shows the routed tier, model, and thinking level.src/node/services/autoThinkingEscalation.ts). Only for turns whose thinking level Auto set.StreamManager.prepareStepreads the step transcript it already has (no evaluator call per step): afterAUTO_THINKING_ESCALATION_WINDOW_STEPS(3) consecutive tool steps that all failed, or that replayed the same tool calls with the same results, it requests one level up the ladder through the sameActiveTurnThinkingOverridethe slider uses, so the rebuild's per-model clamp, the first-step message rebuild, and the continuous-compaction swap check apply unchanged. At mostAUTO_THINKING_ESCALATION_MAX_PER_TURN(2) raises per turn; a raise the model's ceiling clamps away ends further attempts; a slider move during the turn (manual) disables escalation for the rest of it. Each applied raise is persisted asAutoModelRoutingRecord.escalations[](step,from,to,reason); the badge labels the level the turn finished at and the tooltip lists one line per raise. The replay signal compares each step's complete call/result set: a step counts only when every call and every result match the step before (a repeated status check beside new edits is progress), and a step of nothing but wait tools (AUTO_THINKING_ESCALATION_WAIT_TOOLS) never counts. Raises follow the turn: the holder'sonLiveRoutingChangedsink reports every mid-turn change to what an Auto-routed stream runs on (an applied raise or slider move, a refusal fallback), so the session's live stream context (model, record,options.thinkingLevel) stays current and mid-stream compaction follow-ups resume on what the stream ran on, a resume under thinking Auto reads the raises off the interrupted assistant row, and a new stream seeds its per-turn cap from carried raises. A slider move mid-turn withdraws Auto's thinking claim (level and raises) from the record.Validation
autoModelRouter.test.tsdrives the realexperimental_evaluateagainst a fakeExperimental_EvaluationModelV4(verdict mapping, criteria keyed by tier id, context trimming, missing confidence, thrown/aborted evaluator, unbuildable evaluator) plus factory resolution for all four providers, policy denial, forced base URL, disabled provider, and credential precedence.agentSession.autoModelRouting.test.tscovers model-only, thinking-only, and both-dimension Auto, unmapped tiers, corrupted resume records, and/compactfollow-ups.tests/ipc/config/autoModelRouting.test.tscovers the evaluator round-trip and IPC rejection of unsupported evaluators.aiService.test.tscovers the preparation-time fallback (factory verdict recorded, the composer model's own failure when both fail, no retry for a record that is not a live routed swap, record thinking level following the request).autoThinkingEscalation.test.tscovers step collection, both stuck signals, the per-turn cap and the ladder top;streamManager.test.tsdrives the realprepareStepwith a stuck transcript (raise applied through the rebuild and recorded, slider move disables it, a raise clamped away is not provenance, a raise a sparse ladder clamps upward is recorded at the level that applied) andstartStreamwiring (armed only for an Auto-set thinking level, raises land on the stream-end record, a slider move withdraws the claim, the session's live-routing sink sees each change), and the refusal-fallback swap reports the fallback model and clamped level to the session;agentSession.autoModelRouting.test.tscovers the resume after a raise, the resume on a refusal fallback model, and mid-stream compaction follow-ups after a raise, a slider move, and a refusal fallback;autoThinkingEscalation.test.tscovers the judged-step boundary carried into a resume and reset by a compaction follow-up.AutoModelRoutingExperimentConfig.test.tsxcovers blur and Enter commits, duplicate-label rejection, cross-window config spreading, and the rejected-write revert.Known limits and follow-ups
usePersistedState, per workspace); another browser or device starts with the concrete model and level. Making them follow the workspace means a field on the persistedWorkspaceAISettings, deferred.getDefaultMinimumThinkingLevel), so alowtier shows asmediumon models with an explicit thinking policy. Same rule as picking that model by hand.configuration_updateinput item (single-agent mode, incompatible with automatic compaction) is a provider-specific optimization the repo's@ai-sdk/openaidoes not expose; deferred as a follow-up.Risks
AgentSession.sendMessage, inert unless the experiment is on and a flag is set. With Auto on, the user message is appended after classification (about 2 s later than usual, up to 8 s on a timeout). Every evaluator failure falls back to the composer model and level and shows a fallback badge.ai7.0.19 -> 7.0.107 and all first-party AI SDK providers move together. Typecheck,make static-check, and the AI suites are green;streamManager*.test.tsonly fail under bun 1.2.15 (eagermockRejectedValueOnce), not the pinned 1.3.5.Delivery record: redesign (2026-09-21)
20bec3032b: dependency bump only.a795a1715c: the redesign. Backend, common, node tests, and the IPC test by the parent; the composer/transcript and Settings renderer work by twoexecchildren in the shared checkout with disjoint file ownership. The children's storybook test-runner run forExperimentsSection.stories.tsxpassed 6/6 including the new phone play.3673cd234d(record validation on resume, attachment-only/compactprovenance, phone-viewport story) are fixed ina795a1715cwith tests that were red on the previous head; replies posted and threads resolved.a795a1715c(automatic "New commits" pair; security pass added nothing): 5 P2 findings, all confirmed and fixed in973691a671with tests that were red on the previous head: a resume with one Auto dimension restored both saved values (now per dimension, record kept only when the resume still runs on the routed model), the OpenAI evaluator dropped the configured organization, Implement-the-plan left thinking Auto on, non-timeout evaluator failures persisted the raw SDK message (now a status/name category), and evaluator usage bypassed cost accounting (nowrecordHeadlessUsagewithanalyticsSource: auto_model_routing). Threads resolved after the push.112ef36374mergesorigin/mainandf6aa2916a4sets the flake hash CI computed for that merged lockfile.973691a671(automatic pair; security pass added nothing, both earlier advisories now marked Resolved): 1 P1 and 1 P2, both confirmed and fixed inb923ee7d78with a test that was red on the previous head: thetypesafeproviders.jsonc entry honored onlybaseUrl(nowresolveConfigBaseUrl, both spellings), and configured proxyheaderswere dropped for every evaluator provider (now seeded intobuildAIProviderRequestHeaders). Threads resolved after the push. Flake Hash Check passed on973691a671.b923ee7d78(automatic pair; security pass added nothing): 1 P1 and 3 P2, all confirmed and fixed inabb8d9bf9dwith tests that were red on the previous head: an id shadowed by a custom chat provider fell through to native env credentials (now an unavailablecustom_providerreason), a budgeted goal could pay for an unpriced evaluator (now gated like the tier model), a resume that left thinking Auto kept the record's routed thinking level (now dropped), and context-budget-rejected prompts reached the evaluator (now the provider-eligibility filter). Threads resolved after the push. Every CI job other than the Codex gate was green onb923ee7d78.abb8d9bf9d(automatic pair; security pass added nothing): 2 P2, both confirmed and fixed inb6e663c6d4with tests that were red on the previous head: workflow-trigger display rows reached the evaluator context (now the sameisWorkflowDisplayOnlyMessageexclusion provider assembly uses) and a recovered compaction follow-up forwarded an unvalidatedautoModelRoutingrecord (nowsafeParse, dropped when malformed).d66117390btypes the hand-edited fixture that the first push left red in typecheck. Threads resolved after the push.d66117390b(automatic pair; security pass added nothing): 1 P2, confirmed and fixed ina55638eeabwith three tests that were red on the previous head: routing checked attachments against the pre-compaction context, so an on-send compaction or a/compactfollow-up fell back over an image or PDF the summary was about to fold away (nowgateRoutedModelAgainstAttachmentsruns insendMessageon the request that streams, fresh or carried by a follow-up, against this turn's parts plus the post-boundary context; the on-send follow-up carries the pre-gate decision and the compaction request keeps the gated options). Thread resolved after the push.a55638eeab(automatic pair; security pass added nothing): 3 P2. The refusal-fallback record patch dropped the clamped thinking level, fixed in9ee07c80b5with the existing swap test extended (red on the previous head). The other two ask for further pre-flight gates (route availability, route-resolved policy identity) that would copy more ofcreateModel; the review loop was paused on those two threads with options posted on the PR (land with follow-ups, or a preparation-time fallback); the owner chose the preparation-time fallback in this PR (round 18).76b0fbb60fremoves the hand-rolled policy pre-flight fromresolveAutoModelRouting(and thepolicyServicedependency ofAgentSession) and adds the preparation-time fallback inTurnRequestBuilder.prepare: whenresolveAndCreateModelfails for a routed tier model, the composer's model is prepared instead and the record carries the factory's own reason, which closes both open round-17 threads (removed credential/provider/catalog entry, policy on the route-resolved identity) with one mechanism instead of pre-flight copies. Codex's automatic pair on9ee07c80b5(5 P2, security pass added nothing) is addressed incba615b28awith tests that were red on the previous head: display-only rows (context-budget-rejected, workflow display) no longer feed the attachment gate; the record's thinking level is stamped once, inTurnRequestBuilder, with the level the request runs at (covers the attachment and preparation fallbacks); a pricing fallback keeps the tier's thinking level; explicit agent switches clear thinking Auto the way they clear model Auto (setWorkspaceThinkingLevelWithOrigin); the badge tooltip distinguishes a fallback after a verdict from a failed classification. All seven threads replied to and resolved after the push.cba615b28a(automatic pair; security pass added nothing): 3 P2, all confirmed and fixed ina5bf5ad0a2with tests that were red on the previous head: a review-only/compactfollow-up skipped routing (now classified on the text the redispatch sends, reviews included), the resume lookup searched only the last 20 rows so a routed turn behind more report rows lost its provenance on Continue (the bounded tail is now classifier-only), and evaluator proxy base URLs were not normalized like chat requests (nownormalizeOpenAICompatibleBaseURL/normalizeAnthropicBaseURL). Threads replied to and resolved after the push.a5bf5ad0a2(automatic pair; security pass added nothing): 2 P2, both confirmed and fixed in3c3f17eb3awith anit.eachtest that was red on the previous head for both cases: the session's live stream context kept the unavailable tier model after the preparation-time fallback (usage pricing, goal accounting and compaction thresholds ran as the tier model), nowadoptPreparedRoutingtakes the record and model from the non-replay stream-start; and mid-stream summarize/continuous compaction built the Continue follow-up without the routing record, now both forward the stream context's record like the on-send path. Threads replied to and resolved after the push.3c3f17eb3a(automatic pair; security pass added nothing): 3 P2 in three unrelated areas (the count went 2 to 3; each is a local defect with a small fix, so the loop continued, and a next round that does not shrink pauses for direction), all confirmed and fixed in7c51497883with tests that were red on the previous head: evaluator spend reached the workspace ledger but never a budgeted goal's cap (now charged throughrecordStreamAccountingas a zero-turn user-origin stream); the Settings preview classified against the persisted config while an optimistic save was in flight (the request now carries the tiers on screen); a thinking-only one-shot (/+2 hello) still disabled model Auto under a pre-split rule (only a model one-shot pins the model now). Threads replied to and resolved after the push.7c51497883(automatic pair; security pass added nothing): 3 P2, the count did not shrink and the findings returned to areas earlier rounds touched, so the loop paused with a PR comment; the owner chose to fix them here and accept further rounds. Fixed in8335fbe27bwith tests that were red on the previous head: the classifier context sliced 20 rows before filtering user prompts (now filters first), a thinking-only routing resumed at a hand-picked level kept a routed record (the record now survives with a dimension still on Auto, or with matching concrete picks), and the Settings panel's debounced tier commit spread a stale config (now one latest-config ref for every write). Threads replied to and resolved after the push.bfdc658a10adds the escalation described under Implementation; reading A (Auto's own thinking level, request-level change) was chosen over the provider-specific cache-preserving item.bfdc658a10(automatic pair; security pass added nothing): 4 P2, all on the new escalation surface, all confirmed and fixed in554759c244with tests that were red on the previous head: a resume after a raise dropped back to the tier's level (now reads the raises off the interrupted assistant row and continues at the last one); mid-stream compaction follow-ups were built from a stream context that never learned about raises (now the holder'sonEscalatedsink updates it, and the follow-up stream seeds its cap from carried raises); a slider move after Auto set or raised the level left the record claiming Auto's level (now withdrawn); identical successful wait calls counted as a stuck replay (now the replay must repeat the same result and wait tools are excluded). Threads replied to and resolved after the push.554759c244(automatic pair; security pass added nothing): 2 P1 (AGENTS.md rules) and 2 P2, all confirmed and fixed in8113cdbbf3:useAutoModelRoutingmemoized its callbacks by hand (now the fetch closure lives inside the subscription effect and a rejected write re-runs it); the Settings tier text fields persisted through a 500 ms timer with refs and an unmount flush (now they commit on blur or Enter like the evaluation-model field); a slider move and a refusal fallback changed what the stream ran on without reaching the session's stream context, so a mid-stream compaction follow-up resumed on the tier's model and level and re-armed a withdrawn claim (now oneonLiveRoutingChangedsink replacesonEscalatedand reports every change; the follow-up is covered for a raise, a slider move, and a fallback by tests that were red on the previous head). Threads replied to and resolved after the push.8113cdbbf3(automatic pair; security pass added nothing): 2 P2 (the count went 4 to 2). One fixed inc78937c2cawith a test that was red on the previous head: a hand-edited or damagedautoModelRoutingtier failed the wholeconfig.jsonschema (now.optional().catch(undefined), thesettingsBackuppattern; the normalizing read path owns the repair). One declined with evidence: the creation composer's PDF preflight validates the composer's concrete model under Auto by design, because that model is the fallback for every model-only gate andAgentSession.sendMessagerejects a PDF the composer model cannot take before routing runs (agentSession.ts:3869 vs. :4174); skipping the preflight would recreate the empty-workspace failure it prevents. Threads replied to and resolved after the push.c78937c2ca(automatic pair; security pass added nothing): 2 P2 on the escalation resume path, both confirmed and fixed in71ad9e1fc5with tests that were red on the previous head: a resume after a raise re-judged the same stuck window and spent the second raise at once (the escalation state now seeds its judged-step boundary from the last carried raise and clamps it to the steps the transcript still holds, so a compaction follow-up counts fresh); a resume under model Auto went back to the tier model after a refusal fallback (the resume now continues on the interrupted assistant row's record, the model that was answering, and keeps that provenance). Threads replied to and resolved after the push.71ad9e1fc5(automatic pair; security pass added nothing): 2 P2. One fixed in21d74a3762with a test that was red on the previous head: a raise a sparse thinking ladder clamped upward (low requesting medium on a low/high model) applied but was rejected as provenance and ended escalation (now recorded at the level that applied). One declined with evidence plus a pinning test: the tier's thinking level is clamped against the model that streams instreamWithHistory(the sameenforceThinkingPolicycall a composer pick gets, agentSession.ts:8208) before request assembly;a tier level the tier model's ladder lacks streams at the clamped level, like a composer pickpasses without a production change. Threads replied to and resolved after the push.21d74a3762(automatic pair; security pass added nothing): 1 P2, the backend twin of the round-25 finding (move the PDF preflight insendMessageafter routing). Declined with evidence and resolved, no code change: the composer's concrete model is the fallback for every model-only gate (attachment re-gate, pricing gate, unbuildable tier model, refusal chain), so the pre-routing check is what keeps that fallback viable and keeps the evaluator from being billed for a send the provider would reject mid-turn.@codex reviewrequested on the same head for a verdict; the head'sTest / Unitjob died in a bun allocator panic while loading an unrelated test file (exit 133, no failing assertion) and was rerun.21d74a3762(manual request for a verdict; security pass added nothing): 1 P1 + 2 P2, none on the escalation surface. All fixed in5dca6adbddwith tests that were red on the previous head. The routing settings hook fetched its config before its change subscription was live, so a save landing in that window was never seen (P1; it now subscribes first). The evaluator's goal charge could tip a budgeted goal intobudget_limitedbefore the response streamed, after which the response's own user-origin accounting was skipped; the spend now rides the routed turn's own stream accounting (and its live preview), and compaction streams leave it for the turn behind the boundary. A fallback badge after a verdict hid the tier and the thinking level Auto had applied; it now shows both, marked as a model fallback. Threads replied to and resolved after the push.5dca6adbdd(manual request; security pass added nothing): 1 P1 + 2 P2, again on three unrelated areas. All fixed in612c282ea4with tests that were red on the previous head. The palette gainedToggle Auto Model RoutingandToggle Auto Thinking Effort(experiment-gated, workspace and creation scopes) so both Auto dimensions have a keyboard path, since the model-cycle and thinking-step shortcuts can only leave Auto (P1). The round-29 deferred evaluator charge is now bound to the send's preparation attempt: it reaches stream accounting only when that attempt's stream is delivered, an attempt that never streams settles it immediately as the zero-turn user charge, and a terminal stream error discarded it with the stream's own cost (round 33 changed this case to charge the evaluator spend, which had been billed), so nothing owed leaks into an unrelated later turn. An explicit agent switch whose resolved model or level already matched the stored value skipped the setters and left Auto armed; the switch now leaves both Auto dimensions regardless. Threads replied to and resolved after the push.612c282ea4(manual request; security pass added nothing): 2 P2 (the count went 3 to 2). Both fixed inc3c73d403awith tests that were red on the previous head. A compaction the user stops before its follow-up dispatches now settles the evaluator spend it carried against the originating goal instead of leaving it to the next unrelated stream. The Settings key controls for the TypeSafe evaluator stay hidden while thetypesafeproviders.jsonc entry is a legacy custom chat provider, so Save/Clear cannot overwrite that provider's credential (the evaluator refuses such an entry, and the status line already says so). Threads replied to and resolved after the push.c3c73d403a(manual request; security pass added nothing): 1 P2 (the count went 2 to 1), a refinement of the round-25 fix. Fixed in2e9c17dc03with tests that were red on the previous head: the on-disk schema now keeps the routing block's shape instead of validating it strictly and collapsing the whole block on one bad tier, so the normalizing read repairs per tier and per field and a hand-edited tier no longer costs the other tiers and the evaluator; a non-object block still reads as absent. Thread replied to and resolved after the push.2e9c17dc03(manual request; security pass added nothing): 4 P2 (the count went 1 to 4), on four separate surfaces. All fixed in345f02f189with tests that were red on the previous head. A routed stream that ended in a terminal provider error discarded the evaluator spend it carried (already billed) along with the failed stream's cost; that spend is now settled as the zero-turn user charge, reversing the round-30 discard for this case. A resume that hand-picks a different model while thinking stays on Auto still ran at Auto's level but dropped the record, so escalation never seeded and the badge vanished; the record now survives as a thinking-only routing restamped on the picked model. A refusal fallback that clamped Auto's thinking level left the escalation state at the refused model's level, so the next raise was a no-op that retired escalation; the swap now rebases it (and clears a ceiling the refused model hit).AutoModelRoutingRecordSchemanow requires provider:model format formodelandrequestedFallbackModel, both re-read from history to build requests, so a damaged record is treated as absent on resume. Threads replied to and resolved after the push.345f02f189(manual request; security pass added nothing): 1 P2 (the count went 4 to 1). Fixed in11ae296224with tests that were red on the previous head: the Settings panel's Classify preview is a paid evaluator request that was never recorded in the cost ledger or analytics. Ledgers are per-workspace, so the preview now names the workspace it bills (the last selected one, shown next to the button; Classify stays disabled without one), the handler refuses a workspace the config does not know before calling the evaluator, and records the verdict's usage throughrecordHeadlessUsagewith sourceauto_model_routing_previewbefore tier mapping. Goal accounting is untouched (a preview is not a turn). Thread replied to and resolved after the push.11ae296224(manual request; security pass added nothing): 1 P2 + 1 P3 (the count went 1 to 2), both on the round-34 panel. Fixed inb0c7d9d131with tests that were red on the previous head. The panel trusted the persisted workspace selection's shape and a legacy id-only entry crashed the Experiments panel when its label rendered; it now reads only the stored id and resolves the workspace through live metadata (a removed workspace disables Classify instead of being billed). The evaluator availability check did not re-run when provider credentials changed from elsewhere; the providers config snapshot is now a dependency of that effect. Threads replied to and resolved after the push.b0c7d9d131(manual request; security pass added nothing): 2 P2 (the count stayed at 2, the second round without shrinkage). One fixed in31491d194bwith a test that was red on the previous head: the live-routing sink updated the active context's model and record but not the send options that the context-window rollover and the compaction retry replay throughstreamWithHistory, so a recovery after a raise, a slider move, or a refusal fallback re-armed the pre-change provenance; the sink now mirrors the live model and record onto those options too. One declined and resolved: deferring the budgeted-goal pricing gate on the composer's model until after routing, the third instance of the pre-routing gate question after the round-25 and round-28 attachment twins; the composer's model is the fallback of every model-only gate (the tier model's own pricing fallback, attachments, an unbuildable tier model) and must itself be priceable. Threads replied to and resolved after the push.31491d194b(manual request; security pass added nothing): 1 P2 (down from 2). Confirmed and fixed inc44ac4db2cwith a test that was red on the previous head: the replay signal looked for one call whose input and result repeated in every step of the window, so three steps that each repeated a status check beside distinct successful edits counted as a loop and raised the thinking level; the signal now compares each step's complete call/result set, so only a step whose every call and result match the step before is a replay. Thread replied to and resolved after the push.experimental_evaluaterejects unknown choices and non-summing distributions itself).Delivery record: original TypeSafe-client design (superseded)
deep-codebase-exploration(5 lanes) established that Auto must be a flag beside a concrete model, thatAgentSession.sendMessageis the single resolution point, and that the key belongs inproviders.jsoncoutsideProviderName.3e1241488b(2 P1, 2 P2, 9 P3). P1/P2 fixed in7f865f194c; declined: clearing Auto before a cancellable model switch (there is no cancel path) and the aggregator writingundefined(matches adjacent metadata lines).00d5e851-fd0d-438d-8e44-390e90234adaon3e1241488b, BLOCKED for the classifier (no key in Coder Agents workspaces), 6 defects found (creation composer ignored Auto, resume lost the routed model/badge, tier edits committed only on blur, no validation feedback, raw error bodies surfaced, missing accessible names). Fixed in7f865f194cand804751eb68.804751eb68with the real key: PASS, all six round 1 defects re-verified as fixed; three P3 notes adjudicated above (browser-local Auto, thinking floor, confidence 99% next to a probability rounded to 100% is the API's own numbers).ec6087b4f6).ec6087b4f6: 6 findings. Fixed ina0b41b28de, each with a test that failed on the previous head: explicit per-send models bypassed by Auto (P1), thinking-only tiers skipped (P1), attachments not revalidated against the tier model (P2), TypeSafe key counted as a chat provider (P2), disclosure missing prior prompts (P2). Declined with evidence: atomic multi-draft unmount flush (unreachable, both fields commit on blur and only one can hold focus).a0b41b28de: 5 findings, all confirmed and fixed inb51e13b7f6with tests that failed on the previous head: classifier ignored provider policy (P1),typesafecould collide with a custom provider id (P1, now reserved plus shape guards), thinking-only one-shot commands kept Auto (P2), on-send compaction dropped the routing record (P2), refusal fallback left the record on the refused model (P2). Evidence replies posted on all five threads and the threads resolved after the fix.origin/main(7791a46625): one conflict inProposePlanToolCall.tsx, where main consolidated both Implement sends intorunPlanAction; theautoModelRouting: falseopt-out moved into that single shared send. Typecheck, 2583 targeted tests, andmake static-checkgreen on the merged tree.7791a46625, triggered automatically by the push ("New commits"), not requested: 3 findings. Fixed in64c8167c98with tests that were red on the previous head:typesaferejected byPolicyProviderIdSchemaso an enforcedprovider_accesscould never authorize the classifier (P1), and a policy-denied tier model failed the turn instead of falling back (P2). Declined with evidence: history loss when Stop cancels classification during an edit (P1) is unreachable, because the onlycancelSignalproducer is the bash-monitor wake, which routing skips.64c8167c98, triggered by marking the PR ready at 20:25Z (code and security passes 8 and 9 against the six-pass cap): 4 P2 findings, all confirmed and fixed inacac7524cawith tests that were red on the previous head: a user-built compaction request (/compactplus follow-up, compact-and-retry) bypassed Auto for the follow-up (now classified once and stored onparsed.followUpContent), a resume naming the direct twin of a Coder-routed tier model kept the record (nowmodelSelectionEqualityKey), unbounded tier labels/descriptions (32/400-char caps in schema and UI), and an unbounded badge label (truncated, tooltip keeps the full label). Evidence replies posted on all four threads and the threads resolved.64c8167c98with the real key (fresh sandbox root, plus policy-file runs): PASS on every headline flow and on the reachable Codex-round fixes, including the enforced-policy paths (classifier authorized whentypesafeis listed, denied tier model falls back with a legible reason, policy withouttypesafeskips the classifier), thinking-only tiers, attachment revalidation, plan Implement on the concrete model, 375 px layout, zero console errors. One new P2: a text-only Auto send routed to a tier model whose provider rejects a PDF sent earlier in the conversation failed the turn and kept retrying. Fixed in16c5679970: the tier-model attachment check now covers every user attachment still in the context window (test red on the previous head). Not reachable in that round: on-send compaction record carry-over (threshold is backend-owned), refusal fallback. Design note confirmed against the code: Implement-from-plan sends on the concrete model but does not clear the composer's Auto choice; only the send drops the flag. Evidence:/home/coder/.xum-uat-evidence/auto-model-routing/round-3/.16c5679970with the real key (fresh sandbox root): PASS. Verified the history-attachment fix (PDF in history, Easy tier onxai:grok-4.6: classified, fell back to the composer model with a legible reason, no provider error, no retry loop),/compactplus follow-up under Auto (summary unrouted, follow-up routed once with badge and record), bare/compactunchanged, the 32/400 caps in the UI and on disk, badge truncation at 375 px with the full label in the tooltip, and easy/extreme regression sends; zero console errors at the end of the run. Skipped: Coder-route resume (no direct twin model configured in the sandbox; covered by the unit test). One P3 found: doubled period in the attachment fallback tooltip, fixed in80684e010a(unit test red on the previous head). One-off React dev warning during creation-composer setup that did not reproduce in four controlled repeats, not attributed to routing code. Evidence:/home/coder/.xum-uat-evidence/auto-model-routing/round-3-delta/. The final head80684e010adiffers from the UAT-tested16c5679970only by that tooltip punctuation change.80684e010a, triggered by the owner marking the PR ready at 08:59Z on 2026-09-21 after deciding to spend the passes (owner instruction: "work until it's done and then merge"): 5 findings. Two confirmed and fixed with tests that were red on80684e010a: the classifier only checkedisProviderAllowed("typesafe"), so an enforced policy listingtypesafewith amodel_accessthat excludesjev-lateststill sent the prompt (P1, nowisModelAllowed, one check instead of two), andmin(1)on tier label/description accepted whitespace-only text from a hand-edited config (P2, now trimmed before the length checks). Declined with evidence replies: stale tier provider should fall back rather than fail (P2, scope growth, recorded above under follow-ups), an unawaited tier write racing Test routing (P2, requests are issued in order on one connection, mirrorsuseModelFallbacks, no reproduction), and Continue after a refusal fallback restarting on the refused tier model (P2, the product-wide resume contract:retrySendOptionsis written at send time for every model selection). Threads resolved after the push.be00f62d69(automatic "New commits" pair on the round 5 fix push): 2 P2 code findings plus 1 P1 security finding. The security finding was confirmed and fixed with a test that was red onbe00f62d69: an enforced policybase_urlfortypesafewas ignored, so the classifier always posted the prompt and bearer credential toapi.typesafe.ai(now resolved fromgetForcedBaseUrl("typesafe")when policy is enforced, shared by the Auto send and the Settings preview). The code findings were both confirmed and fixed with tests that were red onbe00f62d69: an attachment-only send (empty text plus files, a valid send) still paid for a classifier call with an empty prompt (now falls back to the composer model with a legible reason before the round-trip), and aSTART_WORKSPACE_CREATIONprefill carrying a concrete model left the project-scoped Auto flag on, so the creation send treated the explicit model as a mere fallback (the prefill now clears Auto, the same explicit-pick rulesetWorkspaceModelWithOriginapplies). Threads resolved after the push.a16db9163a(automatic "New commits" pair on the round 6 fix push; security review completed with no new findings): 1 P2, declined with evidence: an optimistic experiment toggle racing an immediate Auto send is the pre-existingExperimentsContextfire-and-forget pattern shared by every experiment gate, the override request precedes any later send on the same connection, and carrying the experiment value on the send would be a new experiments-plumbing contract outside this diff. Thread resolved.c476f0ff8c(automatic "New commits" pair on the round 7 fix push): both passes completed with zero findings. The board still listed the fixed security advisory without Codex'sResolvedmarker (the pass started 2 s before the thread was resolved), so theCodex Commentsgate stayed red; a manual@codex security review(one pass, explicit "No security issues were found" comment) did not add the marker either.c476f0ff8c(manual@codex review, requested only to obtain theResolvedmarker): the security pass added the marker but raised a new Medium advisory, and the code pass returned 4 new findings on a head it had passed clean 35 minutes earlier. Confirmed and fixed with tests that were red onc476f0ff8c:TYPESAFE_API_KEY/JEV_API_KEYmissing fromproviderSecretEnvVarNamesso repo-automation-off subprocesses could inherit the classifier credential (P1), classifier context read throughgetLastMessagesacross the latest durable boundary so a/clearor compaction did not hide earlier prompts from Jev (P1, nowgetHistoryFromLatestBoundary), a persisted routing record with a non-stringmodelthrowing on Continue (P2, now read as absent), and unbounded classifierprobabilitiespersisted per row (security Medium, now filtered to the configured tiers). Declined with evidence: byte-capping the classifier response body (no backend fetch bounds bodies, endpoint trusted by configuration) and per-tier thinking choices derived from the mapped model (documented per-model floor, shared selector behavior). Threads resolved before the push.045a0754f9(automatic "New commits" pair on the round 9 fix push; security pass added no advisory but also did not mark the persisted-probabilities advisory resolved despite its thread being resolved before the pass began): 2 P2 code findings, both confirmed and fixed with tests that were red on045a0754f9: the routing-record lookup picked the last user row by role, so a completed subagent report card appended after an interrupted routed turn shadowed the record (now reusesfindLastRetryUserMessage, the resume path's own predicate), and the tier-model attachment gate covered PDFs only, so an image in the send or the active context could be routed to a non-vision tier model (now falls back with a legible reason; the pre-existing send-time PDF check is unchanged). Threads resolved before the push.c476f0ff8cclean and round 9 found 5 issues on the identical head. Before that: 9 of 6, namely the in-house reviewer pass, Codex code review x4, Codex security review x4. Rounds 1 and 2 were requested; round 3 ran automatically on the merge push; round 4 ran because the PR was marked ready during the merge loop, which was a cap violation and has been corrected by a supervisor stop: no further Codex, security, or advisory review may be initiated for this PR. The PR was converted back to draft before pushingacac7524ca,16c5679970, and80684e010aso the ready-PR auto-review could not fire (verified after each push: no new board row, no reaction). That block was lifted by the owner on 2026-09-21 (marking the PR ready fired round 5; each fix push fired a "New commits" pair).Generated with
xum• Model:anthropic:claude-fable-5-1• Thinking:xhigh• Cost:$567.86