* 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(hub,cli): forward permissionMode on session resume
When a session is resumed, the cached permissionMode is now forwarded
through the Hub → Runner → CLI pipeline via a new --permission-mode
flag. Previously the mode was lost on resume, resetting to 'default'.
Each CLI flavor validates the flag value against its own allowed
permission modes (e.g. CLAUDE_PERMISSION_MODES) and rejects unknown
values. The existing --yolo flag is preserved as a shorthand.
* refactor(cli): extract buildCliArgs from startRunner
Extract the CLI argument construction logic into a standalone
exported function so it can be unit-tested independently.
No behavior change.
* test(cli): add buildCliArgs unit tests for --permission-mode
Verify that the runner correctly forwards valid permission modes
via --permission-mode, rejects invalid values, and falls back to
--yolo when no permission mode is set.
* fix(cli): let --permission-mode take precedence over --yolo
When both flags are present, --permission-mode was silently
overwritten by a later --yolo. Guard legacy flag branches with
a hasExplicitPermissionMode check so the explicit flag wins.
Extract permission and model mode validation into reusable functions utilizing
@hapi/protocol schemas. Use isPermissionModeAllowedForFlavor and
isModelModeAllowedForFlavor to validate modes based on session flavor (claude/codex).
Replace inline type definitions with shared types from @hapi/protocol.
Restructure CLI entry point and RPC handler registration to improve maintainability:
- Introduce command registry pattern with CommandDefinition interface for consistent command structure
- Extract individual command modules (claude, codex, daemon, doctor, gemini, auth, connect, etc.)
- Move RPC handlers into separate feature modules (bash, files, git, directories, ripgrep, difftastic, slashCommands)
- Create shared RPC types and response helpers (rpcTypes.ts, rpcResponses.ts)
- Move SpawnSessionOptions and SpawnSessionResult to rpcTypes for proper type organization
- Simplify index.ts to minimal entrypoint using command registry
- Update imports in apiMachine.ts and daemon/run.ts to use rpcTypes module
This enables:
- Better code organization by feature/command
- Easier testing of individual commands
- Safer RPC handler registration without side effects
- Clearer separation of concerns between routing and business logic