Surface the same reasoning label already shown in the composer StatusBar
in the top SessionHeader for codex/opencode sessions. Also show an
explicit Fast badge only when serviceTier is fast (#1004-aligned).
Closes#1015 (header display portion).
Co-authored-by: Cursor <cursoragent@cursor.com>
* test: reproduce issue #898 (Codex fast mode service tier)
* fix(codex): add Fast mode (service tier) toggle and /fast command (closes#898)
* feat(codex+web): Fast mode UI toggle with full persistence
Wires the Codex Fast mode (service tier) end-to-end so it can be toggled
from the web composer and survives reload/handoff:
- shared: serviceTier on Session/SessionPatch, session-alive payload,
resume target, and a SessionServiceTierRequest schema
- cli: AgentSessionBase carries serviceTier through keepAlive; runCodex
syncs it to the session instance
- hub: service_tier column (schema v10 + migration), store setter,
sessionCache + syncEngine plumbing, POST /sessions/:id/service-tier
- web: api.setServiceTier + mutation, a Fast/Standard toggle in the
composer settings (gated to Codex GPT-5.5/5.4), and StatusBar now
reflects the real tier instead of the effort heuristic
Refs #898
* fix(codex): preserve unset/persisted service tier on startup keepalive
Addresses HAPI Bot [Major] on PR #904: applyCurrentConfigToSession ran
setServiceTier(currentServiceTier ?? null) on wrapper-ready, collapsing the
untouched `undefined` state into explicit Standard. The immediate
setCollaborationMode keepalive then persisted serviceTier: null, silently
downgrading resumed Fast sessions and disabling account-default Fast.
- Seed currentServiceTier from the persisted session (sessionInfo.serviceTier),
so a resumed Fast thread keeps running Fast.
- Only call setServiceTier when the tier is explicit (!== undefined), preserving
the three-state omit semantics at the keepalive boundary.
- Add regression tests: persisted Fast is re-asserted; untouched omits the tier.
* feat(codex+web): gate Fast toggle on catalog-advertised service tier
The Fast toggle was gated on a model-name regex (gpt-5.5/5.4), which still
showed a no-op control to API-key users — Fast credits only apply with
ChatGPT login. Codex's model/list catalog advertises the service tiers
actually available for each model in the current auth/plan context, so gate
on that instead:
- cli: capture serviceTiers (ids) per model in ModelListItem + normalizeModel
- shared: CodexModelSummary.serviceTiers (flows through the existing
getSessionCodexModels pass-through; no hub change needed)
- web: codexModelAdvertisesFastTier(sessionModel, models) replaces the regex;
SessionChat gates the toggle on it (hidden while the catalog is
loading/errored). The toggle now only appears when toggling it will
actually take effect.
Refs #898
* fix(codex): make explicit Standard service tier sticky across resume
Addresses HAPI Bot [Major] (round 2): a single persisted null conflated
"untouched" with "explicit Standard". A user who turned Fast off persisted
null, but startup mapped null -> undefined (untouched) and omitted serviceTier,
so an account/thread-default Fast could silently return after restart/resume.
Introduce a distinct stored representation:
- 'fast' / 'standard' are explicit user choices; null/undefined = untouched.
- Translate 'standard' -> Codex app-server serviceTier: null ONLY when building
thread/turn params (toAppServerServiceTier); untouched omits the field.
- /fast off now stores 'standard'; the web Standard option sends 'standard'.
- Tighten SessionServiceTierRequest to enum(['fast','standard']) so stray tier
strings are never forwarded.
Tests: sticky-Standard-on-resume regression; turn/thread params translate
'standard'->null and omit on untouched; hub route applies fast/standard and
rejects unsupported values + local sessions.
Refs #898
* fix(codex): recognize real Fast tier (id 'priority', name 'Fast') in catalog gate
Live E2E against an authed Codex session revealed the model catalog advertises
the Fast tier with id 'priority' and display name 'Fast' (not id 'fast'), so the
/fast/i gate — which only saw tier ids — wrongly hid the toggle for valid
ChatGPT users on gpt-5.5/gpt-5.4. Capture both the tier id and name as
lowercased tokens so the existing name-based match recognizes 'Fast'. The sent
value stays 'fast' (the documented service_tier value / raw additionalSpeedTiers
request tier). Verified end-to-end: gpt-5.5/gpt-5.4 gate on, gpt-5.4-mini off.
Refs #898
* fix(codex): preserve service tier across session resume
Resuming a Codex session spawns a fresh session (serviceTier null) and merges
the old one in. Unlike model/effort/permissionMode, serviceTier was neither
threaded through the resume spawn nor preserved in mergeSessionData, so a
resumed Fast (or explicit Standard) session silently reverted to the account
default.
Thread serviceTier through the spawn path like its siblings:
- hub: resumeSession passes session.serviceTier to spawnSession; rpcGateway +
syncEngine carry it in the spawn RPC payload; mergeSessionData preserves it
old->new (safety net).
- cli: SpawnSessionOptions.serviceTier; apiMachine forwards it; buildCliArgs
emits --service-tier for codex; the codex command parses it; runCodex seeds
currentServiceTier from the spawn override first (opts.serviceTier ??
sessionInfo.serviceTier), so a resumed thread immediately runs the right tier.
Verified end-to-end: set Fast -> kill process -> reopen -> resumed session (new
id) still runs Fast. Tests: buildCliArgs --service-tier (codex only), runCodex
spawn-override seed, mergeSessionData service-tier preservation.
Refs #898
* fix(codex): send advertised 'priority' tier id for Fast, not 'fast'
The model catalog advertises the Fast tier with request id 'priority' (display
name 'Fast'), and OpenAI docs confirm service_tier='fast' maps to the request
value 'priority'. The app-server serviceTier override is a raw request value
that does not validate unknown strings (a live probe accepted 'bogus-xyz'), so
sending 'fast' risks being silently ignored — no Fast applied.
Translate the stored 'fast' state to app-server 'priority' at the thread/turn
param boundary (toAppServerServiceTier); the stored/UI/command representation
stays 'fast'/'standard'. Verified live: a turn with serviceTier='priority' runs
and consumes the Fast-tier rate budget.
Addresses HAPI Bot [Major]. Refs #898
* fix(codex): validate --service-tier CLI value (fast|standard)
Addresses HAPI Bot [Minor]: the internal --service-tier spawn arg accepted any
non-empty string, unlike the web /service-tier enum, so a malformed value could
be seeded into currentServiceTier and persisted via keepalive. Parse it to
'fast'|'standard' and reject anything else, matching the web endpoint.
Refs #898
* feat(cli,web,hub): migrate Cursor remote sessions to ACP with model/effort pickers
Move stream-json remote launcher to legacy path and add ACP launcher with
set_config_option model/mode sync, optimistic keepalive on config changes, and
shared catalog caching. Web gets dual base/effort Cursor pickers for session and
new-session flows; hide composer status bar when Cursor sends no usage_update.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli,web,shared): Cursor model picker — ACP wires + CLI sku variants
Enrich the web/mobile picker with agent --list-models SKUs grouped under
ACP wire bases, fix session-open base highlight, and keep catalog discovery
safe while the ACP transport holds the CLI lock.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cursor-acp): apply ACP default model when web resets to Default
Web sends model: null for Default; push session/set_config_option with the
ACP default[] wire so Cursor backend matches hub state. Regression tests
for setModel(null) and applyModelConfig(null).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(acp): clear stale agent-acp lock when owning process is gone
Check lock pid with signal 0; remove orphaned lock dirs after SIGKILL or
crash so listCursorModels can run cold probes again. Regression tests for
guard and catalog discovery.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(cursor): use live pid for ACP lock handler tests
Stale-lock cleanup clears dead pids; handler tests must simulate an
active lock with the current process pid to avoid cold probes/timeouts.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(acp): scope agent CLI lock guard to Cursor agent command only
Gemini/OpenCode/Kimi ACP sessions must not register agent-acp-active;
that blocked listCursorModels while unrelated backends were running.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hub,web): reject Cursor model changes for local sessions
Hub returns 409 when controlledByUser is set, matching Codex. Web hides
model and variant pickers for local Cursor sessions so users do not hit
a dead RPC path. Document pre-push-review in AGENTS.md.
Verified: bun typecheck; bun run test (919 cli + 243 hub + 768 web + 46 shared).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): send stable ids for Cursor ask_question replies
Parse and submit question.id and option.id so ACP receives keys like
{ approach: ['a'] } instead of index/label. Verified: bun typecheck && bun run test.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(opencode): support plan mode
* feat(opencode): support reasoning effort
* feat(opencode): surface context usage in web
Bridge OpenCode ACP usage updates into the existing token-count pipeline so the web status bar can show live context and cache information without a separate UI path.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(opencode): restrict plan mode to remote, rollback reasoning effort on failure
- Block local OpenCode plan startup (tools not enforced in local path)
- Allow remote OpenCode plan only (ACP permission handler denies tools)
- Guard web /permission-mode endpoint for local OpenCode plan sessions
- Rollback session reasoning effort when OpenCode rejects set_config_option
- Wire rollback callback through opencodeLoop to runOpencode closure
- Add tests: local plan rejected, remote plan allowed, web guard, effort rollback
* fix(web): auto-retry OpenCode models query to populate model selector without refresh
- Retry early failures (RPC may still be registering on new sessions)
- Poll briefly until availableModels is non-empty
- Stop polling once model options are discovered
- Add tests for retry/poll/stop policy
* fix(opencode): cap model discovery polling
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(codex): add web event rendering harness
* fix(codex): surface plan updates in web
* fix(codex): render MCP tool calls in web
* fix(codex): improve terminal and context display
* fix(codex): format token usage events
* fix(codex): show status context in web
* fix(codex): preserve tool result errors
Extract permission mode display logic into shared utilities for better reusability and maintainability. Add PermissionModeTone type and related helpers to centralize mode-based styling rules across components. Update components to use new PermissionModeOption type for consistent permission mode presentation.
Implements bidirectional sync of permission/model modes between CLI sessions and web app. Adds Codex-specific permission modes (read-only, safe-yolo, yolo) alongside Claude's modes. Web can now control CLI session state via RPC set-session-config handler, while CLI broadcasts state changes through keep-alive payloads. UI controls are flavor-aware, showing appropriate modes for Claude vs Codex vs Gemini. Type centralization in api/types eliminates circular dependencies.