Skip to content

fix(web): preserve provider defaults for optionless threads - #23528

Closed
juliusmarminge wants to merge 7 commits into
mainfrom
fix/issue-23497
Closed

juliusmarminge wants to merge 7 commits into
mainfrom
fix/issue-23497

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Problem

With the new-thread default set to Claude Opus 5.5 with High effort, an existing thread saved without options displayed High and sent an effort override when its composer was left untouched. It should keep the provider default, Medium.

Change

Use the thread selection as the source of its options, including when it has none. Fall back to project/settings options only when there is no thread selection. A focused regression test covers the omitted-options case; a synthetic fixture rebuilds the captured states and is registered as a manual entry for Knip.

Scope and approval

Fixes #23497
GitCafe: https://git.cafe/pingdotgg/t3code/issues/23497
Triaged bug via the automated issue flow. This fixes the reproduced composer fallback and preserves the existing new-thread and explicit-option behavior.

Verification

The regression fails on main with expected [{ id: "effort", value: "high" }] to be undefined and passes with the fix. Focused model-selection, draft-store and ChatView logic tests: 142/142 passed. Targeted lint and web typecheck passed. Both independent code reviewers returned MERGE. The final scoped tests, targeted lint, and web typecheck also pass after merging main.

Independent proof audit: V1–V4 PASS; 84 artifact hashes verified, all 24 diffs recomputed, and fresh V1 R/F/O captures match exactly with zero changed pixels.

Evidence

Full evidence index · Fixture matrix

Rendering environment

Saved thread selections omitted options. Environment default was Claude Opus 5.5 / effort High; project override inherited. The live descriptor declares Medium as default. Before: High selected and dispatched. After: Medium selected; effort omitted from dispatch. Both send implicit fastMode:false. All captured drafts remained optionless when opening/closing menus.

Linux / Chrome 154; DPR 2, zoom 1, loaded system fonts, scroll 0,0, pointer 0,450. CSS viewports exactly 1440×900 and 390×844. Built-in snapshot caps wide PNGs to 1280×800; narrow PNGs are 780×1688. Source recordings are 2880×1800 and 780×1688. Sources are published unchanged. Diffs use equal original dimensions, no post-capture resizing/registration. Browser measurement · Recording metadata · All eight source videos decode successfully

Four affected rows, each with before/after recordings, resting (R), focused (F), and open-menu (O) stills. At 390 wide, the real More composer controls overflow is used.

V1 — 1440×900, light — complete

Steps: start resting; focus and type Reply with OK.; open the traits/overflow menu without selecting; close it; submit; wait for the successful receipt and completed real Claude run. Actual UTC milestones below align the two recordings.

Milestone (actual UTC) Before After
Resting 2026-10-22T09:39:00.215Z 2026-10-22T09:32:59.321Z
Focused/typed 2026-10-22T09:39:02.404Z 2026-10-22T09:33:01.422Z
Menu open 2026-10-22T09:39:05.004Z 2026-10-22T09:33:04.048Z
Send 2026-10-22T09:39:05.885Z 2026-10-22T09:33:05.002Z
Successful receipt 2026-10-22T09:39:05.990Z 2026-10-22T09:33:05.135Z
Persisted run completed 2026-10-22T09:39:09.485Z 2026-10-22T09:33:08.822Z

Before recording · After recording · Before dispatch/receipt/projection · After dispatch/receipt/projection

Threads: proof-V1-clean → proof-V1. Both created optionless through real RPC; exact persisted create events are in the manifest.

State Before After Diff0: threshold 0, gain 8 DiffT: threshold 26/255, gain 8
Source ff525478e2 4c7f479ac9 ff525478e2 → 4c7f479ac9 ff525478e2 → 4c7f479ac9
R Raw still Raw still 1,103 pixels (0.107715%) 792 pixels (0.077344%)
F Raw still Raw still 1,103 pixels (0.107715%) 792 pixels (0.077344%)
O Raw still Raw still 11,738 pixels (1.146289%) 773 pixels (0.075488%)

V1 before High / after Medium, whole open-menu surface

V2 — 1440×900, dark — complete

Steps: start resting; focus and type Reply with OK.; open the traits/overflow menu without selecting; close it; submit; wait for the successful receipt and completed real Claude run. Actual UTC milestones below align the two recordings.

Milestone (actual UTC) Before After
Resting 2026-10-22T09:27:49.942Z 2026-10-22T09:35:55.259Z
Focused/typed 2026-10-22T09:27:52.109Z 2026-10-22T09:35:57.490Z
Menu open 2026-10-22T09:27:54.727Z 2026-10-22T09:36:00.090Z
Send 2026-10-22T09:27:55.596Z 2026-10-22T09:36:00.800Z
Successful receipt 2026-10-22T09:27:55.786Z 2026-10-22T09:36:00.890Z
Persisted run completed 2026-10-22T09:27:59.050Z 2026-10-22T09:36:03.926Z

Before recording · After recording · Before dispatch/receipt/projection · After dispatch/receipt/projection

Threads: proof-V2-take2 → proof-V2-clean. Both created optionless through real RPC; exact persisted create events are in the manifest.

State Before After Diff0: threshold 0, gain 8 DiffT: threshold 26/255, gain 8
Source ff525478e2 4c7f479ac9 ff525478e2 → 4c7f479ac9 ff525478e2 → 4c7f479ac9
R Raw still Raw still 1,094 pixels (0.106836%) 743 pixels (0.072559%)
F Raw still Raw still 1,094 pixels (0.106836%) 743 pixels (0.072559%)
O Raw still Raw still 12,189 pixels (1.190332%) 681 pixels (0.066504%)

V2 before High / after Medium, whole open-menu surface

V3 — 390×844, light — complete

Steps: start resting; focus and type Reply with OK.; open the traits/overflow menu without selecting; close it; submit; wait for the successful receipt and completed real Claude run. Actual UTC milestones below align the two recordings.

Milestone (actual UTC) Before After
Resting 2026-10-22T09:39:26.174Z 2026-10-22T09:36:15.730Z
Focused/typed 2026-10-22T09:39:28.540Z 2026-10-22T09:36:18.134Z
Menu open 2026-10-22T09:39:30.808Z 2026-10-22T09:36:20.392Z
Send 2026-10-22T09:39:31.446Z 2026-10-22T09:36:21.039Z
Successful receipt 2026-10-22T09:39:31.529Z 2026-10-22T09:36:21.091Z
Persisted run completed 2026-10-22T09:39:34.335Z 2026-10-22T09:36:24.314Z

Before recording · After recording · Before dispatch/receipt/projection · After dispatch/receipt/projection

Threads: proof-V3-clean → proof-V3-clean. Both created optionless through real RPC; exact persisted create events are in the manifest.

State Before After Diff0: threshold 0, gain 8 DiffT: threshold 26/255, gain 8
Source ff525478e2 4c7f479ac9 ff525478e2 → 4c7f479ac9 ff525478e2 → 4c7f479ac9
R Raw still Raw still 2,974 pixels (0.225878%) 2,306 pixels (0.175143%)
F Raw still Raw still 0 pixels (0.000000%) 0 pixels (0.000000%)
O Raw still Raw still 56,243 pixels (4.271707%) 0 pixels (0.000000%)

V3 before High / after Medium, whole open-menu surface

V4 — 390×844, dark — complete

Steps: start resting; focus and type Reply with OK.; open the traits/overflow menu without selecting; close it; submit; wait for the successful receipt and completed real Claude run. Actual UTC milestones below align the two recordings.

Milestone (actual UTC) Before After
Resting 2026-10-22T09:29:32.366Z 2026-10-22T09:37:10.691Z
Focused/typed 2026-10-22T09:29:34.697Z 2026-10-22T09:37:13.073Z
Menu open 2026-10-22T09:29:36.903Z 2026-10-22T09:37:15.319Z
Send 2026-10-22T09:29:37.563Z 2026-10-22T09:37:16.046Z
Successful receipt 2026-10-22T09:29:37.645Z 2026-10-22T09:37:16.157Z
Persisted run completed 2026-10-22T09:29:40.515Z 2026-10-22T09:37:19.689Z

Before recording · After recording · Before dispatch/receipt/projection · After dispatch/receipt/projection

Threads: proof-V4 → proof-V4-clean. Both created optionless through real RPC; exact persisted create events are in the manifest.

State Before After Diff0: threshold 0, gain 8 DiffT: threshold 26/255, gain 8
Source ff525478e2 4c7f479ac9 ff525478e2 → 4c7f479ac9 ff525478e2 → 4c7f479ac9
R Raw still Raw still 2,956 pixels (0.224511%) 2,158 pixels (0.163902%)
F Raw still Raw still 0 pixels (0.000000%) 0 pixels (0.000000%)
O Raw still Raw still 60,032 pixels (4.559485%) 0 pixels (0.000000%)

V4 before High / after Medium, whole open-menu surface

Other visible differences and controls

High changes to Medium, the selected menu row changes, and neighboring controls shift with the wider label. All changed pixels in the paired stills are confined to those areas. Narrow focused pairs are pixel-identical because the trait control is hidden. Diff0 amplifies every nonzero RGB delta by 8; DiffT does the same for deltas ≥26/255. The index includes raw counts and the full unmasked images.

Explicit Low and draft overrides remain Low; new threads still inherit the default. Omitted options and explicit empty options both preserve provider defaults. All eight captured real Claude runs completed successfully.

Web was tested at 1440×900 and 390×844 in light and dark. Desktop shares this web helper; its Electron shell was not separately run. Native mobile already uses the saved selection directly and is unaffected. No wire or persistence changes.

Captures use the source SHAs shown above. The later fixture-only commit leaves production code and tests byte-identical to the after capture.

Main was merged as 09c06b91ac; the three fix files remain byte-identical to the audited head.

CI initially found the manual fixture as unused. Registering scripts/fixtures/*.mjs as a Knip entry fixes it: vp run knip:check, scoped formatting/lint, and both generated-file checks pass. Production code, test, and fixture are unchanged.

Models: Claude Opus 5.5 and GPT-6.1-Sol, through Claude Code and Codex in T3 Code.

juliusmarminge and others added 4 commits October 22, 2026 09:12
An existing thread persisted without options shows and sends the
New threads default effort, because deriveEffectiveComposerModelState
falls back to the project/settings default selection's options.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lt options

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Oct 22, 2026
@juliusmarminge juliusmarminge added bugfix Fix PR opened by an issue lead; enters the PR loop and removed vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Oct 22, 2026 — with GitCafe Production
@macroscopeapp

macroscopeapp Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs Human Approval

Macroscope's review found this PR requires human approval — Existing optionless threads will use provider defaults instead of inheriting project/settings options, changing their effective model configuration. Human review is required for this default-policy change and the broadened Knip fixture entry-point configuration.

No code changes detected at c0e3d7b. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@juliusmarminge juliusmarminge added the pr:gate-failed A land gate failed label Oct 22, 2026
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 5.0 KiB 5.0 KiB +6 B (+0.1%) 6.8 KiB ✅
Codex Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Codex Live turn WebSocket wire 1.2 KiB 1.3 KiB +6 B (+0.5%) 2.0 KiB ✅
Codex Live turn WebSocket decoded 21.6 KiB 21.6 KiB 0 B (0.0%) 24.4 KiB ✅
Codex Live turn messages 2 2 0 (0.0%) 8 ✅
Claude Total thread wire 5.0 KiB 5.1 KiB +6 B (+0.1%) 6.8 KiB ✅
Claude Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Claude Live turn WebSocket wire 1.3 KiB 1.3 KiB +6 B (+0.5%) 2.0 KiB ✅
Claude Live turn WebSocket decoded 21.9 KiB 21.9 KiB 0 B (0.0%) 24.4 KiB ✅
Claude Live turn messages 2 2 0 (0.0%) 8 ✅

Baseline: da8cc59 · PR result: c0e3d7b · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 430afd6 and c0e3d7b.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 7f004b1 and 430afd6.

Files selected for processing: 2
  • apps/web/src/composerDraftStore.ts
  • knip.jsonc

Limit details: You’ve used all 10 included reviews currently available.

Walkthrough

When a thread model selection exists, the composer uses only that selection’s options, even when they are absent. A test and fixture cover option resolution, and the scripts workspace includes fixture files as Knip entries.

Changes: Composer model option resolution

Layer / File(s) Summary
Thread option precedence
apps/web/src/composerDraftStore.ts, apps/web/src/modelSelection.test.ts, scripts/fixtures/composer-option-fallback.mjs, knip.jsonc
The resolver uses project options only when there is no thread model selection. The test checks that an optionless thread selection does not inherit project options. The fixture seeds threads with different option configurations, and Knip includes script fixtures as entry files.

Priority: Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Low

Possibly related issues

  • pingdotgg/t3code#23497 — Reports that an optionless thread selection incorrectly inherits project or settings options.

Possibly related PRs

  • pingdotgg/t3code#24619 — Also changes composer model-state resolution and how it handles missing thread options.
  • pingdotgg/t3code#23957 — Changes the source of composer model options in deriveEffectiveComposerModelState.
  • pingdotgg/t3code#23184 — Also preserves explicitly empty model-option selections in web composer state.

Suggested reviewers: t3dotgg, macroscopeapp

Merge Risk: Minimal · previous review 430af

No actionable merge-blocking risk was found in the reviewed changes.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: preserving provider defaults for threads without options.
Description check Passed The description includes complete Problem, Change, Scope and approval, and Verification sections. It explains the bug, fix, linked issue, test results, limitations, and supporting evidence.
Linked Issues check Passed [#23497] requires that a thread saved without options keep provider defaults, not project/settings options. At the reviewed head, deriveEffectiveComposerModelState in `apps/web/src/composerDraftStor…
Out of Scope Changes check Passed All changed code connects to [#23497]. The product change is limited to the option-fallback expression in composerDraftStore.ts. The regression test in modelSelection.test.ts verifies that express…
Full details: Linked Issues check

Explanation

[#23497] requires that a thread saved without options keep provider defaults, not project/settings options. At the reviewed head, deriveEffectiveComposerModelState in apps/web/src/composerDraftStore.ts changes the modelOptions fallback. Draft options still win. Next, the thread selection supplies options when it exists, even when it has none. Project options apply only when threadModelSelection is absent. This matches the issue's stated cause, where options fell back to project/settings defaults while the model resolved from the thread. apps/web/src/modelSelection.test.ts adds a regression case. It uses an optionless thread selection and a project selection with effort: high, and asserts state.modelOptions?.[instanceId] is undefined. Two caveats. The ?? null terminator was removed, so modelOptions can now be undefined rather than null when the thread has no options. The test accepts this, and the author reports web typecheck passed. Second, the send-path dispatch is not traced in this review. The author's browser evidence reports that effort is omitted from dispatch after the change. This review relies on that evidence and on the shared resolver. Coding requirements are met at the state-derivation level.

Full details: Out of Scope Changes check

Explanation

All changed code connects to [#23497]. The product change is limited to the option-fallback expression in composerDraftStore.ts. The regression test in modelSelection.test.ts verifies that expression. scripts/fixtures/composer-option-fallback.mjs is a manual synthetic fixture. It builds the optionless, explicit-low, and inherited thread states used for the bug's before/after proof. knip.jsonc registers scripts/fixtures/*.mjs as Knip entries so that the fixture is not reported as unused. These are verification support for the same fallback behavior, not unrelated product, wire, or persistence changes. The PR description reports no wire or persistence changes. The diffs reviewed here do not show any such change.

Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Oct 22, 2026
@juliusmarminge juliusmarminge added pr:ready Every gate passed; waits for Julius's land label and removed pr:gate-failed A land gate failed labels Oct 22, 2026

Copy link
Copy Markdown
Member Author

Closing in favour of #16546, which makes the same change.

@juliusmarminge juliusmarminge removed the pr:ready Every gate passed; waits for Julius's land label label Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Fix PR opened by an issue lead; enters the PR loop size:M 30-99 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Composer applies the settings default's options to a thread saved without options

1 participant