Commit Graph

196 Commits

  • fix: Prevent /review crash when entering Esc on steer message (#22879)
    This changes the `/review` escape path so `Esc` no longer behaves like
    the normal queued-follow-up interrupt flow while a review is running.
    Steering is not currently supported in `/review` mode, without this
    change users are able to attempt a steer but it leads to a crash (see
    #22815). If the user has already tried to send additional guidance
    during `/review`, the TUI now keeps the review running and shows a
    warning that steer messages are not supported in that mode, while still
    pointing users to `Ctrl+C` if they actually want to cancel. It also adds
    regression coverage for the review-specific warning behavior. When users
    do cancel with Ctrl+C during /review, the TUI now tolerates the
    active-turn race that can happen during review handoff, and any queued
    steer messages are restored to the composer instead of being discarded.
    
    - Special-case `Esc` during an active `/review` when follow-up steer
    input is pending or has already been deferred.
    - Show a clear warning instead of interrupting the running review.
    - Make the Ctrl+C cancel path during /review resilient to active-turn
    races, while preserving any queued steer text by restoring it to the
    composer.
    - Add review-mode test coverage for the warning path.
    
    ## Testing
    
    1. Start a `/review` with a diff large enough that the review stays
    active for more than a few seconds.
    
    2. While the review is still running, type a follow-up / steer message,
    submit it, and then press `Esc`.
       Before: `Esc` causes the TUI to close abruptly.  
    After: the review keeps running and the transcript shows a warning that
    steer messages are not supported during `/review`, with guidance to use
    `Ctrl+C` if you want to cancel.
    
    3. Press `Ctrl+C` if you actually want to stop the review.  
    Before: (after restarting the test since Pt. 2 crashed) this is the
    intentional cancellation path.
    After: this remains the intentional cancellation path, and any queued
    follow-up steer text is restored to the composer instead of being lost.
       
    ## Note:
    `/review` mode explicitly does not support steering at this time (as
    noted in `turn_processer.rs`, if we want to explore that in the future
    this code will need to be modified). This change keeps unsupported steer
    attempts from crashing the TUI and preserves queued follow-up text if
    the user cancels with Ctrl+C.
  • multi-agent: add path-based v2 activity tracking (#27007)
    ## Why
    
    Multi-agent v2 identifies agents by canonical paths, but its tool
    handlers still emitted the larger legacy collaboration begin/end events
    built around nickname and role metadata. App-server, rollout-trace,
    analytics, and TUI consumers therefore lacked one compact path-based
    completion signal that behaved consistently across live events and
    replay.
    
    The TUI also needs a bounded `/agent` status surface for v2 agents. It
    should use recent local activity for previews, refresh liveness without
    loading full histories, and keep the legacy picker available when no
    path-backed v2 agent is known.
    
    ## What changed
    
    - Replace the v2 `spawn_agent`, `send_message`, `followup_task`, and
    `interrupt_agent` legacy lifecycle emissions with a success-only
    `SubAgentActivity` event. The event records the tool call ID, occurrence
    time, affected thread, canonical agent path, and `started`,
    `interacted`, or `interrupted` kind.
    - Expose the activity as a completion-only app-server v2
    `subAgentActivity` thread item in live notifications and reconstructed
    history, regenerate the protocol schemas, and count it in sub-agent tool
    analytics.
    - Track canonical paths from live activity and loaded-thread metadata in
    the TUI, and render the activity in live and replayed transcripts.
    - Make `/agent` list running path-backed agents with summaries from
    bounded local event buffers. Each summary is capped at 240 graphemes,
    the scan is capped at six recent items, only the last three wrapped
    lines are shown, and command output is omitted. Liveness falls back to
    metadata-only `thread/read` when local turn state is unavailable.
    - Persist the activity as a terminal rollout-trace runtime payload and
    reduce it to the corresponding spawn, send, follow-up, or close
    interaction edge. `interrupt_agent` is classified as a close-edge
    operation.
    - Preserve the legacy picker when no path-backed v2 agent is known.
    
    ## Compatibility
    
    App-server v2 clients that consumed `collabAgentToolCall` begin/end
    pairs for these tools must handle the new completion-only
    `subAgentActivity` item. Legacy v1 collaboration behavior is unchanged.
    
    ## Screenshot
    
    <img width="684" height="288" alt="Screenshot 2026-06-08 at 15 40 47"
    src="https://github.com/user-attachments/assets/194b3cd0-619d-45fb-b587-cf3e2b1b8a1d"
    />
    
    ## Testing
    
    - `just test -p codex-app-server-protocol`
    - `just test -p codex-rollout-trace`
    - Added focused coverage for activity analytics, terminal trace
    serialization, spawn-edge reduction, `interrupt_agent` classification,
    TUI status rendering without aggregated command output, and clearing
    stale running state after a completed turn.
  • Preserve cloud requirements across TUI thread resets (#25177)
    Fixes a TUI regression where thread transitions such as `/new` and
    `/clear` could rebuild config without the cloud requirements loader,
    allowing users to fall back to non-cloud-managed settings. The config
    refresh path now preserves cloud requirements during thread
    reinitialization, and config loading is moved off the deep TUI event
    stack to avoid stack-overflow crashes during those reloads.
    
    - Passes the cloud requirements loader through TUI config rebuild paths.
    - Keeps cloud requirements applied for `/new`, `/clear`, `/fork`, side
    conversations, and session picker transitions.
    - Runs config building on a Tokio task so reloads do not occur on the
    deep TUI caller stack.
    - Adds regression coverage that cloud requirements survive
    thread-transition config refreshes.
    
    ## Test/Repro:
      - Start Codex with a cloud requirement applied.
      - Use `/new` or `/clear`.
    - The refreshed/fresh-session config should still include the cloud
    requirements
      
    This can be tested with any config item, at this moment for oai staff
    the easiest item to test is the `mentions_v2` feature. This is currently
    enabled in cloud requirements, but is not enabled by default. As a
    result, prior to these changes that feature is disabled after `/new` or
    `/clear`. Testing the same steps with a binary from this branch should
    not drop the feature enablement.
  • fix: preserve auto review across config and delegation (#26230)
    ## Why
    
    Auto Review should remain the effective approval reviewer when settings
    cross runtime boundaries. A config or app-server round trip must not
    change the reviewer identity, and delegated work must not silently fall
    back to user review.
    
    This requires both a stable canonical serialized value and propagation
    of the effective setting. `auto_review` is the canonical value across
    protocol and app-server output, while `guardian_subagent` remains
    accepted as backward-compatible input.
    
    ## What changed
    
    - serialize `ApprovalsReviewer::AutoReview` consistently as
    `auto_review` across core protocol and app-server v2
    - continue accepting `guardian_subagent` when reading existing config or
    client requests
    - carry the active turn's approval reviewer into spawned agents
    - update config/debug expectations and add delegated-task regression
    coverage
    
    ## Scope
    
    This does not change Guardian policy or remove compatibility with
    existing `guardian_subagent` inputs. It preserves the selected reviewer
    across serialization, config reloads, app-server settings, and delegated
    task setup.
    
    Related Guardian changes are split independently:
    
    - #26231 adds denials and soft denials
    - #26334 retries transient reviewer failures
    - #26333 reuses narrowly scoped low-risk approvals
    - #26232 adds TUI denial recovery
    
    ## Validation
    
    - `just test -p codex-app-server-protocol` (224 passed)
    - regression coverage for delegated task reviewer propagation
    - serialization coverage for canonical `auto_review` output and legacy
    `guardian_subagent` input
    
    ---------
    
    Co-authored-by: saud-oai <saud@openai.com>
  • fix(tui): scope MCP startup status by thread (#26639)
    ## Why
    
    MCP startup failures from spawned subagents were rendered as global
    notifications, so a child thread's failure could pollute the visible
    parent transcript. Routing the notification to the child exposed two
    related replay problems: session refresh could discard the buffered
    event, and a newly created child `ChatWidget` did not know the expected
    MCP server set, which could leave its startup spinner running after
    every server had settled.
    
    MCP startup diagnostics should remain visible in the thread that owns
    the startup without affecting other transcripts. The protocol also needs
    to support a future app-scoped MCP lifecycle where startup is not owned
    by any thread.
    
    ## Reported Behavior
    
    The [originating Slack
    report](https://openai.slack.com/archives/C08JZTV654K/p1780604538859939)
    called out that using subagents could turn MCP startup failures into a
    wall of yellow CLI warnings because repeated failures were not
    deduplicated. The intended behavior is for those diagnostics to remain
    visible once in the thread that owns the startup, without polluting the
    parent transcript.
    
    ## What Changed
    
    - add nullable `threadId` ownership to `mcpServer/startupStatus/updated`
    - populate it from the app-server conversation ID for the current
    thread-scoped lifecycle and regenerate the protocol schema and
    TypeScript artifacts
    - treat a missing or null `threadId` as app-scoped without injecting it
    into the active chat transcript
    - route and buffer thread-owned MCP startup notifications by thread in
    the TUI
    - preserve buffered MCP startup events across child session refresh
    - seed expected MCP servers before replaying a thread snapshot so
    startup reaches its terminal state
    - suppress an identical repeated failure warning for the same server
    within one startup round
    
    The owning thread still renders the detailed failure and final `MCP
    startup incomplete (...)` summary.
    
    ## How to Test
    
    1. Configure an optional MCP server named `smoke` that exits during
    initialization.
    2. Launch the TUI with multi-agent support enabled.
    3. Confirm the main thread's own startup failure renders one detailed
    `smoke` warning and one incomplete-startup summary.
    4. Spawn exactly one subagent.
    5. Confirm the parent transcript does not receive the subagent's MCP
    startup failure.
    6. Switch to the subagent thread and confirm it contains exactly one
    detailed `smoke` failure and one incomplete-startup summary.
    7. Confirm the subagent's MCP startup spinner disappears and the thread
    remains usable.
    8. Switch between the parent and subagent and confirm the warnings
    neither move nor duplicate.
    
    Targeted tests:
    
    - `just test -p codex-app-server-protocol`
    - `just test -p codex-app-server
    thread_start_emits_mcp_server_status_updated_notifications`
    - `just test -p codex-tui mcp_startup`
    
    The parent/child behavior and spinner completion were also exercised
    manually in tmux. `just argument-comment-lint` was attempted but blocked
    by an unrelated local Bazel LLVM empty-glob failure; touched Rust
    callsites were inspected manually.
  • [codex] Deduplicate skill load warnings (#26698)
    Skill reloads can get noisy when the watcher keeps triggering
    `skills/list` and the same invalid `SKILL.md` error comes back each
    time.
    
    This keeps the first warning visible, then suppresses repeats while the
    same `(path, message)` is still active. If the error clears and later
    comes back, or if the message changes, it will show again.
    
    Validation:
    - `just fmt`
    - `just test -p codex-tui skill_load_warning_state`
  • [codex-rs] support v2 personal access tokens (#25731)
    ## Summary
    
    - add v2 personal access token support for `codex login
    --with-access-token` and `CODEX_ACCESS_TOKEN`
    - classify opaque `at-` tokens separately from legacy Agent Identity
    JWTs
    - hydrate required ChatGPT account metadata through AuthAPI
    `/v1/user-auth-credential/whoami`
    - use PATs directly as bearer tokens while preserving existing ChatGPT
    account surfaces
    - expose PAT-backed auth as the explicit `personalAccessToken`
    app-server auth mode
    
    ## Implementation
    
    PAT auth is intentionally small and stateless. Loading a PAT performs
    one AuthAPI metadata request, stores the hydrated metadata in the
    in-memory auth object, and redacts the secret from debug output. Legacy
    Agent Identity JWT handling remains unchanged. The shared access-token
    classifier lives in a private neutral module because it dispatches
    between both credential types.
    
    PAT hydration fails closed when AuthAPI omits any required metadata,
    including email. Hydrated metadata is intentionally not persisted:
    startup performs a live `whoami` preflight so revoked tokens or changed
    account metadata are not accepted from a stale cache.
    
    ## Workspace restriction scope
    
    This change intentionally does **not** apply
    `forced_chatgpt_workspace_id` to PAT authentication. The setting is a
    client-side config guardrail, not an authorization boundary, and PAT
    does not currently require workspace-ID parity. The PAT login and
    `CODEX_ACCESS_TOKEN` paths therefore validate through AuthAPI without
    threading workspace-restriction state through access-token loading.
    Existing workspace checks for non-PAT auth remain on their established
    paths.
    
    ## App-server compatibility
    
    The public app-server `AuthMode` is shared across v1 and v2, and
    PAT-backed auth reports `personalAccessToken` through both APIs.
    Following human review, this intentionally removes the temporary v1
    compatibility mapping that reported PATs as `chatgpt`; the deprecated v1
    API is kept in parity with v2 rather than maintaining a separate closed
    enum. Clients with exhaustive auth-mode handling in either API version
    must add the new case and should generally treat it as ChatGPT-backed
    unless they need PAT-specific behavior.
    
    The v1 auth-status response still omits the raw PAT when `includeToken`
    is requested because that response cannot carry the account metadata
    needed to reuse the credential safely. Persisted PAT auth also omits the
    new enum value so older Codex builds can deserialize `auth.json` and
    infer PAT auth from the credential field after a rollback.
    
    ## Validation
    
    Latest review-fix validation:
    
    - `CARGO_INCREMENTAL=0 just test -p codex-login` (126 passed)
    - `CARGO_INCREMENTAL=0 just test -p codex-cli` (263 passed)
    - `CARGO_INCREMENTAL=0 just test -p codex-cli
    stored_auth_validation_handles_personal_access_token`
    - `CARGO_INCREMENTAL=0 just test -p codex-app-server-protocol` (226
    passed)
    - `CARGO_INCREMENTAL=0 just test -p codex-models-manager
    refresh_available_models_uses_remote_only_catalog_for_chatgpt_auth`
    - `CARGO_INCREMENTAL=0 just test -p codex-tui
    existing_non_oauth_chatgpt_login_counts_as_signed_in`
    - `CARGO_INCREMENTAL=0 just fix -p codex-login -p
    codex-app-server-protocol -p codex-models-manager -p codex-tui -p
    codex-cli`
    - `just fmt`
    - `git diff --check`
    
    The broader `codex-tui` suite previously compiled and ran 2,834 tests.
    Three unrelated environment-sensitive guardian/IDE-socket tests failed
    after retries; the PAT-relevant TUI coverage passed.
  • Fix /goal usage text for control commands (#26551)
    ## Why
    
    The TUI's `/goal` usage text only advertised the objective form even
    though `/goal clear`, `/goal edit`, `/goal pause`, and `/goal resume`
    are implemented. This made the lifecycle controls difficult to discover
    and allowed the duplicated help text to drift from actual behavior.
    
    Fixes #25530.
    
    ## What changed
    
    - Show the complete `/goal [<objective>|clear|edit|pause|resume]` syntax
    in usage messages.
    - Share one usage string across slash-command dispatch and goal-related
    app messages.
    - Add inline snapshot coverage for the control-command usage path.
  • Surface TUI config write error causes (#26537)
    ## Summary
    
    TUI config writes currently wrap app-server failures with local context
    like `config/batchWrite failed in TUI`, but several user-visible paths
    only render the outer error. That hides the actionable app-server
    message, such as validation constraints or read-only `CODEX_HOME`
    failures, leaving users with a dead-end diagnostic.
    
    This change adds a small formatter next to the TUI config write helpers
    that renders the error source chain, then uses it for model persistence,
    feature persistence, project trust, status line writes, hook trust, and
    hook enablement.
    
    Fixes #26077
  • [codex] Forward turn moderation metadata through app-server (#25710)
    ## Why
    First-party backends can supply turn-scoped moderation metadata that
    app-server clients need for client-side presentation. Exposing this as
    an experimental typed notification lets opted-in clients consume it
    without interpreting raw Responses API events.
    
    ## What changed
    - forward `response.metadata.openai_chatgpt_moderation_metadata` from
    Responses API SSE and WebSocket streams as turn-scoped moderation
    metadata
    - emit the experimental app-server v2 `turn/moderationMetadata`
    notification with `{ threadId, turnId, metadata }`
    - add app-server integration coverage for the typed moderation metadata
    notification
    
    ## Testing
    - `just test -p codex-core
    build_ws_client_metadata_includes_window_lineage_and_turn_metadata`
    - `just test -p codex-core` (fails locally: 46 failures and 1 timeout,
    primarily missing `test_stdio_server` and shell snapshot timeouts)
    - `just test -p codex-app-server-protocol`
    - `just test -p codex-app-server
    turn_moderation_metadata_emits_typed_notification_v2`
    - `just test -p codex-app-server` (fails locally: 792 passed, 10 failed,
    and 5 timed out; failures are in existing environment-sensitive tests,
    primarily because nested macOS `sandbox-exec` is not permitted)
    - `just write-app-server-schema --experimental --schema-root
    /tmp/codex-app-server-schema-experimental`
  • [codex] Use model-advertised reasoning effort order (#26446)
    ## Summary
    - preserve the model catalog order for app-server
    `supportedReasoningEfforts` and document that client contract
    - render TUI reasoning choices in the advertised order
    - step reasoning shortcuts by adjacent list position instead of deriving
    order from known effort names
    - anchor unsupported configured values to the advertised default, or the
    first option when needed
    - remove canonical effort ordering helpers and the unused upgrade effort
    mapping
    
    ## Validation
    - `just fmt`
    - Local tests and compilation were not run per request; relying on CI.
    
    Stacked on #26444.
  • [codex] Support model-defined reasoning efforts (#26444)
    ## Summary
    - accept non-empty model-defined reasoning effort values while
    preserving built-in effort behavior
    - propagate the non-Copy effort type through core, app-server, TUI,
    telemetry, and persistence call sites
    - preserve string wire encoding and expose an open-string schema for
    clients
    - update model selection and shortcut behavior for model-advertised
    effort values
    
    ## Root cause
    `ReasoningEffort` gained a string-backed custom variant, so it could no
    longer implement `Copy` or rely on derived closed-enum serialization.
    Existing consumers still moved effort values from shared references and
    assumed a fixed built-in value set.
    
    ## Validation
    - `just fmt`
    - Local tests and compilation were not run per request; relying on CI.
  • Propagate permission approval environment id (#25862)
    ## Stack
    
    1. #25850 - Key request-permission grants by environment: stores and
    applies sticky permission grants per environment id.
    2. #25858 - Add `environmentId` to `request_permissions`: lets the model
    target a selected environment and resolves relative permission paths
    against it.
    3. This PR (#25862) - Propagate permission approval environment id:
    carries the selected environment id through approval events, app-server
    requests, TUI prompts, and delegate forwarding.
    4. #25867 - Add remote request permissions integration coverage:
    verifies the selected remote environment across request, approval, grant
    reuse, and exec.
    
    This PR is stacked on #25858, and #25867 is stacked on this PR.
    
    ## Why
    
    PR2 lets the model bind a `request_permissions` call to a selected
    environment, but the approval event and client-facing request still
    needed to carry that binding. For CCA, the user-facing prompt and
    delegated approval path should know which environment the grant applies
    to instead of relying on cwd alone.
    
    ## What Changed
    
    - Added optional `environmentId` to `RequestPermissionsEvent`.
    - Emit the selected environment id from core permission approval events.
    - Preserve the environment id through delegate forwarding, including
    cwd-based delegated requests.
    - Added `environmentId` to app-server permission approval params,
    generated schema/TypeScript artifacts, and README examples.
    - Preserve and display the environment id in TUI permission approval
    prompts.
    - Updated focused core, app-server protocol, and TUI conversion
    coverage.
    
    ## Testing
    
    Not run locally per instruction. Performed read-only `git diff --check`.
  • Reduce stack pressure in session startup and config rebuilds (#25844)
    ## Why
    
    `/clear` starts a fresh thread with `InitialHistory::Cleared`, which
    re-enters the thread/session startup path. That path now builds large
    async futures through `ThreadManagerState::spawn_thread_with_source`,
    `Codex::spawn`, and `Session::new`. Separately, TUI config rebuilds for
    cwd and permission-profile changes build a similarly heavy
    `ConfigBuilder::build()` future inside the app task. In debug and Bazel
    runs, those call chains can put enough state on the caller stack to
    abort before startup or config refresh completes.
    
    This change keeps the behavior the same while moving the heaviest future
    frames off the caller stack.
    
    ## What changed
    
    - Box `Codex::spawn(...)` in `codex-rs/core/src/thread_manager.rs`
    before awaiting it from `spawn_thread_with_source`.
    - Box `Session::new(...)` in `codex-rs/core/src/session/mod.rs` before
    awaiting it from `Codex::spawn_internal`.
    - Route `ConfigBuilder::build()` through a small `tokio::spawn` helper
    in `codex-rs/tui/src/app/config_persistence.rs` so cwd and
    permission-profile config rebuilds run on a runtime worker stack while
    preserving error context.
    
    ## Verification
    
    CI is running on the PR.
    
    No new targeted tests were added. This is a mechanical stack-pressure
    reduction that keeps the existing behavior and error propagation intact.
  • feat: show enterprise monthly credit limits in status (#24812)
    ## Summary
    
    Enterprise users can have an effective monthly credit limit, but Codex
    `/status` currently drops that metadata from the account-usage response.
    
    This change adds the optional `spend_control.individual_limit`
    projection to the existing rate-limit snapshot flow. The backend client
    reads the monthly limit, app-server exposes it as `individualLimit`, and
    the TUI renders a `Monthly credit limit` row through the existing
    progress-bar renderer.
    
    When the backend does not return an effective monthly limit, existing
    rate-limit behavior is unchanged.
    
    ## Existing backend state
    
    The account-usage backend already returns the effective monthly limit
    and current usage together:
    
    ```json
    {
      "spend_control": {
        "reached": false,
        "individual_limit": {
          "limit": "25000",
          "used": "8000",
          "remaining": "17000",
          "used_percent": 32,
          "remaining_percent": 68,
          "reset_after_seconds": 86400,
          "reset_at": 1778137680
        }
      }
    }
    ```
    
    Before this change, Codex projected rolling `primary` and `secondary`
    windows plus `credits`. It ignored `spend_control.individual_limit`, so
    app-server clients and `/status` could not render the monthly cap.
    
    The updated flow is:
    
    ```text
    account usage backend
      -> backend-client reads spend_control.individual_limit
      -> existing rate-limit snapshot carries optional individual_limit
      -> app-server exposes optional individualLimit
      -> TUI renders Monthly credit limit
    ```
    
    ## App-server contract
    
    `account/rateLimits/read` and sparse `account/rateLimits/updated`
    notifications now include an additive nullable
    `rateLimits.individualLimit` field:
    
    ```json
    {
      "individualLimit": {
        "limit": "25000",
        "used": "8000",
        "remainingPercent": 68,
        "resetsAt": 1778137680
      }
    }
    ```
    
    In an `account/rateLimits/read` response, `null` means no monthly limit
    is available. `account/rateLimits/updated` remains a sparse rolling
    notification: clients merge available values into their most recent
    `account/rateLimits/read` snapshot or refetch. Nullable account metadata
    in a rolling notification does not clear a previously observed value.
    
    ## Design decisions
    
    - Extend the existing rate-limit snapshot instead of introducing a
    separate request or wire-level update protocol.
    - Keep the Codex projection narrow: `/status` needs the effective limit,
    current usage, remaining percentage, and reset timestamp.
    - Render the monthly row through the existing progress-bar renderer,
    with one optional detail line for `8,000 of 25,000 credits used`.
    - Keep the backend response optional so existing accounts and older
    usage states preserve their current behavior.
    - Preserve cached monthly metadata when sparse rolling notifications
    omit it. Live account-usage reads remain authoritative and can clear a
    removed limit.
    
    ## Visual evidence
    
    ```text
     Monthly credit limit:   [██████████████░░░░░░] 68% left (resets 07:08 on 7 May)
                             8,000 of 25,000 credits used
    ```
    
    Snapshot:
    `codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_enterprise_monthly_credit_limit.snap`
    
    ## Testing
    
    Tests: generated app-server schema verification, protocol tests,
    backend-client tests, app-server integration coverage, TUI snapshot
    coverage, formatting, and workspace lint cleanup.
  • feat(tui): restore output-free cancelled prompts (#25316)
    ## TL;DR
    
    When you press Esc or Ctrl+C after sending a prompt but before any
    output was rendering, it restores the last composer and the message.
    
    ## Summary
    
    Cancelling a prompt immediately after submission should behave like
    returning to edit that prompt, not like discarding the user's draft.
    Today, pressing `Esc` or `Ctrl+C` before Codex responds leaves the
    submitted prompt in the transcript and returns an empty composer,
    forcing the user to recall or retype it.
    
    When an interrupted turn has not produced substantive visible output,
    restore its submitted prompt directly into the composer and roll back
    that latest turn. This also covers the first prompt in a fresh thread,
    before the TUI has retained a local user-history cell. The restored
    draft keeps its text, image attachments, and active collaboration mode
    so it can be edited and resubmitted in place.
    
    Restoration is intentionally suppressed once the turn has produced
    user-visible activity such as assistant output, tool work, hooks, or
    patches. A transient thinking status does not make the prompt
    ineligible. Rollback also rebuilds terminal scrollback from the retained
    transcript cells so repeated cancellations and terminal resizes do not
    duplicate history.
    
    ## How to Test
    
    1. Start the TUI with `cargo run -p codex-cli --bin codex`.
    2. In a fresh thread, submit the first prompt and press `Esc` before
    Codex emits substantive output. Confirm that the prompt returns to the
    composer for editing and its submitted transcript row is removed.
    3. Repeat with `Ctrl+C`, then repeat after at least one completed turn.
    Confirm the same behavior.
    4. Submit a prompt, wait for assistant output or tool activity, then
    cancel. Confirm that the transcript remains intact and the prompt is not
    restored into the composer.
    5. Cancel several output-free prompts and resize the terminal between
    attempts. Confirm that the startup banner, tip, and transcript history
    do not duplicate in scrollback.
    
    Targeted tests:
    - `just test -p codex-tui cancelled_turn_edit_restores_prompt`
    - `just test -p codex-tui
    output_free_interrupted_turn_requests_prompt_restore`
    - `just test -p codex-tui
    visible_output_prevents_cancelled_turn_prompt_restore`
    - `just test -p codex-tui
    thinking_status_keeps_cancelled_turn_prompt_restore_eligible`
    - `just test -p codex-tui
    patch_activity_prevents_cancelled_turn_prompt_restore`
    
    The full `just test -p codex-tui` run completed with `2746` passing
    tests and two unrelated existing guardian feature-flag failures. `just
    argument-comment-lint` remains blocked locally by the existing Bazel
    LLVM `compiler-rt` sanitizer-header glob failure; the touched Rust diff
    was manually audited for positional literal comments.
  • store and expose parent_thread_id on Threads (#25113)
    ## Why
    
    This PR
    https://github.com/openai/codex/pull/24161#discussion_r3325692763
    revealed a subagent data modeling issue, where we overloaded
    `forked_from_id` to also mean `parent_thread_id`. That's incorrect since
    guardian and review subagents can be a subagent and NOT fork the main
    thread's history.
    
    The solution here is to explicitly store a new `parent_thread_id` on
    `SessionMeta`, alongside `forked_from_id` which already exists. While
    we're at it, also expose it in the app-server protocol on the `Thread`
    object.
    
    A thread->subagent relationship and a fork of thread history are
    orthogonal concepts.
    
    ## What Changed
    
    - Added top-level `parent_thread_id` persistence on `SessionMeta` and
    runtime/session plumbing through `SessionConfiguredEvent`,
    `CodexSpawnArgs`, `SessionConfiguration`, `ThreadConfigSnapshot`,
    `TurnContext`, and `ModelClient`.
    - Made turn metadata, request headers, analytics, and subagent-start
    events read the separate runtime/top-level parent field instead of
    deriving general parent lineage from `SessionSource` or
    `forked_from_thread_id`.
    - Passed parent lineage separately at delegated subagent, review,
    guardian, agent-job, and multi-agent spawn construction sites;
    copied-history fork lineage remains derived only from `InitialHistory`.
    - Persisted and exposed parent lineage through rollout/thread-store
    projections and app-server v2 `Thread.parentThreadId`.
    - Updated app-server README text and regenerated app-server schema
    fixtures for the additive `parentThreadId` response field.
  • Add thread archive CLI commands (#25021)
    ## Problem
    
    Saved threads can already be archived through app-server RPCs, but the
    command line did not expose direct archive or unarchive commands.
    
    ## Solution
    
    Add `codex archive <thread>` and `codex unarchive <thread>`, resolving
    UUIDs or exact thread names before calling the existing `thread/archive`
    and `thread/unarchive` RPCs. The commands support scoped remote flags so
    callers can target remote app-server endpoints when archiving or
    unarchiving threads.
    
    This also fixes a long-standing bug in `codex resume <thread id>` and
    `codex fork <thread id>` that I found when testing the new commands.
    These operations shouldn't be allowed on archived sessions. They now
    fail with an error that tells the user to run `codex unarchive <thread
    id>` first.
    
    ## Verification
    
    Added app-server coverage for rejecting archived thread resume by id and
    checking that the error includes the matching `codex unarchive <thread
    id>` command.
  • Constrain Windows sandbox requirements (#23766)
    # Why
    
    Managed requirements can already constrain sandbox policy choices, but
    Windows sandbox implementation selection was still resolved
    independently from those requirements. That left the TUI able to
    continue through the unelevated fallback even when an organization wants
    to require the elevated Windows sandbox implementation.
    
    # What
    
    - Add `[windows].allowed_sandbox_implementations` requirements support
    for the Windows `elevated` and `unelevated` implementations.
    - Apply that allowlist during core config resolution so disallowed
    configured or feature-selected Windows sandbox implementations fall back
    to an allowed implementation with the existing requirements warning
    path.
    - Reuse the existing TUI Windows setup prompts to block disallowed
    unelevated continuation, keep required elevated setup in front of the
    user, and refuse to persist a TUI-selected Windows sandbox mode that
    requirements disallow.
    
    # Semantics
    
    | Allowed | Selected | Effective |
    | --- | --- | --- |
    | `["elevated"]` | `unelevated` / unset | `elevated` |
    | `["unelevated"]` | `elevated` / unset | `unelevated` |
    | `["elevated", "unelevated"]` | `elevated` | `elevated` |
    | `["elevated", "unelevated"]` | `unelevated` | `unelevated` |
    | `["elevated", "unelevated"]` | unset | `elevated` |
    
    Availability is handled by interactive setup surfaces after allowlist
    resolution. If the effective elevated implementation is not ready,
    elevated-only requirements block on setup. When unelevated is also
    allowed, the UI may offer the existing unelevated fallback.
    
    ## TUI Screens
    
    If elevated setup is not already complete:
    ```
      Your organization requires the default Codex agent sandbox to continue. Set it up to protect your files and control
      network access.
      Learn more <https://developers.openai.com/codex/windows>
    
    › 1. Set up default sandbox (requires Administrator permissions)
      2. Quit
    ```
    
    If admin setup fails under `["elevated"]`:
    ```
      Couldn't set up your sandbox with Administrator permissions
    
      Your organization requires the default sandbox before Codex can continue.
      Learn more <https://developers.openai.com/codex/windows>
    
    › 1. Try setting up admin sandbox again
      2. Quit
    ```
    
    # Next Steps
    
    
    - extend the requirements/readout surface, such as
    `configRequirements/read`, so clients can inspect the loaded
    `[windows].allowed_sandbox_implementations` requirement instead of
    inferring it from Windows setup state
    - consider extending `windowsSandbox/readiness` as well
    - update the App startup guide, setup flow, and banner surfaces so an
    elevated-only requirement omits any continue-unelevated escape hatch and
    blocks startup until a permitted implementation is ready;
    - preserve the existing unelevated fallback path when requirements allow
    it, including the `["unelevated"]` case where elevated is disallowed
  • Add /archive slash command (#25027)
    ## Why
    
    TUI users can archive saved sessions from other surfaces, but there is
    no in-session command for archiving the active session. Since archiving
    the active session also exits the TUI, the command should ask for
    explicit confirmation instead of firing immediately.
    
    I'm also working on [a companion
    PR](https://github.com/openai/codex/pull/25021) that adds `codex
    archive` and `codex unarchive` top-level CLI commands.
    
    ## What changed
    
    - Adds a new `/archive` slash command described as `archive this session
    and exit`.
    - Shows a confirmation dialog with `No, don't archive` selected first
    and `Yes, archive and exit` as the explicit action.
    - On confirmation, calls the existing `thread/archive` app-server RPC
    for the active main session and exits after success.
    - Keeps `/archive` disabled while a task is running and unavailable in
    side conversations.
    
    ## Verification
    
    Added focused TUI coverage for the `/archive` confirmation flow,
    disabled-while-task-running behavior, and the `/ar` slash-command popup
    snapshot.
  • Align TUI permissions labels with app (#25017)
    ## Summary
    
    The desktop app now presents the on-request permissions mode as `Ask for
    approval` and the manual-review-backed mode as `Approve for me`. The TUI
    still exposed older/internal labels like `Default` and `Auto-review`,
    which made the same underlying settings look different across clients.
    
    This updates the TUI UX copy to match the app without changing the
    underlying default behavior. Fresh threads continue to use the existing
    on-request approval mode, now displayed as `Ask for approval`.
    
    The label changes cover `/permissions`, explicit profile permissions
    menus, status surfaces, config persistence history/error text, and the
    corresponding TUI snapshots.
    
    ### Before
    <img width="1181" height="119" alt="Screenshot 2026-05-28 at 10 19
    47 PM"
    src="https://github.com/user-attachments/assets/0664846b-b6dd-4931-b4dd-d0af0d42058e"
    />
    <img width="523" height="19" alt="Screenshot 2026-05-28 at 10 21 29 PM"
    src="https://github.com/user-attachments/assets/7899c33e-b35d-4684-8389-97e357803423"
    />
    
    ### After
    <img width="1216" height="117" alt="Screenshot 2026-05-28 at 10 19
    32 PM"
    src="https://github.com/user-attachments/assets/015aab43-ac97-411f-8031-75cdd887251b"
    />
    <img width="567" height="18" alt="Screenshot 2026-05-28 at 10 20 24 PM"
    src="https://github.com/user-attachments/assets/28b6422c-b823-4298-b221-c83d46d09d66"
    />
  • windows-sandbox: pass workspace roots to runner (#24108)
    ## Why
    
    #23813 switches the Windows sandbox runner path to `PermissionProfile`,
    but it still left one runtime anchor for resolving symbolic
    `:workspace_roots` entries. That is not enough once a turn has multiple
    effective workspace roots: exact entries and deny globs under
    `:workspace_roots` need to be materialized for every runtime root before
    the command runner chooses token mode or builds ACL plans.
    
    ## What Changed
    
    - Replaces the Windows runner/setup `permission_profile_cwd` plumbing
    with `workspace_roots: Vec<AbsolutePathBuf>`.
    - Resolves Windows-local `PermissionProfile` data with
    `materialize_project_roots_with_workspace_roots(...)` instead of the
    single-cwd helper.
    - Threads `Config::effective_workspace_roots()` through core execution,
    unified exec, TUI setup/read-grant flows, app-server setup, app-server
    `command/exec`, and `debug sandbox` on Windows.
    - Preserves those workspace roots through the zsh-fork escalation
    executor instead of rebuilding them from `sandbox_policy_cwd`.
    - Makes `ExecRequest::new(...)` and the remaining
    `build_exec_request(...)` helper path take
    `windows_sandbox_workspace_roots` explicitly so new call sites cannot
    silently fall back to `vec![cwd]`.
    - Clarifies the `debug sandbox` non-Windows comment: remaining
    cwd-dependent resolution still uses `sandbox_policy_cwd`, while
    `:workspace_roots` entries are already materialized from config roots.
    - Updates elevated runner IPC `SpawnRequest` to send `workspace_roots`
    and bumps the framed IPC protocol version to `3` for the payload shape
    change.
    - Adds Windows-local resolver coverage for expanding exact and glob
    `:workspace_roots` entries across multiple roots, plus core helper
    coverage proving explicit roots are preserved.
    
    ## Verification
    
    - `cargo check -p codex-windows-sandbox -p codex-core -p codex-tui -p
    codex-cli -p codex-app-server`
    - `cargo test -p codex-windows-sandbox`
    - `cargo test -p codex-core windows_sandbox`
    - `cargo test -p codex-core unix_escalation`
    - `cargo test -p codex-app-server windows_sandbox`
    - `cargo test -p codex-tui windows_sandbox`
    - `cargo test -p codex-cli debug_sandbox`
    - `just test -p codex-core unified_exec`
    - `just test -p codex-core
    build_exec_request_preserves_windows_workspace_roots`
    - `env -u CODEX_NETWORK_PROXY_ACTIVE -u
    CODEX_NETWORK_ALLOW_LOCAL_BINDING just test -p codex-app-server --lib
    command_exec`
    - `just test -p codex-windows-sandbox`
    - `just test -p codex-exec sandbox`
    - `just fix -p codex-core -p codex-app-server -p codex-windows-sandbox`
    
    A local macOS cross-check with `cargo check --target
    x86_64-pc-windows-msvc ...` did not reach crate Rust code because native
    dependencies require Windows SDK headers (`windows.h` / `assert.h`) in
    this environment; Windows CI remains the real target validation.
    
    Two local targeted filters compile but do not run assertions on macOS:
    `env -u CODEX_NETWORK_PROXY_ACTIVE -u CODEX_NETWORK_ALLOW_LOCAL_BINDING
    just test -p codex-app-server --lib command_exec_processor` matched zero
    tests, and `just test -p codex-linux-sandbox landlock` matched zero
    tests because the landlock suite is Linux-only.
  • [codex] Add user input client ids (#24653)
    ## Summary
    
    Adds an optional `clientId` field to app-server v2 `UserInput` and
    carries it through the core `UserInput` model so clients can correlate
    echoed user input items without relying on payload equality.
    
    ## Details
    
    - Adds `client_id: Option<String>` to core `UserInput` variants.
    - Exposes the v2 app-server field as `clientId` on the wire and in
    generated TypeScript.
    - Preserves the id when converting between app-server v2 and core
    protocol types.
    - Regenerates app-server schema fixtures.
    
    ## Validation
    
    - `just fmt`
    - `just write-app-server-schema`
    - `cargo test -p codex-app-server-protocol`
    - `cargo test -p codex-protocol`
    - `just fix -p codex-app-server-protocol`
    - `just fix -p codex-protocol`
    - `git diff --check`
  • Expose MCP server info as part of server status (#24698)
    # Summary
    
    Expose MCP server info via App Server (when available) so apps can
    render a richer MCP experience
  • feat(tui): add OSC 8 web links to rich content (#24472)
    ## Why
    
    Wrapped URLs in rich TUI output, especially URLs rendered inside
    Markdown tables, are split across terminal rows. In terminals that
    support OSC 8 hyperlinks, treating each visible fragment as part of the
    complete destination enables reliable open-link and copy-link actions
    even after table layout wraps the URL.
    
    This addresses the semantic-link portion of #12200 and the behavior
    described in
    https://github.com/openai/codex/issues/12200#issuecomment-4535452980. It
    does not change ordinary drag-selection across bordered table rows.
    
    ## What Changed
    
    - Added shared TUI OSC 8 support that validates `http://` and `https://`
    destinations, sanitizes terminal payloads, and applies metadata
    separately from visible line width/layout.
    - Added semantic web-link annotations to assistant and proposed-plan
    Markdown, including explicit web links and bare web URLs in prose and
    table cells while excluding code and non-web Markdown destinations.
    - Preserved complete URL targets through table wrapping, narrow pipe
    fallback, streaming, transcript overlay rendering, history insertion,
    and resize replay.
    - Routed intentional Codex-owned links in notices,
    status/setup/app-link, feedback, onboarding, MCP/plugin help, memories,
    and update surfaces through the shared hyperlink handling.
    
    ## How to Test
    
    1. Run Codex in a terminal with OSC 8 link support, such as Ghostty, and
    request an assistant response containing a Markdown table whose last
    column contains a long `https://` URL.
    2. Make the terminal narrow enough for the URL to wrap across multiple
    bordered table rows.
    3. Use the terminal's open-link or copy-link action on more than one
    wrapped URL fragment and confirm each fragment resolves to the complete
    original URL.
    4. Resize the terminal after the table is rendered and repeat the link
    action to confirm the destination survives scrollback replay.
    5. Open the transcript overlay while rich output is present and confirm
    web links remain interactive there.
    6. As a regression check, render inline/fenced code containing URL text
    and a Markdown link such as
    `[https://example.com](mailto:support@example.com)`; confirm these do
    not acquire a web OSC 8 destination.
    
    Targeted automated coverage exercised Markdown links and exclusions,
    wrapped and pipe-fallback tables, streaming/transcript overlay
    propagation, status-link truncation, and rendered word-wrapping cell
    alignment. `just test -p codex-tui` was also run; it passed the
    hyperlink coverage and reproduced two unrelated existing guardian
    feature-flag test failures.
  • TUI config cleanup: plugin marketplace (#24257)
    ## Why
    
    Plugin and marketplace mutations are applied by the app server, but
    several TUI follow-up paths still refreshed state from the TUI host
    config. In remote workspace mode, that can leave plugin UI state tied to
    stale client-local `config.toml` after the server has already applied
    the mutation.
    
    ## What
    
    - Stop reloading the TUI host config after app-server-owned plugin,
    marketplace, skill, and app mutations.
    - Use the same app-server-owned refresh path for local and remote
    sessions: ask the app server to reload user config where the running
    session needs it, then refetch plugin list/detail state from the app
    server.
    - Build plugin mention candidates from existing app-server `plugin/list`
    and `plugin/read` data in both local and remote sessions instead of
    TUI-host plugin config.
    - Avoid the duplicate local config reload after `ReloadUserConfig` asks
    the app server to reload config.
    
    ## Verification
    
    Manually launched a local WebSocket app-server with a temp server
    `CODEX_HOME`, launched the TUI with a separate temp host `CODEX_HOME`
    and `--remote`, installed a sample plugin from a temp local marketplace
    through `/plugins`, and confirmed the TUI refreshed to installed state
    while only the server config gained `[plugins."sample@debug"]`. Trace
    logs showed the TUI using app-server `plugin/list` and `plugin/read` for
    the refresh path.
  • Uprev Rust toolchain pins to 1.95.0 (#24684)
    ## Summary
    - Bump the workspace Rust toolchain from `1.93.0` to `1.95.0` across
    Cargo, Bazel, CI, release workflows, devcontainers, and the Codex
    environment config.
    - Refresh `MODULE.bazel.lock` so the Bazel Rust toolchain artifacts
    match the new version.
    - Leave purpose-specific toolchains unchanged, including the
    `argument-comment-lint` nightly and the upstream `rusty_v8` `1.91.0`
    build pin.
    - Includes fixes for new lints from `just fix` and a few codex-authored
    fixes for lints without a suggestion.
  • windows-sandbox: remove SandboxPolicy runner plumbing (#23813)
    ## Why
    
    The Windows sandbox runner still carried the old `SandboxPolicy`
    compatibility path even though core now computes `PermissionProfile`.
    That meant Windows command-runner execution could only see the legacy
    projection, so profile-only filesystem rules such as deny globs were not
    part of the runner input.
    
    ## What Changed
    
    - Removed the Windows-local `SandboxPolicy` parser/export and deleted
    `windows-sandbox-rs/src/policy.rs`.
    - Changed restricted-token capture/session setup, elevated setup,
    world-writable audit, read-root grant, and command-runner session APIs
    to accept `PermissionProfile` plus the profile cwd.
    - Bumped the elevated command-runner IPC protocol to version 2 because
    `SpawnRequest` now carries `permission_profile` /
    `permission_profile_cwd` instead of the legacy `policy_json_or_preset` /
    `sandbox_policy_cwd` fields.
    - Updated core exec, unified exec, debug-sandbox, TUI setup/grant flows,
    and app-server setup to pass the actual effective `PermissionProfile`.
    - Left regression coverage asserting the old IPC policy fields are
    absent and the runner serializes tagged `PermissionProfile` JSON.
    
    ## Verification
    
    - `cargo test -p codex-windows-sandbox`
    - `cargo test -p codex-core windows_sandbox`
    - `cargo test -p codex-app-server
    request_processors::windows_sandbox_processor`
    - `just fix -p codex-windows-sandbox -p codex-core -p codex-app-server
    -p codex-cli -p codex-tui`
    - `just fix -p codex-cli -p codex-tui`
    - `just fix -p codex-windows-sandbox -p codex-tui`
    - `rg "\\bSandboxPolicy\\b" codex-rs/windows-sandbox-rs` returned no
    matches.
    
    Note: `cargo test -p codex-cli` was attempted but did not reach crate
    tests because local disk filled while compiling dependencies (`No space
    left on device`). The targeted clippy pass compiled the affected CLI/TUI
    surfaces afterward.
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/23813).
    * #24108
    * __->__ #23813
  • TUI config cleanup: plugin mentions (#24266)
    ## Summary
    
    TUI plugin mention refresh still joined app-server plugin inventory with
    client-local plugin config, which can diverge once plugin state is owned
    by the app server.
    
    This changes the TUI to mirror the GUI client: `plugin/list` is the
    autocomplete source, and mention candidates are plugin-level entries
    filtered to installed, enabled, and not disabled by admin. The TUI no
    longer reads local plugin config or calls `plugin/read` while refreshing
    plugin mention candidates.
    
    ## API shape and limitations
    
    The current app-server API does not expose effective per-session plugin
    capability summaries for mention autocomplete. As in the GUI,
    autocomplete now trusts `plugin/list` metadata rather than proving which
    plugin capabilities are loaded in the active session.
    
    That avoids stale client-local reads and the cwd/remote detail gaps in
    `plugin/read`, but intentionally accepts the same list-level tradeoff as
    the app: if `plugin/list` reports a remote plugin before its local
    bundle is materialized, the plugin can still appear as a mention
    candidate.
  • Use thread config for TUI MCP inventory (#24532)
    ## Summary
    `/mcp` in the TUI should reflect the current loaded thread, including
    project-local MCP servers from that thread config. Before this change,
    `mcpServerStatus/list` only read the latest global MCP config, so the
    active chat could miss project-local servers.
    
    This adds optional `threadId` to `mcpServerStatus/list`. When present,
    app-server resolves the loaded thread and lists MCP status from the
    refreshed effective config for that thread; when omitted, existing
    global config behavior stays unchanged.
    
    The TUI now sends the active chat thread id for `/mcp` and `/mcp
    verbose`, carries that origin through the async inventory result, and
    ignores stale completions if the user has switched threads before the
    fetch returns. The app-server schemas were regenerated.
    
    ## Follow-up
    Once this app-server API change lands, the desktop app should make the
    same `threadId` plumbing so its MCP inventory also uses the current
    thread config.
    
    Fixes #23874
  • TUI config cleanup: MCP inventory (#24265)
    ## Summary
    
    The TUI `/mcp` inventory flow should reflect the app server’s MCP status
    response. It was also joining those results with the TUI process’s local
    `config.mcp_servers`, which can diverge once MCP state is owned by a
    remote app server and cause stale local command, URL, status, or
    empty-state details to render.
    
    This change removes the local config join from the app-server-backed
    inventory renderer. The TUI now renders directly from the existing
    `mcpServerStatus/list` payload and treats an empty status response as
    the empty MCP inventory state.
    
    ## Known limitation
    
    The existing `mcpServerStatus/list` payload does not include
    disabled-state or disabled-reason fields. To preserve the current
    app-server API, this PR does not try to infer that state from
    client-local config. If remote `/mcp` needs to show disabled/reason
    details again, that should come from app-server-owned status data in a
    follow-up.
    
    Related to #22914, #22915, and #22916.
  • Show remote connection details in /status (#24420)
    ## Summary
    
    Fixes #24411.
    
    `/status` currently has no way to show when the TUI is talking to Codex
    through a remote transport. That makes embedded local sessions, local
    daemon sessions, and true remote sessions look the same, and it hides
    the remote server version when debugging connection-specific behavior.
    
    This PR adds a single `Remote` row for non-embedded connections only.
    The row shows the sanitized connection address and a dimmed version
    parenthetical, preserving the existing status output for embedded local
    sessions.
    
    <img width="791" height="144" alt="image"
    src="https://github.com/user-attachments/assets/529d7940-1c45-4586-8b06-f20a1f04b771"
    />
    
    
    ## Verification
    
    - Manually validated when connecting remotely (either implicitly to
    local daemon or explicitly)
  • config: remove legacy profile write paths (#24055)
    ## Why
    
    [#23883](https://github.com/openai/codex/pull/23883) moved the
    user-facing `--profile` flag onto profile v2 and
    [#23886](https://github.com/openai/codex/pull/23886) removed CLI
    forwarding for the legacy profile-v1 path. Core and TUI config
    persistence still carried `active_profile` and
    `ConfigEditsBuilder::with_profile`, which let later writes continue
    targeting legacy `[profiles.<name>]` tables after profile selection
    moved to profile-v2 config files.
    
    ## What
    
    - Remove legacy profile routing from
    [`ConfigEditsBuilder`](https://github.com/openai/codex/blob/4b38e9c22e762261d7f7eef49d8a21792e241a06/codex-rs/core/src/config/edit.rs#L1064-L1294),
    so core config edits no longer carry `with_profile` or infer
    `[profiles.*]` write targets from a `profile` key.
    - Drop `active_profile` plumbing from runtime `Config`, TUI
    startup/state, app-server config override forwarding, and Windows
    sandbox setup persistence.
    - Make app-server-backed TUI config edits use unscoped model,
    service-tier, feature, Auto-review, plan-mode, and Windows sandbox paths
    through
    [`tui/src/config_update.rs`](https://github.com/openai/codex/blob/4b38e9c22e762261d7f7eef49d8a21792e241a06/codex-rs/tui/src/config_update.rs#L43-L112).
    - Update config edit coverage so legacy `profile` state stays untouched
    by direct model writes, and remove tests whose only contract was the
    deleted profile-scoped persistence path.
    
    ## Testing
    
    - Not run locally.
  • Fix auto-review permission profile override (#23956)
    ## Summary
    The auto-review runtime sync path was assigning a raw
    `PermissionProfile` into `runtime_permission_profile_override`, whose
    field now expects `RuntimePermissionProfileOverride`. That broke the TUI
    Bazel build.
    
    This changes the assignment to store
    `RuntimePermissionProfileOverride::from_config(&self.config)`, matching
    the other runtime override paths and preserving the active profile and
    network metadata with the permission profile.
  • [3 of 4] tui: route feature and memory toggles through app server (#22915)
    ## Why
    Experimental feature toggles and memory settings can update several
    related config values in one interaction. Keeping those writes local in
    a remote TUI session is especially dangerous because the UI can diverge
    from the app-server config while also leaving behind partially stale
    supporting keys.
    
    This is **[3 of 4]** in a stacked series that moves TUI-owned config
    mutations onto app-server APIs.
    
    ## What changed
    - Routed feature flag persistence through app-server batch writes,
    including the supporting reviewer and permission updates used by
    guardian approval.
    - Routed Windows sandbox mode persistence and legacy Windows feature
    cleanup through app-server writes.
    - Routed memory settings through app-server batch writes and updated the
    TUI tests to exercise the embedded app-server path.
    
    ## Config keys affected
    - `features.<feature_key>`
    - `profiles.<profile>.features.<feature_key>`
    - `approval_policy`
    - `sandbox_mode`
    - `approvals_reviewer`
    - `windows.sandbox`
    - `features.experimental_windows_sandbox`
    - `features.elevated_windows_sandbox`
    - `features.enable_experimental_windows_sandbox`
    - Profile-scoped Windows legacy feature variants under
    `profiles.<profile>.features.*`
    - `memories.use_memories`
    - `memories.generate_memories`
    - Profile-scoped memory variants under `profiles.<profile>.memories.*`
    
    ## Suggested manual validation
    - Connect the TUI to a remote app server, toggle guardian approval on
    and off, and confirm the remote config updates
    `features.guardian_approval`, reviewer state, approval policy, and
    sandbox mode coherently.
    - Toggle a default-false experimental feature at the root level, disable
    it again, and confirm the key clears instead of lingering as an
    unnecessary explicit `false`.
    - Change memory settings and confirm the remote config updates both
    memory keys while the running TUI reflects the new state.
    - On Windows, switch sandbox mode through the TUI and confirm
    `windows.sandbox` is updated while the legacy Windows feature keys are
    cleared.
    
    ## Stack
    1. [#22913](https://github.com/openai/codex/pull/22913) `[1 of 4]`
    primary settings writes
    2. [#22914](https://github.com/openai/codex/pull/22914) `[2 of 4]` app
    and skill enablement
    3. [#22915](https://github.com/openai/codex/pull/22915) `[3 of 4]`
    feature and memory toggles
    4. [#22916](https://github.com/openai/codex/pull/22916) `[4 of 4]`
    startup and onboarding bookkeeping
  • TUI: skip goal replace prompt for completed goals (#23792)
    ## Why
    Users reported that the replacement confirmation feels unnecessary when
    the current thread goal is already complete. In that state, `/goal
    <objective>` is starting fresh rather than interrupting active work.
    
    ## What changed
    `/goal <objective>` now skips the replace confirmation when the existing
    goal has `complete` status and uses the existing fresh replacement path.
    Goals that are active, paused, blocked, usage-limited, or budget-limited
    still require confirmation before being replaced.
  • Improve /goal error messages for ephemeral sessions (#23796)
    ## Why
    
    When a user runs `/goal` in a temporary session, the TUI can currently
    surface an internal app-server failure such as `thread/goal/get failed
    in TUI`. That message is technically true, but it does not explain the
    actual constraint: goals require a saved session because goal state is
    persisted with the thread.
    
    This is especially confusing when `codex doctor` reports the background
    app-server as running in ephemeral mode, since that wording is easy to
    conflate with ephemeral thread/session behavior.
    
    ## What changed
    
    - Added a TUI-side formatter for thread-goal RPC failures in
    `codex-rs/tui/src/app/thread_goal_actions.rs`.
    - Detects app-server/core errors that indicate goals are unsupported for
    an ephemeral thread/session.
    - Replaces the internal RPC failure with a user-facing explanation:
    
    ```text
    Goals need a saved session. This session is temporary.
    Run `codex` to start a saved session, or `codex resume` / `/resume` to reopen one.
    ```
    
    - Preserves the existing generic failure wording for non-ephemeral goal
    errors.
    
    ## Verification
    
    - `cargo test -p codex-tui thread_goal_error_message --lib`
    
    I also tried `cargo test -p codex-tui`; it built successfully but the
    test runner aborted in an unrelated side-thread stack overflow
    (`app::tests::discard_side_thread_removes_agent_navigation_entry`),
    which reproduced when run by itself.
  • tui: plumb permission profile selection (#23708)
    ## Why
    
    The named-profile `/permissions` picker needs a small TUI action path
    that can select permission profiles without folding the menu UI and
    profile metadata into the same review.
    
    ## What changed
    
    - Carry permission-profile selections through the TUI app event flow.
    - Persist selected profiles while preserving the existing approval
    settings and guardrail prompts.
    - Keep the legacy `/permissions` picker behavior in this layer; the
    profile-mode menu stays in the follow-up PR.
    
    ## Stack
    
    1. [#22931](https://github.com/openai/codex/pull/22931):
    runtime/session/network propagation for active permission profiles.
    2. **This PR**: TUI selection plumbing and guardrail flow.
    3. [#21559](https://github.com/openai/codex/pull/21559): profile-aware
    `/permissions` menu and custom profile display.
    
    <img width="1632" height="1186" alt="image"
    src="https://github.com/user-attachments/assets/69ddcd5e-b57c-468d-8c1d-246916323c15"
    />
    
    ## Validation
    
    - `git diff --cached --check` before commit.
    - Full test run skipped at the user request while pushing the split
    stack.
  • Honor client-resolved service tier defaults (#23537)
    ## Why
    
    Model catalog responses can now advertise a nullable
    `default_service_tier` for each model. Codex needs to preserve three
    distinct states all the way from config/app-server inputs to inference:
    
    - no explicit service tier, so the client may apply the current model
    catalog default when FastMode is enabled
    - explicit `default`, meaning the user intentionally wants standard
    routing
    - explicit catalog tier ids such as `priority`, `flex`, or future tiers
    
    Keeping those states distinct prevents the UI from showing one tier
    while core sends another, especially after model switches or app-server
    `thread/start` / `turn/start` updates.
    
    ## What Changed
    
    - Plumbed `default_service_tier` through model catalog protocol types,
    app-server model responses, generated schemas, model cache fixtures, and
    provider/model-manager conversions.
    - Added the request-only `default` service tier sentinel and normalized
    legacy config spelling so `fast` in `config.toml` still materializes as
    the runtime/request id `priority`.
    - Moved catalog default resolution to the TUI/client side, including
    recomputing the effective service tier when model/FastMode-dependent
    surfaces change.
    - Updated app-server thread lifecycle config construction so
    `serviceTier: null` preserves explicit standard-routing intent by
    mapping to `default` instead of internal `None`.
    - Kept core responsible for validating explicit tiers against the
    current model and stripping `default` before `/v1/responses`, without
    applying catalog defaults itself.
    
    ## Validation
    
    - `CARGO_INCREMENTAL=0 cargo build -p codex-cli`
    - `CARGO_INCREMENTAL=0 cargo test -p codex-app-server model_list`
    - `cargo test -p codex-tui service_tier`
    - `cargo test -p codex-protocol service_tier_for_request`
    - `cargo test -p codex-core get_service_tier`
    - `RUST_MIN_STACK=8388608 CARGO_INCREMENTAL=0 cargo test -p codex-core
    service_tier`
  • core: refresh active permission profiles at runtime (#22931)
    ## Why
    
    Once a named permission profile is selected, runtime state has to keep
    that profile identity intact instead of collapsing back to anonymous
    effective permissions. The session refresh path also needs to rebuild
    profile-derived network proxy state so active profile switches take
    effect consistently.
    
    ## What changed
    
    - Preserve the active permission profile through session updates.
    - Rebuild profile-derived runtime/network configuration when the active
    profile changes.
    - Keep the runtime path aligned with the current session configuration
    APIs.
    - Tighten the affected tests, including the Windows delete-pending
    memory-file case that was intermittently tripping CI.
    
    ## Stack
    
    1. **This PR**: runtime/session/network propagation for active
    permission profiles.
    2. [#23708](https://github.com/openai/codex/pull/23708): TUI selection
    plumbing and guardrail flow.
    3. [#21559](https://github.com/openai/codex/pull/21559): profile-aware
    `/permissions` menu and custom profile display.
    
    <img width="1296" height="906" alt="image"
    src="https://github.com/user-attachments/assets/077fa3a7-80cb-4925-80b1-d2395018d90a"
    />
  • Sync TUI thread settings through app server (#23507)
    Builds on #23502.
    
    ## Why
    
    #23502 adds the app-server `thread/settings/update` API and matching
    `thread/settings/updated` notification. The TUI already lets users
    change thread-scoped settings such as model, reasoning effort, service
    tier, approvals, permissions, personality, and collaboration mode, but
    those updates need to flow through the app server so embedded and
    connected clients observe the same thread state.
    
    This is a rework (simplification) of PR
    https://github.com/openai/codex/pull/22510. It has the same
    functionality, but the underlying `thread/settings/update` api is now
    simpler in that it no longer returns the effective settings as a
    response. Now, clients receive the effective settings only through the
    `thread/settings/updated` notification.
    
    ## What Changed
    
    This updates the TUI to send `thread/settings/update` whenever those
    thread-scoped settings change and to treat the RPC response as the
    authoritative acknowledgement. It also routes `thread/settings/updated`
    notifications back into cached session state and the visible chat widget
    so active and inactive threads stay in sync after app-server-originated
    changes.
    
    The implementation is kept to the TUI layer: settings conversion and
    merge logic live under `codex-rs/tui/src/app/thread_settings.rs`, with
    dispatch/routing hooks in the existing app and chat widget paths.
    
    ## Verification
    
    I manually tested using `codex app-server --listen unix://` and then
    launching two copies of the TUI that use the same local app server. I
    then resumed the same thread on both and verified that changes like plan
    mode, fast mode, model, reasoning effort, etc. are reflected "live" in
    the second client when modified in the first and vice versa.
  • Add thread/settings/update app-server API (#23502)
    ## Why
    
    App-server clients need a way to update a thread's next-turn settings
    without starting a turn, adding transcript content, or waiting for turn
    lifecycle events. This gives settings UI a direct path for durable
    thread settings while clients observe the eventual effective state
    through a notification.
    
    This is a simplified rework of PR
    https://github.com/openai/codex/pull/22509. In particular, it changes
    the `thread/settings/update` api to return immediately rather than
    waiting and returning the effective (updated) thread settings. This
    makes the new api consistent with `turn/start` and greatly reduces the
    complexity of the implementation relative to the earlier attempt.
    
    ## What Changed
    
    - Adds experimental `thread/settings/update` with partial-update request
    fields and an empty acknowledgment response.
    - Adds experimental `thread/settings/updated`, carrying full effective
    `ThreadSettings` and scoped by `threadId` to subscribed clients for the
    affected thread.
    - Shares durable settings validation with `turn/start`, including
    `sandboxPolicy` plus `permissions` rejection and `serviceTier: null`
    clearing.
    - Emits the same settings notification when `turn/start` overrides
    change the stored effective thread settings.
    - Regenerates app-server protocol schema fixtures and updates
    `app-server/README.md`.
  • [2 of 2] Start fresh TUI thread in background (#23176)
    ## Why
    
    After the terminal-probe work in #23175, fresh-session startup still
    waits for `thread/start` before the chat input can become usable. The
    chat widget already has the machinery to hold early submissions until a
    session is configured, so fresh `thread/start` does not need to stay on
    the input-ready hot path.
    
    Refs #16335.
    
    ## What
    
    This PR starts fresh app-server threads in a background task, reports
    completion through a startup app event, and attaches the primary session
    once `thread/start` returns. Resume and fork startup paths remain
    synchronous.
    
    ## Benchmark
    
    In the local pty startup benchmark, this PR's pre-optimization base
    branch, #23175, measured about 152ms median from launch to accepted chat
    input. The stacked result measured about 66ms median, for an approximate
    additional savings of 85-95ms. For broader context, the original `main`
    baseline before either startup optimization was about 250.5ms median. We
    also measured Codex 0.117.0 on the same machine at about 64.6ms median,
    so the stacked branch is back in the old-startup-time range.
    
    ## Stack
    
    1. [#23175: [1 of 2] Optimize TUI startup terminal
    probes](https://github.com/openai/codex/pull/23175) — base PR
    2. [#23176: [2 of 2] Start fresh TUI thread in
    background](https://github.com/openai/codex/pull/23176) — this PR
    
    ## Verification
    
    - `cargo test -p codex-tui`
  • Fix: TUI starting in wrong CWD (#23538)
    This fixes a regression wher codex could start in the wrong directory
    when a live local app-server socket was present. The issue was that
    implicit local socket reuse was being treated like an explicit remote
    workspace session, which dropped the invoking cwd unless --cd was
    passed.
    
    The change separates local socket transport from true remote workspace
    semantics.
    - Plain local startup keeps local cwd, trust, resume, picker, and
    config-refresh behavior.
    - Explicit --remote keeps the existing remote cwd behavior.
    - Added coverage for launch target selection and local-session
    filtering/cwd behavior.
    
    Steps to test:
    - Start a local app-server from a different directory than the repo you
    want to use.
      - Launch codex from a project/worktree without --cd.
    - Confirm the session starts in the invoking directory, not the
    app-server process directory.
    - Confirm explicit codex --remote ... still preserves existing remote
    behavior.
  • Make deny canonical for filesystem permission entries (#23493)
    ## Why
    Filesystem permission profiles used `none` for deny-read entries, which
    is less direct than the action the entry actually represents. This
    change makes `deny` the canonical filesystem permission spelling while
    preserving compatibility for older configs that still send `none`.
    
    ## What changed
    - rename `FileSystemAccessMode::None` to `Deny`
    - serialize and generate schemas with `deny` as the canonical value
    - retain `none` only as a legacy input alias for temporary config
    compatibility
    - update filesystem glob diagnostics and regression coverage to use the
    canonical spelling
    - refresh config and app-server schema fixtures to match the new wire
    shape
    
    ## Validation
    - `cargo test -p codex-protocol`
    - `cargo test -p codex-app-server-protocol`
    - `cargo test -p codex-core config_toml_deserializes_permission_profiles
    --lib`
    - `cargo test -p codex-core
    read_write_glob_patterns_still_reject_non_subpath_globs --lib`
    
    Earlier in the session, a broad `cargo test -p codex-core` run reached
    unrelated pre-existing failures in timing/snapshot/git-info tests under
    this environment; the targeted surfaces touched by this PR passed
    cleanly.
  • [2 of 4] tui: route app and skill enablement through app server (#22914)
    ## Why
    App and skill toggles are user config mutations too. When the TUI is
    attached to a remote app server, writing those toggles into the local
    `config.toml` makes the UI report success without updating the server
    that actually owns the session.
    
    This is **[2 of 4]** in a stacked series that moves TUI-owned config
    mutations onto app-server APIs.
    
    ## What changed
    - Routed app enable/disable persistence through app-server config batch
    writes.
    - Routed skill enable/disable persistence through `skills/config/write`.
    - Avoided refreshing local config from disk after these writes when the
    TUI is connected to a remote app server.
    
    ## Config keys affected
    - `apps.<app_id>.enabled`
    - `apps.<app_id>.disabled_reason`
    - `[[skills.config]]` entries keyed by `path`, with `enabled = false`
    used for persisted disables
    
    ## Suggested manual validation
    - Connect the TUI to a remote app server, disable an app, reconnect, and
    confirm the app remains disabled from remote config rather than local
    disk state.
    - Re-enable the same app and confirm both `apps.<app_id>.enabled` and
    `apps.<app_id>.disabled_reason` are cleared remotely.
    - Disable a skill in the manage-skills UI and confirm a remote
    `[[skills.config]]` disable entry appears.
    - Re-enable that skill and confirm the disable entry is removed and the
    effective enabled state updates without relying on local config reloads.
    
    ## Stack
    1. [#22913](https://github.com/openai/codex/pull/22913) `[1 of 4]`
    primary settings writes
    2. [#22914](https://github.com/openai/codex/pull/22914) `[2 of 4]` app
    and skill enablement
    3. [#22915](https://github.com/openai/codex/pull/22915) `[3 of 4]`
    feature and memory toggles
    4. [#22916](https://github.com/openai/codex/pull/22916) `[4 of 4]`
    startup and onboarding bookkeeping
  • Clarify resume hints for renamed threads (#23234)
    Addresses #23181
    
    ## Why
    Renamed threads can share names, so hints that suggest resuming directly
    by name are ambiguous. Issue #23181 asks for the picker hint to include
    the thread name and thread ID in parens so users can disambiguate
    safely.
    
    ## What
    - Adds a shared resume hint formatter for named threads: run `codex
    resume`, then select `<name> (<thread-id>)`.
    - Uses that hint for /rename confirmations, TUI session summaries, and
    CLI/TUI exit messages.
    - Keeps direct `codex resume <thread-id>` guidance for unnamed threads.
    
    ## Verification
    Manually verified that message after `/rename` and after `/exit` include
    session ID in parens.
    
    ---------
    
    Co-authored-by: Felipe Coury <felipe.coury@openai.com>
  • goal: pause continuation loops on usage limits and blockers (#23094)
    Addresses #22833, #22245, #23067
    
    ## Why
    `/goal` can keep synthesizing turns even when the next turn cannot make
    meaningful progress. Hard usage exhaustion can replay failing turns, and
    repeated permission or external-resource blockers can keep burning
    tokens while waiting for user or system intervention.
    
    ## What changed
    - Add resumable `blocked` and `usageLimited` goal states. As with
    `paused`, goal continuation stops with these states.
    - Move to `usageLimited` after usage-limit failures.
    - Allow the built-in `update_goal` tool to set `blocked` only under
    explicit repeated-impasse guidance. Updated goal continuation prompt to
    specify that agent should use `blocked` only when it has made at least
    three attempts to get past an impasse.
    
    Most of the files touched by this PR are because of the small app server
    protocol update.
    
    ## Validation
    
    I manually reproduced a number of situations where an agent can run into
    a true impasse and verified that it properly enters `blocked` state. I
    then resumed and verified that it once again entered `blocked` state
    several turns later if the impasse still exists.
    
    I also manually reproduced the usage-limit condition by creating a
    simulated responses API endpoint that returns 429 errors with the
    appropriate error message. Verified that the goal runtime properly moves
    the goal into `usageLimited` state and TUI UI updates appropriately.
    Verified that `/goal resume` resumes (and immediately goes back into
    `ussageLImited` state if appropriate).
    
    
    ## Follow-up PRs
    
    Small changes will be needed to the GUI clients to properly handle the
    two new states.