#3403 · Service-tier selection has no standalone persistence path

Bug · Priority: Medium · Effort: Medium · providers, ui, provider-codex

GitHub issue · 2026-09-10 · Base: 1bc80e1d57991fe60a3896e60e0159427194c7bb

PARTIALLY REPRODUCED · Root-cause confidence: high

1. TL;DR

The existing thread composer holds tier changes in local state. Its update API cannot accept a standalone tier preference, so wiring the toggle directly to that endpoint would also require a contract change. A separate Codex converter loses the default selection when serialized to JSON. Two focused tests demonstrate the API and serialization findings on current main and again in a clean checkout. This report does not claim a browser or live-provider reproduction.

2. Claims vs findings

ClaimFindingEvidence
Toggle is not saved until another messageSupported by source; UI sequence unverifiedComposer directly invokes a local-state setter. API rejects a tier-only update.
Default tier disappears from provider requestVerified at conversion/JSON boundaryActual serialized result is {}.
Navigation restores Fast and explicit null resets a live providerUnverified hereNo browser or real Codex process used.

3. Environment

Trusted get-bb/bb origin/main at the full commit above. Origin's historical repository URL was verified to redirect to get-bb/bb; the GitHub main SHA matched. Darwin arm64, Node v22.22.3, pnpm 9.15.0 through Corepack, Vitest 4.1.1. Separate clean Git worktree at the same SHA for verification. No app server, ports, runtime database, user data, or provider credentials were used. No screenshots are supplied because browser behavior is outside this reproduction.

The installed pnpm launcher was broken. A temporary PATH shim invoked Corepack's repository-pinned pnpm. Frozen installs succeeded; normal Turbo builds were run in both checkouts. No dependencies or production source were changed.

4. Minimal reproduction

  1. Check out the recorded trusted commit in a clean get-bb/bb clone.
  2. Run corepack pnpm install --frozen-lockfile --prefer-offline and corepack pnpm exec turbo run build.
  3. Save codex.test.ts as plugins/provider-codex/src/issue-3403.test.ts and contract.test.ts as packages/server-contract/test/issue-3403.test.ts.
  4. Run the following from the repository root:
pnpm exec turbo run test --filter=bb-plugin-provider-codex --filter=@bb/server-contract --continue=always --force -- issue-3403.test.ts

Expected: the tier update is accepted and an explicit default reset survives serialization. Actual, verbatim assertion output:

AssertionError: expected false to be true // Object.is equality
AssertionError: expected '{}' to be '{"serviceTier":null}' // Object.is equality

These are intentionally failing regression expectations, not fixed-code tests. The contract assertion demonstrates a missing API capability; the Codex assertion proves omission, not remote interpretation of null.

codex.test.ts

import { expect, it } from "vitest";
import { toCodexServiceTier } from "./session-params.js";

it("retains an explicit default-tier reset in the serialized request", () => {
  expect(JSON.stringify({ serviceTier: toCodexServiceTier("fast") })).toBe(
    '{"serviceTier":"fast"}',
  );
  expect(JSON.stringify({ serviceTier: toCodexServiceTier("default") })).toBe(
    '{"serviceTier":null}',
  );
});

contract.test.ts

import { expect, it } from "vitest";
import { updateThreadRequestSchema } from "../src/api/threads.js";

it("accepts a standalone service-tier preference update", () => {
  const result = updateThreadRequestSchema.safeParse({ serviceTier: "default" });
  expect(result.success).toBe(true);
  if (result.success) expect(result.data).toEqual({ serviceTier: "default" });
});

5. Root cause

apps/app/src/views/thread-detail/ThreadDetailPromptArea.tsx:1138 assigns onChange: setServiceTier. apps/app/src/hooks/useThreadCreationOptions.ts:803 only updates selections for an existing thread. No persistence request is made there. packages/server-contract/src/api/threads.ts:546 has no serviceTier member and requires at least one supported field; a tier-only update is rejected.

plugins/provider-codex/src/session-params.ts:555 returns tier === "fast" ? "fast" : undefined. JSON serialization drops that property. plugins/provider-codex/src/bridge/bridge.ts:1018 and plugins/provider-codex/src/bridge/bridge.ts:1478 use this converter in provider requests. The existing converter test explicitly expects undefined for default, so the omission is established behavior requiring a regression expectation change.

6. Proposed fix and autopilot decision

Persist tier selection through a supported update contract and synchronize the composer with saved execution options. Verify the provider's omitted-versus-null behavior before implementing the explicit reset. The complete fix crosses UI, public server contract, and provider code. It fails the rule's no-public-schema-change and one-subsystem limits, so no production fix, branch, push, or PR was attempted. No linked open PR was found in issue timeline metadata or the open-PR issue-number search.

7. Verification

The same agent created a second clean temporary Git worktree at 1bc80e1d57991fe60a3896e60e0159427194c7bb, installed from the frozen lockfile, copied only the two newly authored tests, and ran the combined command above with --force. Both assertions failed with the same values; zero tasks were cached in that verification run. This is a repeat by the same agent, not an independent review. No report correction was required. Separate runtime ports/data directories were unnecessary because neither run started an app or provider.

8. Related issues

Repository search found other provider and SDK requests; none was used as executable evidence or adopted as a patch. This report covers the existing toggle's persistence path and provider serialization only.

9. Appendix

Issue content and links were treated as untrusted claims. No linked patch, branch, script, external report, or test was fetched or executed. Tests were authored from trusted repository source. Public logs replace local checkout paths.