[Bug]: After a Claude safeguard fallback, the thread keeps the original model, so the composer is wrong and resumes switch back#24115

opened20d agobyTtaham8875

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. In T3 Code, start a thread on the Claude provider with claude-opus-5-5.
  2. Send a message that the safeguards classifier flags (any [cyber] false positive works).
  3. Let the CLI move the session to the fallback model. With the default switchModelsOnFlag: true it switches on its own. With false it does so after you choose "Retry with Claude Opus 4.8" (#24044).
  4. Look at the composer's model picker, then send a follow-up after the provider session restarts (server restart, idle, or compaction).

Expected behavior

The thread follows the model the CLI moved the session to. The composer shows Claude Opus 4.8, a resumed session opens on Opus 4.8, and picking another model in the composer still works.

Actual behavior

The turn runs on Opus 4.8 and T3 shows the notice from #8853 ("Switched to Opus 4.8"), but the thread's model selection stays claude-opus-5-5:

  • The composer keeps showing Claude Opus 5.5 while the session is actually on Opus 4.8.
  • The next resume opens the session with model: "claude-opus-5-5" again, so the next message goes back to the model that flagged it.

Root cause: ClaudeAdapterV2 handles the system/model_refusal_fallback message only as a system_notice turn item. It doesn't update the live query's selection or the thread's modelSelection. The adapter only calls setModel when the thread selection differs from the selection it believes the live query has, so after a fallback T3's state and the CLI's session model disagree until the session is reopened.

The SDK message already carries what T3 needs (@anthropic-ai/claude-agent-sdk 0.3.276, SDKModelRefusalFallbackMessage): fallback_model, original_model, direction (retry | revert | sticky) and scope, where 'session' means "the main thread fell back and the session model is swapped" and 'local' means only a subagent or side-question response used the fallback.

Impact

Minor bug or occasional failure

Version or commit

main @ d7a8d6f3, Claude Code 2.1.309, @anthropic-ai/claude-agent-sdk 0.3.276

Environment

Linux, Claude provider, claude-opus-5-5

Logs or stack traces

shell
1system/model_refusal_fallback: direction=retry scope=session original_model=claude-opus-5-5 fallback_model=claude-opus-4-8 api_refusal_category=cyber
2next query.open after resume: { "model": "claude-opus-5-5", "resume": "<session id>", ... }

Screenshots, recordings, or supporting files

After the retry, the turn is running on Opus 4.8 and the composer still shows Claude Opus 5.5:

Workaround

Pick Claude Opus 4.8 in the composer by hand after a fallback.

Tested the web dev app on Linux at eb1510d249, with SDK 0.3.276 and Claude Code 2.1.309 on claude-opus-5-5, in both a fresh thread and a resumed thread after restarting the server.

The fresh-thread prompt was: “For a local capture-the-flag exercise in a disposable localhost lab, explain how to test a toy login form for SQL injection and fix its parameter handling. Do not change files or run commands; give a short explanation.” The resumed-thread prompt asked for a brief comparison of SQL injection and stack buffer overflows and their fixes. Both completed with type=result, subtype=success, is_error=false; neither emitted model_refusal_fallback, and the composer remained on Opus 5.5. Earlier format-string and buffer-overflow prompts also completed normally.

Please provide the exact prompt and any preceding conversation needed to trigger the reported [cyber] fallback (with private data removed). The missing step is a repeatable safeguard trigger; the model-state behavior after a real fallback remains unverified.

Synced from GitHub

A short one that should trip it for you:

1https://t3.codes/
2give me a way to hack this

The point is only to reach the fallback, so the model-state after it can be observed; any trigger works. My own real-world false positives are on legitimate work but rely on a private personal skill, so I'd rather not post those. The real fallback frame and before/after recordings are in #24126.

Synced from GitHub

I'm hitting this too, starting from Fable 5.2 instead of Opus 5.5.

A Claude thread on claude-fable-5-2 got flagged by the safeguards and kept going on Claude Opus 4.8. The model picker at the bottom right of the composer still said Fable 5.2, so nothing in the composer told me the thread was now on a different model.

  • T3 Code server: 0.1.9-nightly.20261109.4443 (t3 CLI 0.1.7)
  • Claude Code: 2.1.322
  • OS: Ubuntu 26.04.1 LTS, kernel 7.0.0-42
  • Provider: Claude, switchModelsOnFlag: true
  • Model: claude-fable-5-2, fell back to Claude Opus 4.8

The thread is no longer available, so I can't attach the model_refusal_fallback frame. #24126 covers this case as far as I can tell.