Commit Graph

12 Commits

  • Option to Notify Workspace Owner When Usage Limit is Reached (#16969)
    ## Summary
    - Replace the manual `/notify-owner` flow with an inline confirmation
    prompt when a usage-based workspace member hits a credits-depleted
    limit.
    - Fetch the current workspace role from the live ChatGPT
    `accounts/check/v4-2023-04-27` endpoint so owner/member behavior matches
    the desktop and web clients.
    - Keep owner, member, and spend-cap messaging distinct so we only offer
    the owner nudge when the workspace is actually out of credits.
    
    ## What Changed
    - `backend-client`
    - Added a typed fetch for the current account role from
    `accounts/check`.
      - Mapped backend role values into a Rust workspace-role enum.
    - `app-server` and protocol
      - Added `workspaceRole` to `account/read` and `account/updated`.
    - Derived `isWorkspaceOwner` from the live role, with a fallback to the
    cached token claim when the role fetch is unavailable.
    - `tui`
      - Removed the explicit `/notify-owner` slash command.
    - When a member is blocked because the workspace is out of credits, the
    error now prompts:
    - `Your workspace is out of credits. Request more from your workspace
    owner? [y/N]`
      - Choosing `y` sends the existing owner-notification request.
    - Choosing `n`, pressing `Esc`, or accepting the default selection
    dismisses the prompt without sending anything.
    - Selection popups now honor explicit item shortcuts, which is how the
    `y` / `n` interaction is wired.
    
    ## Reviewer Notes
    - The main behavior change is scoped to usage-based workspace members
    whose workspace credits are depleted.
    - Spend-cap reached should not show the owner-notification prompt.
    - Owners and admins should continue to see `/usage` guidance instead of
    the member prompt.
    - The live role fetch is best-effort; if it fails, we fall back to the
    existing token-derived ownership signal.
    
    ## Testing
    - Manual verification
      - Workspace owner does not see the member prompt.
    - Workspace member with depleted credits sees the confirmation prompt
    and can send the nudge with `y`.
    - Workspace member with spend cap reached does not see the
    owner-notification prompt.
    
    ### Workspace member out of usage
    
    https://github.com/user-attachments/assets/341ac396-eff4-4a7f-bf0c-60660becbea1
    
    ### Workspace owner
    <img width="1728" height="1086" alt="Screenshot 2026-04-09 at 11 48
    22 AM"
    src="https://github.com/user-attachments/assets/06262a45-e3fc-4cc4-8326-1cbedad46ed6"
    />
  • Forward app-server turn clientMetadata to Responses (#16009)
    ## Summary
    App-server v2 already receives turn-scoped `clientMetadata`, but the
    Rust app-server was dropping it before the outbound Responses request.
    This change keeps the fix lightweight by threading that metadata through
    the existing turn-metadata path rather than inventing a new transport.
    
    ## What we're trying to do and why
    We want turn-scoped metadata from the app-server protocol layer,
    especially fields like Hermes/GAAS run IDs, to survive all the way to
    the actual Responses API request so it is visible in downstream
    websocket request logging and analytics.
    
    The specific bug was:
    - app-server protocol uses camelCase `clientMetadata`
    - Responses transport already has an existing turn metadata carrier:
    `x-codex-turn-metadata`
    - websocket transport already rewrites that header into
    `request.request_body.client_metadata["x-codex-turn-metadata"]`
    - but the Rust app-server never parsed or stored `clientMetadata`, so
    nothing from the app-server request was making it into that existing
    path
    
    This PR fixes that without adding a new header or a second metadata
    channel.
    
    ## How we did it
    ### Protocol surface
    - Add optional `clientMetadata` to v2 `TurnStartParams` and
    `TurnSteerParams`
    - Regenerate the JSON schema / TypeScript fixtures
    - Update app-server docs to describe the field and its behavior
    
    ### Runtime plumbing
    - Add a dedicated core op for app-server user input carrying turn-scoped
    metadata: `Op::UserInputWithClientMetadata`
    - Wire `turn/start` and `turn/steer` through that op / signature path
    instead of dropping the metadata at the message-processor boundary
    - Store the metadata in `TurnMetadataState`
    
    ### Transport behavior
    - Reuse the existing serialized `x-codex-turn-metadata` payload
    - Merge the new app-server `clientMetadata` into that JSON additively
    - Do **not** replace built-in reserved fields already present in the
    turn metadata payload
    - Keep websocket behavior unchanged at the outer shape level: it still
    sends only `client_metadata["x-codex-turn-metadata"]`, but that JSON
    string now contains the merged fields
    - Keep HTTP fallback behavior unchanged except that the existing
    `x-codex-turn-metadata` header now includes the merged fields too
    
    ### Request shape before / after
    Before, a websocket `response.create` looked like:
    ```json
    {
      "type": "response.create",
      "client_metadata": {
        "x-codex-turn-metadata": "{\"session_id\":\"...\",\"turn_id\":\"...\"}"
      }
    }
    ```
    Even if the app-server caller supplied `clientMetadata`, it was not
    represented there.
    
    After, the same request shape is preserved, but the serialized payload
    now includes the new turn-scoped fields:
    ```json
    {
      "type": "response.create",
      "client_metadata": {
        "x-codex-turn-metadata": "{\"session_id\":\"...\",\"turn_id\":\"...\",\"fiber_run_id\":\"fiber-start-123\",\"origin\":\"gaas\"}"
      }
    }
    ```
    
    ## Validation
    ### Targeted tests added / updated
    - protocol round-trip coverage for `clientMetadata` on `turn/start` and
    `turn/steer`
    - protocol round-trip coverage for `Op::UserInputWithClientMetadata`
    - `TurnMetadataState` merge test proving client metadata is added
    without overwriting reserved built-in fields
    - websocket request-shape test proving outbound `response.create`
    contains merged metadata inside
    `client_metadata["x-codex-turn-metadata"]`
    - app-server integration tests proving:
    - `turn/start` forwards `clientMetadata` into the outbound Responses
    request path
      - websocket warmup + real turn request both behave correctly
      - `turn/steer` updates the follow-up request metadata
    
    ### Commands run
    - `just write-app-server-schema`
    - `cargo test -p codex-app-server-protocol`
    - `cargo test -p codex-protocol`
    - `cargo test -p codex-core
    turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields
    --lib`
    - `cargo test -p codex-core --test all
    responses_websocket_preserves_custom_turn_metadata_fields`
    - `cargo test -p codex-app-server --test all client_metadata`
    - `cargo test -p codex-app-server --test all
    turn_start_forwards_client_metadata_to_responses_websocket_request_body_v2
    -- --nocapture`
    - `just fmt`
    - `just fix -p codex-core -p codex-protocol -p codex-app-server-protocol
    -p codex-app-server`
    - `just fix -p codex-exec -p codex-tui-app-server`
    - `just argument-comment-lint`
    
    ### Full suite note
    `cargo test` in `codex-rs` still fails in:
    -
    `suite::v2::turn_interrupt::turn_interrupt_resolves_pending_command_approval_request`
    
    I verified that same failure on a clean detached `HEAD` worktree with an
    isolated `CARGO_TARGET_DIR`, so it is not caused by this patch.
  • Add realtime voice selection (#17176)
    - Add realtime voice selection for realtime/start.
    - Expose the supported v1/v2 voice lists and cover explicit, configured,
    default, and invalid voice paths.
  • fix(tui): reduce startup and new-session latency (#17039)
    ## TL;DR
    
    - Fetches account/rateLimits/read asynchronously so the TUI can continue
    starting without waiting for the rate-limit response.
    - Fixes the /status card so it no longer leaves a stale “refreshing
    cached limits...” notice in terminal history.
    
    ## Problem
    
    The TUI bootstrap path fetched account rate limits synchronously
    (`account/rateLimits/read`) before the event loop started for
    ChatGPT/OpenAI-authenticated startups. This added ~670 ms of blocking
    latency in the measured hot-start case, even though rate-limit data is
    not needed to render the initial UI or accept user input. The delay was
    especially noticeable on hot starts where every other RPC
    (`account/read`, `model/list`, `thread/start`) completed in under 70 ms
    total.
    
    Moving that fetch to the background also exposed a `/status` UI bug: the
    status card is flattened into terminal scrollback when it is inserted. A
    transient "refreshing limits in background..." line could not be cleared
    later, because the async completion updated the retained `HistoryCell`,
    not the already-written terminal history.
    
    ## Mental model
    
    Before this change, `AppServerSession::bootstrap()` performed three
    sequential RPCs: `account/read` → `model/list` →
    `account/rateLimits/read`. The result of the third call was baked into
    `AppServerBootstrap` and applied to the chat widget before the event
    loop began.
    
    After this change, `bootstrap()` only performs two RPCs (`account/read`
    + `model/list`), and rate-limit fetching is kicked off as an async
    background task immediately after the first frame is scheduled. A new
    enum, `RateLimitRefreshOrigin`, tags each fetch so the event handler
    knows whether the result came from the startup prefetch or from a
    user-initiated `/status` command; they have different completion
    side-effects.
    
    The `get_login_status()` helper (used outside the main app flow) was
    also decoupled: it previously called the full `bootstrap()` just to
    check auth mode, wasting model-list and rate-limit work. It now calls
    the narrower `read_account()` directly.
    
    For `/status`, this PR keeps the background refresh request but stops
    printing transient refresh notices into status history when cached
    limits are already available. If a refresh updates the cache, the next
    `/status` command will render the new values.
    
    ## Non-goals
    
    - This change does not alter the rate-limit data itself.
    - This change does not introduce caching, retries, or staleness
    management for rate limits.
    - This change does not affect the `model/list` or `thread/start` RPCs;
    they remain on the critical startup path.
    
    ## Tradeoffs
    
    - **Stale-on-first-render**: The status bar will briefly show no
    rate-limit info until the background fetch completes; observed
    background fetches landed roughly in the 400-900 ms range after the UI
    appeared. This is acceptable because the user cannot meaningfully act on
    rate-limit data in the first fraction of a second.
    - **Error silence on startup prefetch**: If the startup prefetch fails,
    the error is logged but the UI is not notified (unlike `/status` refresh
    failures, which go through the status-command completion path). This
    avoids surfacing transient network errors as a startup blocker.
    - **Static `/status` history**: `/status` output is terminal history,
    not a live widget. The card now avoids progress-style language that
    would appear stuck in scrollback; users can run `/status` again to see
    newly cached values.
    - **`account_auth_mode` field removed from `AppServerBootstrap`**: The
    only consumer was `get_login_status()`, which no longer goes through
    `bootstrap()`. The field was dead weight.
    
    ## Architecture
    
    ### New types
    
    - `RateLimitRefreshOrigin` (in `app_event.rs`): A `Copy` enum
    distinguishing `StartupPrefetch` from `StatusCommand { request_id }`.
    Carried through `RefreshRateLimits` and `RateLimitsLoaded` events so the
    handler applies the right completion behavior.
    
    ### Modified types
    
    - `AppServerBootstrap`: Lost `account_auth_mode` and
    `rate_limit_snapshots`; gained `requires_openai_auth: bool` (passed
    through from the account response so the caller can decide whether to
    fire the prefetch).
    
    ### Control flow
    
    1. `bootstrap()` returns with `requires_openai_auth` and
    `has_chatgpt_account`.
    2. After scheduling the first frame, `App::run_inner` fires
    `refresh_rate_limits(StartupPrefetch)` if both flags are true.
    3. When `RateLimitsLoaded { StartupPrefetch, Ok(..) }` arrives,
    snapshots are applied and a frame is scheduled to repaint the status
    bar.
    4. When `RateLimitsLoaded { StartupPrefetch, Err(..) }` arrives, the
    error is logged and no UI update occurs.
    5. `/status`-initiated refreshes continue to use `StatusCommand {
    request_id }` and call `finish_status_rate_limit_refresh` on completion
    (success or failure).
    6. `/status` history cells with cached rate-limit rows no longer render
    an additional "refreshing limits" notice; the async refresh updates the
    cache for future status output.
    
    ### Extracted method
    
    - `AppServerSession::read_account()`: Factored out of `bootstrap()` so
    that `get_login_status()` can call it independently without triggering
    model-list or rate-limit work.
    
    ## Observability
    
    - The existing `tracing::warn!` for rate-limit fetch failures is
    preserved for the startup path.
    - No new metrics or spans are introduced. The startup-time improvement
    is observable via the existing `ready` timestamp in TUI startup logs.
    
    ## Tests
    
    - Existing tests in `status_command_tests.rs` are updated to match on
    `RateLimitRefreshOrigin::StatusCommand { request_id }` instead of a bare
    `request_id`.
    - Focused `/status` tests now assert that status history avoids
    transient refresh text, continues to request an async refresh, and uses
    refreshed cached limits in future status output.
    - No new tests are added for the startup prefetch path because it is a
    fire-and-forget spawn with no observable side-effect other than the
    widget state update, which is already covered by the
    snapshot-application tests.
    
    ---------
    
    Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  • Use model metadata for Fast Mode status (#16949)
    Fast Mode status was still tied to one model name in the TUI and
    model-list plumbing. This changes the model metadata shape so a model
    can advertise additional speed tiers, carries that field through the
    app-server model list, and uses it to decide when to show Fast Mode
    status.
    
    For people using Codex, the behavior is intended to stay the same for
    existing models. Fast Mode still requires the existing signed-in /
    feature-gated path; the difference is that the UI can now recognize any
    model the model list marks as Fast-capable, instead of requiring a new
    client-side slug check.
  • Add WebRTC transport to realtime start (#16960)
    Adds WebRTC startup to the experimental app-server
    `thread/realtime/start` method with an optional transport enum. The
    websocket path remains the default; WebRTC offers create the realtime
    session through the shared start flow and emit the answer SDP via
    `thread/realtime/sdp`.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • [codex] reduce module visibility (#16978)
    ## Summary
    - reduce public module visibility across Rust crates, preferring private
    or crate-private modules with explicit crate-root public exports
    - update external call sites and tests to use the intended public crate
    APIs instead of reaching through module trees
    - add the module visibility guideline to AGENTS.md
    
    ## Validation
    - `cargo check --workspace --all-targets --message-format=short` passed
    before the final fix/format pass
    - `just fix` completed successfully
    - `just fmt` completed successfully
    - `git diff --check` passed
  • [codex-analytics] add protocol-native turn timestamps (#16638)
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/16638).
    * #16870
    * #16706
    * #16659
    * #16641
    * #16640
    * __->__ #16638
  • Add remote --cd forwarding for app-server sessions (#16700)
    Addresses #16124
    
    Problem: `codex --remote --cd <path>` canonicalized the path locally and
    then omitted it from remote thread lifecycle requests, so remote-only
    working directories failed or were ignored.
    
    Solution: Keep remote startup on the local cwd, forward explicit `--cd`
    values verbatim to `thread/start`, `thread/resume`, and `thread/fork`,
    and cover the behavior with `codex-tui` tests.
    
    Testing: I manually tested `--remote --cd` with both absolute and
    relative paths and validated correct behavior.
    
    
    ---
    
    Update based on code review feedback:
    
    Problem: Remote `--cd` was forwarded to `thread/resume` and
    `thread/fork`, but not to `thread/list` lookups, so `--resume --last`
    and picker flows could select a session from the wrong cwd; relative cwd
    filters also failed against stored absolute paths.
    
    Solution: Apply explicit remote `--cd` to `thread/list` lookups for
    `--last` and picker flows, normalize relative cwd filters on the
    app-server before exact matching, and document/test the behavior.
  • Fix fork source display in /status (expose forked_from_id in app server) (#16596)
    Addresses #16560
    
    Problem: `/status` stopped showing the source thread id in forked TUI
    sessions after the app-server migration.
    
    Solution: Carry fork source ids through app-server v2 thread data and
    the TUI session adapter, and update TUI fixtures so `/status` matches
    the old TUI behavior.
  • Fix stale /status rate limits in active TUI sessions (#16201)
    Fix stale weekly limit in `/status` (#16194): /status reused the
    session’s cached rate-limit snapshot, so the weekly remaining limit
    could stay frozen within an active session.
    
    With this change, we now dynamically update the rate limits after status
    is displayed.
    
    I needed to delete a few low-value test cases from the chatWidget tests
    because the test.rs file is really large, and the new tests in this PR
    pushed us over the 512K mandated limit. I'm working on a separate PR to
    refactor that test file.
  • Rename tui_app_server to tui (#16104)
    This is a follow-up to https://github.com/openai/codex/pull/15922. That
    previous PR deleted the old `tui` directory and left the new
    `tui_app_server` directory in place. This PR renames `tui_app_server` to
    `tui` and fixes up all references.