Commit Graph

5251 Commits

  • Queue Realtime V2 response.create while active (#17306)
    Builds on #17264.
    
    - queues Realtime V2 `response.create` while an active response is open,
    then flushes it after `response.done` or `response.cancelled`
    - requests `response.create` after background agent final output and
    steering acknowledgements
    - adds app-server integration coverage for all `response.create` paths
    
    Validation:
    - `just fmt`
    - `cargo check -p codex-app-server --tests`
    - `git diff --check`
    - CI green
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • feat(guardian): send only transcript deltas on guardian followups (#17269)
    ## Description
    
    We reuse a guardian thread for a given user thread when we can. However,
    we had always sent the full transcript history every time we made a
    followup review request to an existing guardian thread.
    
    This is especially bad for long guardian threads since we keep
    re-appending old transcript entries instead of just what has changed.
    The fix is to just send what's new.
    
    **Caveat**: Whenever a thread is compacted or rolled back, we fall back
    to sending the full transcript to guardian again since the thread's
    history has been modified. However in the happy path we get a nice
    optimization.
    
    ## Before
    Initial guardian review sends the full parent transcript:
    
    ```
    The following is the Codex agent history whose request action you are assessing...
    >>> TRANSCRIPT START
    [1] user: Please check the repo visibility and push the docs fix if needed.
    [2] tool gh_repo_view call: {"repo":"openai/codex"}
    [3] tool gh_repo_view result: repo visibility: public
    [4] assistant: The repo is public; I now need approval to push the docs fix.
    >>> TRANSCRIPT END
    The Codex agent has requested the following action:
    >>> APPROVAL REQUEST START
    ...
    >>> APPROVAL REQUEST END
    ```
    
    And a followup to the same guardian thread would send the full
    transcript again (including items 1-4 we already sent):
    ```
    The following is the Codex agent history whose request action you are assessing...
    >>> TRANSCRIPT START
    [1] user: Please check the repo visibility and push the docs fix if needed.
    [2] tool gh_repo_view call: {"repo":"openai/codex"}
    [3] tool gh_repo_view result: repo visibility: public
    [4] assistant: The repo is public; I now need approval to push the docs fix.
    [5] user: Please push the second docs fix too.
    [6] assistant: I need approval for the second docs fix.
    >>> TRANSCRIPT END
    The Codex agent has requested the following action:
    >>> APPROVAL REQUEST START
    ...
    >>> APPROVAL REQUEST END
    ```
    
    ## After
    Initial guardian review sends the full parent transcript (this is
    unchanged):
    
    ```
    The following is the Codex agent history whose request action you are assessing...
    >>> TRANSCRIPT START
    [1] user: Please check the repo visibility and push the docs fix if needed.
    [2] tool gh_repo_view call: {"repo":"openai/codex"}
    [3] tool gh_repo_view result: repo visibility: public
    [4] assistant: The repo is public; I now need approval to push the docs fix.
    >>> TRANSCRIPT END
    The Codex agent has requested the following action:
    >>> APPROVAL REQUEST START
    ...
    >>> APPROVAL REQUEST END
    ```
    
    But a followup now sends:
    ```
    The following is the Codex agent history added since your last approval assessment. Continue the same review conversation...
    >>> TRANSCRIPT DELTA START
    [5] user: Please push the second docs fix too.
    [6] assistant: I need approval for the second docs fix.
    >>> TRANSCRIPT DELTA END
    The Codex agent has requested the following next action:
    >>> APPROVAL REQUEST START
    ...
    >>> APPROVAL REQUEST END
    ```
  • fix: MCP leaks in app-server (#17223)
    The disconnect path now reuses the same teardown flow as explicit
    unsubscribe, and the thread-state bookkeeping consistently reports only
    threads that lost their last subscriber
    
    https://github.com/openai/codex/issues/16895
  • feat: make rollout recorder reliable against errors (#17214)
    The rollout writer now keeps an owned/monitored task handle, returns
    real Result acks for flush/persist/shutdown, retries failed flushes by
    reopening the rollout file, and keeps buffered items until they are
    successfully written. Session flushes are now real durability barriers
    for fork/rollback/read-after-write paths, while turn completion surfaces
    a warning if the rollout still cannot be saved after recovery.
  • feat: move exec-server ownership (#16344)
    This introduces session-scoped ownership for exec-server so ws
    disconnects no longer immediately kill running remote exec processes,
    and it prepares the protocol for reconnect-based resume.
    - add session_id / resume_session_id to the exec-server initialize
    handshake
      - move process ownership under a shared session registry
    - detach sessions on websocket disconnect and expire them after a TTL
    instead of killing processes immediately (we will resume based on this)
    - allow a new connection to resume an existing session and take over
    notifications/ownership
    - I use UUID to make them not predictable as we don't have auth for now
    - make detached-session expiry authoritative at resume time so teardown
    wins at the TTL boundary
    - reject long-poll process/read calls that get resumed out from under an
    older attachment
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Add output_schema to code mode render (#17210)
    This updates code-mode tool rendering so MCP tools can surface
    structured output types from their `outputSchema`.
    
    What changed:
    - Detect MCP tool-call result wrappers from the output schema shape
    instead of relying on tool-name parsing or provenance flags.
    - Render shared TypeScript aliases once for MCP tool results
    (`CallToolResult`, `ContentBlock`, etc.) so multiple MCP tool
    declarations stay compact.
    - Type `structuredContent` from the tool definition's `outputSchema`
    instead of rendering it as `unknown`.
    - Update the shared MCP aliases to match the MCP draft `CallToolResult`
    schema more closely.
    
    Example:
    - Before: `declare const tools: { mcp__rmcp__echo(args: { env_var?:
    string; message: string; }): Promise<{ _meta?: unknown; content:
    Array<unknown>; isError?: boolean; structuredContent?: unknown; }>; };`
    - After: `declare const tools: { mcp__rmcp__echo(args: { env_var?:
    string; message: string; }): Promise<CallToolResult<{ echo: string; env:
    string | null; }>>; };`
  • Stream Realtime V2 background agent progress (#17264)
    Stream Realtime V2 background agent updates while the background agent
    task is still running, then send the final tool output when it
    completes. User input during an active V2 handoff is acknowledged back
    to realtime as a steering update.
    
    Stack:
    - Depends on #17278 for the background_agent rename.
    - Depends on #17280 for the input task handler refactor.
    
    Coverage:
    - Adds an app-server integration regression test that verifies V2
    progress is sent before the final function-call output.
    
    Validation:
    - just fmt
    - cargo check -p codex-core
    - cargo check -p codex-app-server --tests
    - git diff --check
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • adding parent_thread_id in guardian (#17249)
    ## Summary
    
    This PR adds the parent conversation/session id to the subagent-start
    analytics event for Guardian subagents.
    
    Previously, Guardian sessions were emitted as subagent
    thread-initialized events, but their `parent_thread_id` was serialized
    as `null`. After this change, the `codex_thread_initialized` analytics
    event for a Guardian child session includes the parent user conversation
    id.
  • Extract realtime input task handlers (#17280)
    Refactor the realtime input task select loop into named handlers for
    user text, background agent output, realtime server events, and user
    audio without changing the V2 behavior.
    
    Stack:
    - Depends on #17278 for the background_agent rename.
    
    Validation:
    - just fmt
    - cargo check -p codex-core
    - git diff --check
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Rename Realtime V2 tool to background_agent (#17278)
    Rename the Realtime V2 delegation tool and parser constant to
    background_agent, and update the tool description and fixtures to match.
    
    Validation: just fmt; cargo check -p codex-api; git diff --check
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • 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"
    />
  • Install rustls provider for remote websocket client (#17288)
    Addresses #17283
    
    Problem: `codex --remote wss://...` could panic because
    app-server-client did not install rustls' process-level crypto provider
    before opening TLS websocket connections.
    
    Solution: Add the existing rustls provider utility dependency and
    install it before the remote websocket connect.
  • Emit live hook prompts before raw-event filtering (#17189)
    # What
    Project raw Stop-hook prompt response items into typed v2 hookPrompt
    item-completed notifications before applying the raw-response-event
    filter. Keep ordinary raw response items filtered for normal
    subscribers; only the existing hookPrompt bridge runs on the filtered
    raw-item path.
    
    # Why
    Blocked Stop hooks record their continuation instruction as a raw
    model-history user item. Normal v2 desktop subscribers do not opt into
    raw response events, so the app-server listener filtered that raw item
    before the existing hookPrompt translator could emit the typed live
    item/completed notification. As a result, the hook-prompt bubble only
    appeared after thread history was reloaded.
  • preserve search results order in tool_search_output (#17263)
    we used to alpha-sort tool search results because we were using
    `BTreeMap`, which threw away the actual search result ordering.
    
    Now we use a vec to preserve it.
    
    ### Tests
    Updated tests
  • fix: support split carveouts in windows elevated sandbox (#14568)
    ## Summary
    - preserve legacy Windows elevated sandbox behavior for existing
    policies
    - add elevated-only support for split filesystem policies that can be
    represented as readable-root overrides, writable-root overrides, and
    extra deny-write carveouts
    - resolve those elevated filesystem overrides during sandbox transform
    and thread them through setup and policy refresh
    - keep failing closed for explicit unreadable (`none`) carveouts and
    reopened writable descendants under read-only carveouts
    - for explicit read-only-under-writable-root carveouts, materialize
    missing carveout directories during elevated setup before applying the
    deny-write ACL
    - document the elevated vs restricted-token support split in the core
    README
    
    ## Example
    Given a split filesystem policy like:
    
    ```toml
    ":root" = "read"
    ":cwd" = "write"
    "./docs" = "read"
    "C:/scratch" = "write"
    ```
    
    the elevated backend now provisions the readable-root overrides,
    writable-root overrides, and extra deny-write carveouts during setup and
    refresh instead of collapsing back to the legacy workspace-only shape.
    
    If a read-only carveout under a writable root is missing at setup time,
    elevated setup creates that carveout as an empty directory before
    applying its deny-write ACE; otherwise the sandboxed command could
    create it later and bypass the carveout. This is only for explicit
    policy carveouts. Best-effort workspace protections like `.codex/` and
    `.agents/` still skip missing directories.
    
    A policy like:
    
    ```toml
    "/workspace" = "write"
    "/workspace/docs" = "read"
    "/workspace/docs/tmp" = "write"
    ```
    
    still fails closed, because the elevated backend does not reopen
    writable descendants under read-only carveouts yet.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Stop Realtime V2 response.done delegation (#17267)
    Stop parsing Realtime V2 response completion as a Codex handoff;
    delegation stays tied to item completion.\n\nValidation: just fmt; git
    diff --check
    
    Co-authored-by: Codex <noreply@openai.com>
  • Omit empty app-server instruction overrides (#17258)
    ## Summary
    - omit serialized Responses instructions when an app-server base
    instruction override is empty
    - skip empty developer instruction messages and add v2 coverage for the
    empty-override request shape
    
    ## Validation
    - just fmt
    - git diff --check
  • feat(tui): Ctrl+O copy hotkey and harden copy-as-markdown behavior (#16966)
    ## TL;DR
    
    - New `Ctrl+O` shortcut on top of the existing `/copy` command, allowing
    users to copy the latest agent response without having to cancel a plan
    or type `/copy`
    - Copy server clipboard to the client over SSH (OSC 52)
    - Fixes linux copy behavior: a clipboard handle has to be kept alive
    while the paste happens for the contents to be preserved
    - Uses arboard as primary mechanism on Windows, falling back to
    PowerShell copy clipboard function
    - Works with resumes, rolling back during a session, etc.
    
    Tested on macOS, Linux/X11, Windows WSL2, Windows cmd.exe, Windows
    PowerShell, Windows VSCode PowerShell, Windows VSCode WSL2, SSH (macOS
    -> macOS).
    
    ## Problem
    
    The TUI's `/copy` command was fragile. It relied on a single
    `last_copyable_output` field that was bluntly cleared on every rollback
    and thread reconfiguration, making copied content unavailable after
    common operations like backtracking. It also had no keyboard shortcut,
    requiring users to type `/copy` each time. The previous clipboard
    backend mixed platform selection policy with low-level I/O in a way that
    was hard to test, and it did not keep the Linux clipboard owner alive —
    meaning pasted content could vanish once the process that wrote it
    dropped its `arboard::Clipboard`.
    
    This addresses the text-copy failure modes reported in #12836, #15452,
    and #15663: native Linux clipboard access failing in remote or
    unreachable-display environments, copy state going blank even after
    visible assistant output, and local Linux X11 reporting success while
    leaving the clipboard empty.
    
    ## Shortcut rationale
    
    The copy hotkey is `Ctrl+O` rather than `Alt+C` because Alt/Option
    combinations are not delivered consistently by macOS terminal emulators.
    Terminal.app and iTerm2 can treat Option as text input or as a
    configurable Meta/Esc prefix, and Option+C may be consumed or
    transformed before the TUI sees an `Alt+C` key event. `Ctrl+O` is a
    stable control-key chord in Terminal.app, iTerm2, SSH, and the existing
    cross-platform terminal stack.
    
    ## Mental model
    
    Agent responses are now tracked as a bounded, ordinal-indexed history
    (`agent_turn_markdowns: Vec<AgentTurnMarkdown>`) rather than a single
    nullable string. Each completed agent turn appends an entry keyed by its
    ordinal (the number of user turns seen so far). Rollbacks pop entries
    whose ordinal exceeds the remaining turn count, then use the visible
    transcript cells as a best-effort fallback if the ordinal history no
    longer has a surviving entry. This means `/copy` and `Ctrl+O` reflect
    the most recent surviving agent response after a backtrack, instead of
    going blank.
    
    The clipboard backend was rewritten as `clipboard_copy.rs` with a
    strategy-injection design: `copy_to_clipboard_with` accepts closures for
    the OSC 52, arboard, and WSL PowerShell paths, making the selection
    logic fully unit-testable without touching real clipboards. On Linux,
    the `Clipboard` handle is returned as a `ClipboardLease` stored on
    `ChatWidget`, keeping X11/Wayland clipboard ownership alive for the
    lifetime of the TUI. When native copy fails under WSL, the backend now
    tries the Windows clipboard through PowerShell before falling back to
    OSC 52.
    
    ## Non-goals
    
    - This change does not introduce rich-text (HTML) clipboard support; the
    copied content is raw markdown.
    - It does not add a paste-from-history picker or multi-entry clipboard
    ring.
    - WSL support remains a best-effort fallback, not a new configuration
    surface or guarantee for every terminal/host combination.
    
    ## Tradeoffs
    
    - **Bounded history (256 entries)**: `MAX_AGENT_COPY_HISTORY` caps
    memory. For sessions with thousands of turns this silently drops the
    oldest entries. The cap is generous enough for realistic sessions.
    - **`saw_copy_source_this_turn` flag**: Prevents double-recording when
    both `AgentMessage` and `TurnComplete.last_agent_message` fire for the
    same turn. The flag is reset on turn start and on turn complete,
    creating a narrow window where a race between the two events could
    theoretically skip recording. In practice the protocol delivers them
    sequentially.
    - **Transcript fallback on rollback**:
    `last_agent_markdown_from_transcript` walks the visible transcript cells
    to reconstruct plain text when the ordinal history has been fully
    truncated. This path uses `AgentMessageCell::plain_text()` which joins
    rendered spans, so it reconstructs display text rather than the original
    raw markdown. It keeps visible text copyable after rollback, but
    responses with markdown-specific syntax can diverge from the original
    source.
    - **Clipboard fallback ordering**: SSH still uses OSC 52 exclusively
    because native/PowerShell clipboard access would target the wrong
    machine. Local sessions try native clipboard first, then WSL PowerShell
    when running under WSL, then OSC 52. This adds one process-spawn
    fallback for WSL users but keeps the normal desktop and SSH paths
    simple.
    
    ## Architecture
    
    ```
    chatwidget.rs
    ├── agent_turn_markdowns: Vec<AgentTurnMarkdown>  // ordinal-indexed history
    ├── last_agent_markdown: Option<String>            // always == last entry's markdown
    ├── completed_turn_count: usize                    // incremented when user turns enter history
    ├── saw_copy_source_this_turn: bool                // dedup guard
    ├── clipboard_lease: Option<ClipboardLease>        // keeps Linux clipboard owner alive
    │
    ├── record_agent_markdown(&str)                    // append/update history entry
    ├── truncate_agent_turn_markdowns_to_turn_count()  // rollback support
    ├── copy_last_agent_markdown()                     // public entry point (slash + hotkey)
    └── copy_last_agent_markdown_with(fn)              // testable core
    
    clipboard_copy.rs
    ├── copy_to_clipboard(text) -> Result<Option<ClipboardLease>>
    ├── copy_to_clipboard_with(text, ssh, wsl, osc52_fn, arboard_fn, wsl_fn)
    ├── ClipboardLease { _clipboard on linux }
    ├── arboard_copy(text)          // platform-conditional native clipboard path
    ├── wsl_clipboard_copy(text)    // WSL PowerShell fallback
    ├── osc52_copy(text)            // /dev/tty -> stdout fallback
    ├── SuppressStderr              // macOS stderr redirect guard
    ├── is_ssh_session()
    └── is_wsl_session()
    
    app_backtrack.rs
    ├── last_agent_markdown_from_transcript()  // reconstruct from visible cells
    └── truncate call sites in trim/apply_confirmed_rollback
    ```
    
    ## Observability
    
    - `tracing::warn!` on native clipboard failure before OSC 52 fallback.
    - `tracing::debug!` on `/dev/tty` open/write failure before stdout
    fallback.
    - History cell messages: "Copied last message to clipboard", "Copy
    failed: {error}", "No agent response to copy" appear in the TUI
    transcript.
    
    ## Tests
    
    - `clipboard_copy.rs`: Unit tests cover OSC 52 encoding roundtrip,
    payload size rejection, writer output, SSH-only OSC52 routing, non-WSL
    native-to-OSC52 fallback, WSL native-to-PowerShell fallback, WSL
    PowerShell-to-OSC52 fallback, and all-error reporting via strategy
    injection.
    - `chatwidget/tests/slash_commands.rs`: Updated existing `/copy` tests
    to use `last_agent_markdown_text()` accessor. Added coverage for the
    Linux clipboard lease lifecycle, missing
    `TurnComplete.last_agent_message` fallback through completed assistant
    items, replayed legacy agent messages, stale-output prevention after
    rollback, and the `Ctrl+O` no-output hotkey path.
    - `app_backtrack.rs`: Added
    `agent_group_count_ignores_context_compacted_marker` verifying that
    info-event cells don't inflate the agent group count.
    
    ---------
    
    Co-authored-by: Felipe Coury <felipe.coury@gmail.com>
    Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  • [mcp] Expand tool search to custom MCPs. (#16944)
    - [x] Expand tool search to custom MCPs.
    - [x] Rename several variables/fields to be more generic.
    
    Updated tool & server name lifecycles:
    
    **Raw Identity**
    
    ToolInfo.server_name is raw MCP server name.
    ToolInfo.tool.name is raw MCP tool name.
    MCP calls route back to raw via parse_tool_name() returning
    (tool.server_name, tool.tool.name).
    mcpServerStatus/list now groups by raw server and keys tools by
    Tool.name: mod.rs:599
    App-server just forwards that grouped raw snapshot:
    codex_message_processor.rs:5245
    
    **Callable Names**
    
    On list-tools, we create provisional callable_namespace / callable_name:
    mcp_connection_manager.rs:1556
    For non-app MCP, provisional callable name starts as raw tool name.
    For codex-apps, provisional callable name is sanitized and strips
    connector name/id prefix; namespace includes connector name.
    Then qualify_tools() sanitizes callable namespace + name to ASCII alnum
    / _ only: mcp_tool_names.rs:128
    Note: this is stricter than Responses API. Hyphen is currently replaced
    with _ for code-mode compatibility.
    
    **Collision Handling**
    
    We do initially collapse example-server and example_server to the same
    base.
    Then qualify_tools() detects distinct raw namespace identities behind
    the same sanitized namespace and appends a hash to the callable
    namespace: mcp_tool_names.rs:137
    Same idea for tool-name collisions: hash suffix goes on callable tool
    name.
    Final list_all_tools() map key is callable_namespace + callable_name:
    mcp_connection_manager.rs:769
    
    **Direct Model Tools**
    
    Direct MCP tool declarations use the full qualified sanitized key as the
    Responses function name.
    The raw rmcp Tool is converted but renamed for model exposure.
    
    **Tool Search / Deferred**
    
    Tool search result namespace = final ToolInfo.callable_namespace:
    tool_search.rs:85
    Tool search result nested name = final ToolInfo.callable_name:
    tool_search.rs:86
    Deferred tool handler is registered as "{namespace}:{name}":
    tool_registry_plan.rs:248
    When a function call comes back, core recombines namespace + name, looks
    up the full qualified key, and gets the raw server/tool for MCP
    execution: codex.rs:4353
    
    **Separate Legacy Snapshot**
    
    collect_mcp_snapshot_from_manager_with_detail() still returns a map
    keyed by qualified callable name.
    mcpServerStatus/list no longer uses that; it uses
    McpServerStatusSnapshot, which is raw-inventory shaped.
  • app-server: Use shared receivers for app-server message processors (#17256)
    We do not rely on the mutability here, so express it in the type system.
  • 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.
  • feat: add Codex Apps sediment file remapping (#15197)
    ## Summary
    - bridge Codex Apps tools that declare `_meta["openai/fileParams"]`
    through the OpenAI file upload flow
    - mask those file params in model-visible tool schemas so the model
    provides absolute local file paths instead of raw file payload objects
    - rewrite those local file path arguments client-side into
    `ProvidedFilePayload`-shaped objects before the normal MCP tool call
    
    ## Details
    - applies to scalar and array file params declared in
    `openai/fileParams`
    - Codex uploads local files directly to the backend and uses the
    uploaded file metadata to build the MCP tool arguments locally
    - this PR is input-only
    
    ## Verification
    - `just fmt`
    - `cargo test -p codex-core mcp_tool_call -- --nocapture`
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • [codex] Show ctrl + t hint on truncated exec output in TUI (#17076)
    ## What
    
    Show an inline `ctrl + t to view transcript` hint when exec output is
    truncated in the main TUI chat view.
    
    ## Why
    
    Today, truncated exec output shows `… +N lines`, but it does not tell
    users that the full content is already available through the existing
    transcript overlay. That makes hidden output feel lost instead of
    discoverable.
    
    This change closes that discoverability gap without introducing a new
    interaction model.
    
    Fixes: CLI-5740
    
    ## How
    
    - added an output-specific truncation hint in `ExecCell` rendering
    - applied that hint in both exec-output truncation paths:
      - logical head/tail truncation before wrapping
      - row-budget truncation after wrapping
    - preserved the existing row-budget behavior on narrow terminals by
    reserving space for the longer hint line
    - updated the relevant snapshot and added targeted regression coverage
    
    ## Intentional design decisions
    
    - **Aligned shortcut styling with the visible footer UI**  
    The inline hint uses `ctrl + t`, not `Ctrl+T`, to match the TUI’s
    rendered key-hint style.
    
    - **Kept the noun `transcript`**  
    The product already exposes this flow as the transcript overlay, so the
    hint points at the existing concept instead of inventing a new label.
    
    - **Preserved narrow-terminal behavior**  
    The longer hint text is accounted for in the row-budget truncation path
    so the visible output still respects the existing viewport cap.
    
    - **Did not add the hint to long command truncation**  
    This PR only changes hidden **output** truncation. Long command
    truncation still uses the plain ellipsis form because `ctrl + t` is not
    the same kind of “show hidden output” escape hatch there.
    
    - **Did not widen scope to other truncation surfaces**  
    This does not change MCP/tool-call truncation in `history_cell.rs`, and
    it does not change transcript-overlay behavior itself.
    
    ## Validation
    
    ### Automated
    - `just fmt`
    - `cargo test -p codex-tui`
    
    ### Manual
    - ran `just tui-with-exec-server`
    - executed `!seq 1 200`
    - confirmed the main view showed the new `ctrl + t to view transcript`
    truncation hint
    - pressed `ctrl + t` and confirmed the transcript overlay still exposed
    the full output
    - closed the overlay and returned to the main view
    
    ## Visual proof
    
    Screenshot/video attached in the PR UI showing:
    - the truncated exec output row with the new hint
    - the transcript overlay after `ctrl + t`
  • refactor(proxy): clarify sandbox block messages (#17168)
    ## Summary
    - Replace Codex-branded network-proxy block responses with concise
    reason text
    - Mention sandbox policy for local/private network and deny-policy
    wording
    - Remove “managed” from the proxy-disabled denial detail
  • [codex] add memory extensions (#16276)
    # External (non-OpenAI) Pull Request Requirements
    
    Before opening this Pull Request, please read the dedicated
    "Contributing" markdown file or your PR may be closed:
    https://github.com/openai/codex/blob/main/docs/contributing.md
    
    If your PR conforms to our contribution guidelines, replace this text
    with a detailed and high quality description of your changes.
    
    Include a link to a bug report or enhancement request.
  • chore: merge name and title (#17116)
    Merge title and name concept to leverage the sqlite title column and
    have more efficient queries
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Skip local shell snapshots for remote unified exec (#17217)
    ## Summary
    - detect remote exec-server sessions in the unified-exec runtime
    - bypass the local shell-snapshot bootstrap only for those remote
    sessions
    - preserve existing local snapshot wrapping, PowerShell UTF-8 prefixing,
    sandbox orchestration, and zsh-fork handling
    
    ## Why
    The shell snapshot file is currently captured and stored next to Core.
    If Core wraps a remote command with `. /path/to/local/snapshot`, the
    process starts on the executor and tries to source a path from the
    orchestrator filesystem. This keeps remote commands from receiving that
    known-local path until shell snapshots are captured/restored on the
    executor side.
    
    ## Validation
    - `just fmt`
    - `git diff --check`
    - `cargo test -p codex-core --lib tools::runtimes::tests`
    
    Co-authored-by: Codex <noreply@openai.com>
  • Render statusline context as a meter (#17170)
    Problem: The statusline reported context as an “X% left” value, which
    could be mistaken for quota, and context usage was included in the
    default footer.
    
    Solution: Render configured context status items as a filling context
    meter, preserve `context-used` as a legacy alias while hiding it from
    the setup menu, and remove context from the default statusline. It will
    still be available as an opt-in option for users who want to see it.
    
    <img width="317" height="39" alt="image"
    src="https://github.com/user-attachments/assets/3aeb39bb-f80d-471f-88fe-d55e25b31491"
    />
  • feat: advanced announcements per OS and plans (#17226)
    Support things like
    ```
      [[announcements]]
      content = "custom message"
      from_date = "2026-04-09"
      to_date = "2026-06-01"
      target_app = "cli"
      target_plan_types = ["pro"]
      target_oses = ["macos"]
      version_regex = "..." # add version of the patch
      ```
  • feat: /resume per ID/name (#17222)
    Support `/resume 00000-0000-0000-00000000` from the TUI (equivalent for
    the name)
  • [codex] Defer steering until after sampling the model post-compaction (#17163)
    ## Summary
    - keep pending steered input buffered until the active user prompt has
    received a model response
    - keep steering pending across auto-compact when there is real
    model/tool continuation to resume
    - allow queued steering to follow compaction immediately when the prior
    model response was already final
    - keep pending-input follow-up owned by `run_turn` instead of folding it
    into `SamplingRequestResult`
    - add regression coverage for mid-turn compaction, final-response
    compaction, and compaction triggered before the next request after tool
    output
    
    ## Root Cause
    Steered input was drained at the top of every `run_turn` loop. After
    auto-compaction, the loop continued and immediately appended any pending
    steer after the compact summary, making a queued prompt look like the
    newest task instead of letting the model first resume interrupted
    model/tool work.
    
    ## Implementation Notes
    This patch keeps the follow-up signals separated:
    
    - `SamplingRequestResult.needs_follow_up` means model/tool continuation
    is needed
    - `sess.has_pending_input().await` means queued user steering exists
    - `run_turn` computes the combined loop condition from those two signals
    
    In `run_turn`:
    
    ```rust
    let has_pending_input = sess.has_pending_input().await;
    let needs_follow_up = model_needs_follow_up || has_pending_input;
    ```
    
    After auto-compact we choose whether the next request may drain
    steering:
    
    ```rust
    can_drain_pending_input = !model_needs_follow_up;
    ```
    
    That means:
    
    - model/tool continuation + pending steer: compact -> resume once
    without draining steer
    - completed model answer + pending steer: compact -> drain/send the
    steer immediately
    - fresh user prompt: do not drain steering before the model has answered
    the prompt once
    
    The drain is still only `sess.get_pending_input().await`; when
    `can_drain_pending_input` is false, core uses an empty local vec and
    leaves the steer pending in session state.
    
    ## Validation
    - PASS `cargo test -p codex-core --test all steered_user_input --
    --nocapture`
    - PASS `just fmt`
    - PASS `git diff --check`
    - NOT PASSING HERE `just fix -p codex-core` currently stops before
    linting this change on an unrelated mainline test-build error:
    `core/src/tools/spec_tests.rs` initializes `ToolsConfigParams` without
    `image_generation_tool_auth_allowed`; this PR does not touch that file.
  • make webrtc the default experience (#17188)
    ## Summary
    - make realtime default to the v2 WebRTC path
    - keep partial realtime config tables inheriting
    `RealtimeConfig::default()`
    
    ## Validation
    - CI found a stale config-test expectation; fixed in 974ba51bb3
    - just fmt
    - git diff --check
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Skip update prompts for source builds (#17186)
    Addresses #17166
    
    Problem: Source builds report version 0.0.0, so the TUI update path can
    treat any released Codex version as upgradeable and show startup or
    popup prompts.
    
    Solution: Skip both TUI update prompt entry points when the running CLI
    version is the source-build sentinel 0.0.0.
  • Default realtime startup to v2 model (#17183)
    - Default realtime sessions to v2 and gpt-realtime-1.5 when no override
    is configured.
    - Add Op::RealtimeConversationStart integration coverage and keep
    v1-specific tests explicit.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Add TUI notification condition config (#17175)
    Problem: TUI desktop notifications are hard-gated on terminal focus, so
    terminal/IDE hosts that want in-focus notifications cannot opt in.
    
    Solution: Add a flat `[tui] notification_condition` setting (`unfocused`
    by default, `always` opt-in), carry grouped TUI notification settings
    through runtime config, apply method + condition together in the TUI,
    and regenerate the config schema.
  • 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.
  • Move default realtime prompt into core (#17165)
    - Adds a core-owned realtime backend prompt template and preparation
    path.
    - Makes omitted realtime start prompts use the core default, while null
    or empty prompts intentionally send empty instructions.
    - Covers the core realtime path and app-server v2 path with integration
    coverage.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Fix stale thread-name resume lookups (#16646)
    Addresses #15943
    
    Problem: Name-based resume could stop on a newer session_index entry
    whose rollout was never persisted, shadowing an older saved thread with
    the same name.
    
    Solution: Materialize rollouts before indexing thread names and make
    name lookup skip unresolved entries until it finds a persisted rollout.
  • Support Warp for OSC 9 notifications (#17174)
    Problem: Warp supports OSC 9 notifications, but the TUI's automatic
    notification backend selection did not recognize its
    `TERM_PROGRAM=WarpTerminal` environment value.
    
    Solution: Treat `TERM_PROGRAM=WarpTerminal` as OSC 9-capable when
    choosing the TUI desktop notification backend.
  • Add WebRTC realtime app-server e2e tests (#17093)
    Summary:
    - add app-server WebRTC realtime e2e harness
    - cover v1 handoff and v2 codex tool delegation over sideband
    
    Validation:
    - just fmt
    - git diff --check
    - local tests not run; relying on PR CI
  • Auto-approve MCP server elicitations in Full Access mode (#17164)
    Currently, when a MCP server sends an elicitation to Codex running in
    Full Access (`sandbox_policy: DangerFullAccess` + `approval_policy:
    Never`), the elicitations are auto-cancelled.
    
    This PR updates the automatic handling of MCP elicitations to be
    consistent with other approvals in full-access, where they are
    auto-approved. Because MCP elicitations may actually require user input,
    this mechanism is limited to empty form elicitations.
    
    ## Changeset
    - Add policy helper shared with existing MCP tool call approval
    auto-approve
    - Update `ElicitationRequestManager` to auto-approve elicitations in
    full access when `can_auto_accept_elicitation` is true.
    - Add tests
    
    Co-authored-by: Codex <noreply@openai.com>
  • Update guardian output schema (#17061)
    ## Summary
    - Update guardian output schema to separate risk, authorization,
    outcome, and rationale.
    - Feed guardian rationale into rejection messages.
    - Split the guardian policy into template and tenant-config sections.
    
    ## Validation
    - `cargo test -p codex-core mcp_tool_call`
    - `env -u CODEX_SANDBOX_NETWORK_DISABLED INSTA_UPDATE=always cargo test
    -p codex-core guardian::`
    
    ---------
    
    Co-authored-by: Owen Lin <owen@openai.com>
  • Add top-level exec-server subcommand (#17162)
    ## Summary
    - add a top-level `codex exec-server` subcommand, marked experimental in
    CLI help
    - launch an adjacent or PATH-provided `codex-exec-server`, with a
    source-tree `cargo run -p codex-exec-server --` fallback
    - cover the new subcommand parser path
    
    ## Validation
    - `just fmt`
    - `git diff --check`
    - not run: Rust test suite
    
    Co-authored-by: Codex <noreply@openai.com>
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Attach WebRTC realtime starts to sideband websocket (#17057)
    Summary:
    - parse the realtime call Location header and join that call over the
    direct realtime WebSocket
    - keep WebRTC starts alive on the existing realtime conversation path
    
    Validation:
    - just fmt
    - git diff --check
    - cargo check -p codex-api
    - cargo check -p codex-core --tests
    - local cargo tests not run; relying on PR CI
  • Wire realtime WebRTC native media into Bazel (#17145)
    - Builds codex-realtime-webrtc through the normal Bazel Rust macro so
    native macOS WebRTC sources are included.\n- Shares the macOS -ObjC link
    flag with Bazel targets that can link libwebrtc.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Fix ToolsConfigParams initializer in tool registry test (#17154)
    ## Summary
    - add the missing `image_generation_tool_auth_allowed` field to the new
    tool registry plan test initializer
    
    ## Validation
    - `just fmt`
    - `cargo test -p codex-tools image_generation`
    - `cargo test -p codex-tools --no-run`
  • Fix missing fields (#17149)
    Fix missing `image_generation_tool_auth_allowed` in two locations.