Repository navigation
Replies: 1 comment
|
A few useful precedents from other harnesses, based on their documentation rather than comparative testing: Kiro’s fallback notices distinguish which model answered this turn, whether it became the conversation’s model, and whether no model answered. That would make the proposed Paseo notice clearer: show the reason, original and answering models, and whether the switch applies to this response or the session. Subagent switches should appear in the child’s timeline. Senpi separates refusal fallbacks, which stay pinned, from temporary failures that can revert after a cooldown. This reinforces the persistence question above: a fallback notice should say whether subsequent turns will stay on that model. OMP’s recovery policy also checks whether replay could duplicate output or tool actions. For Paseo, I’d leave retries with Claude Code and verify that switching or editing after a refusal doesn’t replay completed tool work. Any retained refused partial should remain display-only. These examples strengthen the visibility and recovery requirements without needing Paseo to build another fallback engine. |
Uh oh!
There was an error while loading. Please reload this page.
What are you trying to do?
I run Claude Code agents in Paseo on Opus 5.5 for security work, which trips Claude's cyber classifier often. When a request is flagged, Claude Code re-runs it on Opus 4.8 and the session stays there. I want to see when that happens, and to choose between switching to the fallback model and editing the prompt to retry on Opus 5.5.
How do you do it today?
I find out when the output gets significantly worse, then confirm it in the session JSONL under
~/.claude/projects/. In the four weeks to 24 October my Paseo sessions logged 106model_refusal_fallbackevents across 47 sessions, allcyber, all Opus 5.5 to Opus 4.8. For each one I counted responses until Opus 5.5 answered again. 76 got back within the session's log, after a median of 36 responses from other models. The other 30 hadn't by the time the log ended. 46 of the fallbacks retracted a partly streamed reply.The cost is more than a weaker model. Opus 4.8 can't read Opus 5.5's thinking blocks, so the API drops them from every request Opus 4.8 answers (preserved thinking: on the Claude API only Fable 5.1 and Mythos 5.1 read Opus 5.5's thinking). The blocks stay in the history and Opus 5.5 can read them again when the session comes back, but Opus 4.8 works without them. In one session Opus 5.5 had settled in its thinking how to build a test fixture. 90 seconds after the fallback, Opus 4.8 was building a different one, with nothing on screen to say why.
My examples use Opus 5.5 and Opus 4.8 because that's the pair I hit. Claude Code routes refusals by model and category, so none of this should be specific to either (details below).
Where does Paseo get in the way?
Main at
72e3316, SDK 0.3.246:appendSystemMessageEventshandlesinit,status,compact_boundary,task_notificationandtask_progress.model_refusal_fallbackandmodel_refusal_no_fallbackfall through. Both are typed in the pinned SDK, with the original and fallback model, the category and the API's explanation.init, and Paseo's Claude provider never emitsmodel_changed, so the agent shows Opus 5.5 while Opus 4.8 answers. Surface the model and thinking level agents actually use #2659 (closed) tried to surface the model in use, noting that "models fall back mid-session". OMP shows its fallback model since feat(omp): steer turns, show fallback model, and expose Fast mode #5557.retracted_message_uuidslists what the CLI withdrew. Paseo doesn't read it, so the partial reads as Opus 5.5's answer, with Opus 4.8's reply after it.switchModelsOnFlagsetting plus an SDK dialog,onUserDialogwithsupportedDialogKinds: ["refusal_fallback_prompt"].buildOptionsdeclares no dialog kinds, and the CLI fails closed: it never sends a kind the host didn't declare. bug: Claude provider never answers MCP elicitation requests — approval-gated MCP tool calls are silently auto-declined #3941 is the same gap for MCP elicitation. With the setting off, a flagged turn ends with a refusal error instead, which I expect Paseo shows as a normal completed turn (the bug: a Claude turn that hits the session limit completes as a normal reply, leaving the agent idle #7636 shape).What would a better workflow look like?
Three changes. Each is useful alone.
1. Show it. When Claude Code falls back, put a warning in the timeline with Claude Code's own wording (the event's
content) and the API's explanation when there is one, and switch the header to the model that's answering. The same notice can offer a rewind to the refused message, so I can edit and retry on Opus 5.5 after an automatic switch. When there's no fallback, fail the turn with the explanation instead of completing it. That fits #7637, and therefusalend reason #7810 proposes in its "cut off" group.2. Mark the refused partial, don't hide it. The SDK says to remove
retracted_message_uuidsfrom the transcript on receipt. I'd keep them on screen, dimmed or collapsed and labelled "Refused partial reply from Opus 5.5, not sent to the model". What Opus 5.5 got as far as writing helps me decide whether to switch or rephrase, and Opus 4.8 never sees it.3. Let me choose. A Claude provider setting with three options: use my Claude settings (the default, and today's behaviour), switch automatically, or ask me. "Ask me" shows Claude Code's switch-or-edit dialog on the existing permission card.
Open questions:
refusal_fallback_promptpayload and results come from the CLI. The SDK types documentonUserDialogandsupportedDialogKindsbut not this kind's shape. Is that solid enough for change 3, or should Paseo stop at 1 and 2?cancelledand ends the turn refused. Claude Code itself never prompts subagents and always falls back for them.config.model. If a fallback goes throughmodel_changed, Opus 4.8 would survive a reload, and the next query rebuild today passesconfig.modeland quietly returns to Opus 5.5. I'd keep the user's choice as the configured model and show the fallback as runtime state.Technical notes
Routing. Claude Code routes by model and category. Fable 5.1, Fable 5 and Opus 5.5 fall back to Opus 4.8 on
cyberand to Opus 5 onbio, and Sonnet 5.5 falls back to Sonnet 5 oncyber. Abioflag on Sonnet 5.5 or Opus 5 has no fallback and ends inmodel_refusal_no_fallback. Paseo should take the models and the category from the event and hardcode none of them.Scope. Emit
model_changedwhen the event'sscopeissessionor absent.localmeans only a subagent, a/btwside question or a background fork fell back, and the session model hasn't changed.Timing. The SDK calls
model_refusal_fallbackthe end-of-turn notice, so the warning, the explanation andrefused_user_message_uuid(the SDK's "rewind target and composer prefill for edit-and-retry") all arrive after Opus 4.8's reply. Handling that notice is the reliable path. The replacement assistant message can carry an optionalsupersedeslisting the refused messages. Where it's present, Paseo can mark the partial and place the warning earlier, but it arrives after the first content block completes, so it can't guarantee a warning before fallback text streams.refused_user_message_uuidis null when the turn wasn't human-authored (22 of my 106), and then the notice offers no rewind.api_refusal_explanationwas null in 37 of the 106, socontentcarries the notice. The SDK types say never to parse the explanation.The setting. Paseo can pass
switchModelsOnFlagthrough the SDKsettingsoption, the way fast mode already does. "Use my Claude settings" passes nothing. "Switch automatically" and "ask me" passtrueandfalse, which override a savedswitchModelsOnFlagin the user's own settings. That's why they're explicit choices and not the default.The dialog. "Ask me" declares the kind and maps the dialog onto the permission card as
kind: "other"with custom actions, as plan approval uses:retry_fallbackedit_promptDismissing the card gives
cancelled, and the turn ends refused. No refusal event fires on the edit path, sorefused_user_message_uuidisn't available there and Paseo uses its own turn anchor. In ask mode the dialog's payload says to evict the partial only once the dialog resolves, never on receipt, so it's on screen while you decide.Compatibility. Current apps already render custom actions on any non-question card, and
otheris in the protocol enum, so nothing breaks for old apps (protocol-compatibility.md). The kind is declared once at query start, so the daemon should declare it only on a CLI version that has the dialog (there's a probe inresolveClaudeCodeVersion).Prior art. Pi keeps a refusal's explanation as the error with
rawStopReason: "refusal", leaves refused turns out of what it replays to the model, and since 1.3.0 drops thinking from a different model on purpose (earendil-works/pi#11000), with a TUI notice ("Anthropic dropped N thinking blocks") when the API drops some. Its recovery moves are switch model and resend, or fork from the user message and edit it, the same two choices the dialog offers.All reactions