Commit Graph

3115 Commits

  • feat(tui): redesign session picker (#20065)
    ## Why
    
    The resume/fork picker is becoming the main way users recover previous
    work, but the old fixed table made sessions hard to scan once thread
    names, branches, working directories, and timestamps all mattered. This
    redesign makes the picker denser by default, easier to search, and safer
    to inspect before resuming or forking.
    
    <table>
    <tr>
    <td>
    <img width="1660" height="1103" alt="CleanShot 2026-05-03 at 12 34 10"
    src="https://github.com/user-attachments/assets/313ede1d-1da4-4863-acd2-56b3e27e9703"
    />
    </td>
    <td>
    <img width="1662" height="1100" alt="CleanShot 2026-05-03 at 12 34 15"
    src="https://github.com/user-attachments/assets/cfde7d5c-bab0-4994-a807-254e53f344ea"
    />
    </td>
    </tr>
    <tr>
    <td>
    <img width="1664" height="1107" alt="CleanShot 2026-05-03 at 12 39 22"
    src="https://github.com/user-attachments/assets/e1ee58ca-4dc5-4a35-ae0f-47562da3974c"
    />
    </td>
    <td>
    <img width="1662" height="1100" alt="CleanShot 2026-05-03 at 12 35 09"
    src="https://github.com/user-attachments/assets/9c888072-eedf-4f45-985c-0c14df28bcc7"
    />
    </td>
    </tr>
    </table>
    
    ## What Changed
    
    - Replaces the old session table with responsive session rows that
    prioritize the session name or preview, then show timestamp, cwd, and
    branch metadata.
    - Makes dense view the default while keeping comfortable view available
    through `Ctrl+O`.
    - Persists the picker view preference in `[tui].session_picker_view`,
    including active profile-scoped config.
    - Adds sort/filter controls for updated time, created time, cwd, and all
    sessions.
    - Expands search matching across session name, preview, thread id,
    branch, and cwd.
    - Makes `Esc` safer in search mode: it clears an active query before
    starting a new session.
    - Adds lazy transcript inspection:
      - `Space` expands recent transcript context inline.
      - `Ctrl+T` opens a transcript overlay.
      - raw reasoning visibility follows `show_raw_agent_reasoning`.
    - Keeps remote cwd filtering server-side for remote app-server sessions
    so local path normalization does not incorrectly hide remote results.
    - Updates snapshots and config schema for the new picker states and
    config option.
    
    ## How to Test
    
    1. Start Codex in a repo with several saved sessions.
    2. Press `Ctrl+R` / resume picker entry point.
    3. Confirm the picker opens in dense mode and shows session name or
    preview, timestamp, cwd, and branch metadata.
    4. Press `Ctrl+O` and confirm it switches between dense and comfortable
    views.
    5. Restart Codex and confirm the selected view persists.
    6. Type a query that matches a branch, cwd, thread id, or session name;
    confirm matching sessions appear.
    7. Press `Esc` while the query is non-empty and confirm it clears search
    instead of starting a new session.
    8. Select a session and press `Space`; confirm recent transcript context
    expands inline.
    9. Press `Ctrl+T`; confirm the transcript overlay opens and respects
    raw-reasoning visibility settings.
    
    Targeted tests:
    - `cargo test -p codex-tui resume_picker --no-fail-fast`
    - `cargo test -p codex-core
    runtime_config_resolves_session_picker_view_default_and_override`
    - `cargo test -p codex-core profile_tui_rejects_unsupported_settings`
    - `cargo check -p codex-thread-manager-sample`
    - `cargo insta pending-snapshots`
  • hook trust metadata and enforcement (#20321)
    # Why
    
    We want shared hook trust that both the app and the TUI can build on,
    but the metadata is only useful if runtime behavior agrees with it. This
    PR adds a single backend trust model for hooks so unmanaged hooks cannot
    run until the current definition has been reviewed, while managed hooks
    remain runnable and non-configurable.
    
    # What
    
    - persist `trusted_hash` alongside hook state in `config.toml`
    - expose `currentHash` and derived `trustStatus` through `hooks/list`
    - derive trust from normalized hook definitions so equivalent hooks from
    `config.toml` and `hooks.json` share the same trust identity
    - gate unmanaged hooks on trust before they enter the runnable handler
    set
    
    # Reviewer Notes
    
    - key file to review is `codex-rs/hooks/src/engine/discovery.rs`
    - the only **core** change is schema related
  • Route process tools to selected environments (#20647)
    ## Why
    When a turn exposes multiple selected environments, shell-style tools
    need a model-facing way to identify the intended target environment and
    handlers need to resolve that target before parsing cwd-relative
    permission fields or launching processes.
    
    This PR scopes that rollout to process tools. Filesystem-oriented tools
    such as `apply_patch`, `view_image`, and `list_dir` are intentionally
    left for follow-up slices.
    
    ## What Changed
    - Adds an `include_environment_id` option to shell-style tool schema
    builders.
    - Exposes optional `environment_id` on `shell`, `shell_command`, and
    `exec_command` only when `ToolEnvironmentMode::Multiple` is active.
    - Adds a shared handler helper that parses `environment_id` and
    `workdir` from JSON function-call arguments and returns the selected
    `Environment` plus effective absolute cwd.
    - Uses that helper in `shell`, `shell_command`, and `exec_command`
    handling so process execution uses the selected environment filesystem
    and cwd.
    - Changes `ExecCommandRequest` to carry a required resolved `cwd`,
    removing the process-manager fallback to the primary turn cwd for new
    exec commands.
    - Leaves `write_stdin` unchanged because it targets an existing process
    id, not a new environment.
    
    ## Testing
    - Added unit coverage for process-tool schema exposure, selected
    environment resolution, primary fallback, no-environment handling,
    unknown environment ids, and resolving cwd-relative permission paths
    against the selected environment cwd.
    - Added a remote-suite e2e coverage case for `exec_command` routing
    across explicit zero environments, one local environment, and
    local+remote environments.
    - Ran `just fmt` and `git diff --check`.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • feat(tui): add raw scrollback mode (#20819)
    ## Why
    
    Granular copy is particularly difficult with the current output. Part of
    it was solved with the introduction of the `/copy` command but when you
    only need to copy parts of a response, you still encounter some issues:
    
    - When you copy a paragraph, the result is a sequence of separate lines
    instead of one correctly joined paragraph.
    - When a word wraps, part of it stays on the original line and the rest
    appears at the start of the next line.
    - When you copy a long command, extra line breaks are often inserted,
    and command arguments can be split across multiple lines.
    
    
    https://github.com/user-attachments/assets/0ef85c84-9363-4aad-b43a-15fce062a443
    
    ## Solution
    
    Now that we own the scrollback and we re-create it when we resize, we
    have the opportunity of toggling between the raw text and the rich text
    we see today.
    
    - Add TUI raw scrollback mode with `tui.raw_output_mode`, `/raw
    [on|off]`, and the configurable `tui.keymap.global.toggle_raw_output`
    action.
    - Render transcript cells through rich/raw-aware paths so raw mode
    preserves source text and lets the terminal soft-wrap selection-friendly
    output.
    - Bind raw-mode toggle to `alt-r` by default, with the keybinding path
    toggling silently while `/raw` continues to emit confirmation messages.
    
    ## Related Issues
    
    Likely addressed by raw mode:
    
    - #12200: clean copy for multiline and soft-wrapped output. Raw mode
    removes Codex-inserted wrapping/indentation and lets the terminal
    soft-wrap logical lines.
    - #9252: command suggestions gain unwanted leading spaces when copied.
    Raw mode renders transcript text without the rich-mode left
    padding/gutter.
    - #8258: prompt output is hard to copy because of leading indentation.
    Raw mode renders user/source-backed transcript text without that
    decorative indentation.
    
    Partially or conditionally addressed:
    
    - #2880: copy/export message as Markdown. Raw mode exposes raw Markdown
    for terminal selection, but this PR does not add a dedicated
    export/copy-message command.
    - #19820: mouse drag selection + copy in the TUI. Raw mode improves
    terminal-native selection of output/history text, but this PR does not
    implement in-TUI mouse selection, highlighting, auto-copy, or composer
    selection.
    - #18979: copied content is divided into two parts. This should improve
    cases caused by Codex-inserted wraps/padding in rendered output; if the
    report is about pasting into the composer/input path, that remains
    outside this PR.
    
    ## Validation
    
    - `just write-config-schema`
    - `just fmt`
    - `cargo test -p codex-config`
    - `cargo test -p codex-tui`
    - `just fix -p codex-tui`
    - `just argument-comment-lint`
    - `cargo test -p codex-tui
    raw_output_mode_can_change_without_inserting_notice -- --nocapture`
    - `cargo test -p codex-tui
    raw_slash_command_toggles_and_accepts_on_off_args -- --nocapture`
    - `cargo test -p codex-tui raw_output_toggle -- --nocapture`
    - `git diff --check`
    - `cargo insta pending-snapshots`
  • revert legacy notify deprecation (#21152)
    # Why
    
    Revert #20524 for now because the computer use plugin has not migrated
    off legacy `notify` yet. Keeping the deprecation in place today would
    show users a warning before the plugin path is ready to move, so this
    rolls the change back until that migration is complete.
    
    # What
    
    - revert the legacy `notify` deprecation change from #20524
    - restore the prior `notify` behavior and remove the temporary
    deprecation metrics/docs from that change
    
    Once the computer use plugin has migrated, we can land the same
    deprecation again.
  • Support PreToolUse additionalContext (#20692)
    # Why
    
    `PreToolUse` already exposes `hookSpecificOutput.additionalContext` in
    the generated hook schema, but the runtime still rejected it as
    unsupported. That leaves `PreToolUse` out of step with the other
    context-injecting hooks and prevents hook authors from attaching
    model-visible guidance to a pending tool call before it runs.
    
    # What
    
    - Parse `PreToolUse.additionalContext` and carry it through the hook
    event pipeline.
    - Record `PreToolUse` context at the hook boundary so successful context
    is preserved for both allowed and blocked calls without widening the
    tool registry surface.
    - Preserve existing deny behavior when context is combined with either
    `permissionDecision: "deny"` or the legacy `decision: "block"` shape.
  • Add goal lifecycle metrics (#20799)
    ## Why
    
    Adding goal metrics makes it possible to track how often goals are
    created, completed, and stopped by budget limits, plus the final token
    and wall-clock usage for terminal outcomes.
    
    ## What Changed
    
    - Added OpenTelemetry metric constants for goal lifecycle tracking:
    - `codex.goal.created`: increments each time a new persisted goal is
    created or an existing goal is replaced with a new objective.
    - `codex.goal.completed`: increments when a goal transitions to
    `complete`.
    - `codex.goal.budget_limited`: increments when a goal transitions to
    `budget_limited` because its token budget has been reached.
    - `codex.goal.token_count`: records the final persisted token count when
    a goal transitions to `complete` or `budget_limited`.
    - `codex.goal.duration_s`: records the final persisted elapsed
    wall-clock time, in seconds, when a goal transitions to `complete` or
    `budget_limited`.
    - Emitted creation metrics when a goal is created or replaced.
    - Emitted terminal outcome counters and final usage histograms when a
    goal transitions to `complete` or `budget_limited`, avoiding
    double-counting later in-flight accounting for already budget-limited
    goals.
    - Added focused `codex-core` tests for create/complete metrics and
    one-time budget-limit metrics.
  • tools: remove unused experimental list_dir tool (#21170)
    ## Why
    `list_dir` still carries a full spec/handler/test path, but nothing in
    the current model catalog advertises it via
    `experimental_supported_tools`. That leaves us maintaining an
    environment-backed tool surface that is effectively unused.
    
    ## What changed
    - delete the `list_dir` handler and its tests from `codex-core`
    - remove the `list_dir` spec builder, handler kind, and registry wiring
    from `codex-tools`
    - clean up the remaining internal README and registry tests so they no
    longer mention the removed tool
  • 1- Add model service tiers metadata (#20969)
    ## Why
    
    The model list needs to carry display-ready service tier metadata so
    clients can render tier choices with stable IDs, names, and
    descriptions. A raw speed-tier string list is not enough for richer UI
    copy or future tier labels.
    
    ## What changed
    
    - Added `ModelServiceTier` to shared model metadata with string `id`,
    `name`, and `description` fields.
    - Added `service_tiers` to `ModelInfo` and `ModelPreset`, preserving
    empty defaults for older cached model payloads.
    - Exposed `serviceTiers` on app-server v2 `Model` responses and threaded
    it through TUI app-server model conversion.
    - Marked legacy `additional_speed_tiers` / `additionalSpeedTiers`
    metadata as deprecated in source and generated schema output.
    - Regenerated app-server protocol JSON schema and TypeScript fixtures,
    including `ModelServiceTier.ts`.
    
    ## Verification
    
    - Ran `just write-app-server-schema`.
    - Did not run local tests per repo instruction; relying on PR CI.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Spill large hook outputs from context (#21069)
    ## Why
    
    Large hook outputs can enter model-visible context through hook-specific
    paths such as `additionalContext` and `Stop` continuation prompts.
    Without a dedicated cap, one hook can inject a large blob directly into
    conversation history instead of leaving a bounded preview for the model
    and preserving the full text elsewhere.
    
    ## What
    
    - spill hook text once it exceeds a fixed `2_500`-token budget,
    preserving the full output on disk and leaving a head/tail preview plus
    saved path in context
    - add shared hook-output spilling under
    `CODEX_HOME/hook_outputs/<thread_id>/<uuid>.txt`
    - apply the cap to both `additionalContext`, `feedback_message`, and
    `Stop` continuation fragments
  • codex: migrate (more) app-server thread history reads to ThreadStore (#20575)
    Migrate token usage replay, rollback responses, and detached review
    setup (a special case of forking) to be served from ThreadStore reads
    rather direct rollout files.
    
    - replay restored token usage from already-loaded `RolloutItem` history
    instead of reopening `Thread.path`
    - rebuild rollback responses from loaded `ThreadStore` snapshots and
    history
    - start detached reviews from store-backed parent history and stored
    review-thread metadata
    - remove obsolete app-server rollout-summary helper code that became
    dead after the store-backed migration
    - preserve response/notification ordering for resume, fork, rollback,
    and detached review flows
    - add integration test coverage for the affected paths
  • Add plugin ID to skill analytics (#20923)
    ## Summary
    - thread plugin skill roots through the skills loader with their plugin
    ID
    - store plugin ID on loaded skill metadata for plugin-provided skills
    - include plugin ID on skill invocation analytics events
    
    ## Test plan
    - cargo check -p codex-core-skills
    - cargo check -p codex-core -p codex-core-plugins -p codex-analytics
    - cargo check -p codex-tui
    - cargo check -p codex-plugin -p codex-core -p codex-core-plugins -p
    codex-analytics
    - cargo check -p codex-app-server
    - cargo test -p codex-analytics
    - HOME=/private/tmp/codex-empty-home cargo test -p codex-core-skills
    - just fix -p codex-core-skills
    - just fix -p codex-analytics
    - just fix -p codex-core-plugins
    - just fix -p codex-core
    - just fmt
    - git diff --check
  • codex: route metadata updates through ThreadStore (#20576)
    - Route `thread/metadata/update` through
    `ThreadStore::update_thread_metadata`.
    - Add `LocalThreadStore` git metadata patch support for set, partial
    update, and clear semantics.
    - Add some unit tests for the new thread store code
    - Remove a lot of dead code/tests!
  • [codex-analytics] add item lifecycle timing (#20514)
    ## Why
    
    Tool families already disagree on what their existing `duration` fields
    mean, so lifecycle latency should live on the shared item envelope
    instead of being inferred from per-tool execution fields. Carrying that
    envelope through app-server notifications gives downstream consumers one
    reusable timing signal without pretending every tool has the same
    execution semantics.
    
    ## What changed
    
    - Adds `started_at_ms` to core `ItemStartedEvent` values and
    `completed_at_ms` to core `ItemCompletedEvent` values.
    - Populates those timestamps in the shared session lifecycle emitters,
    so protocol-native items get timing without each producer tracking its
    own clock state.
    - Exposes `startedAtMs` on app-server `item/started` notifications and
    `completedAtMs` on `item/completed` notifications.
    - Maps the lifecycle timestamps through the app-server boundary while
    leaving legacy-converted notifications nullable when no lifecycle
    timestamp exists.
    - Regenerates the app-server JSON schema and TypeScript fixtures for the
    notification-envelope change and updates downstream fixtures that
    construct those notifications directly.
    - Extends the existing web-search and image-generation integration flows
    to assert the new lifecycle timestamps on the native item events.
    
    ## Verification
    
    - `cargo check -p codex-protocol -p codex-core -p
    codex-app-server-protocol -p codex-app-server -p codex-tui -p codex-exec
    -p codex-app-server-client`
    - `cargo test -p codex-core --test all web_search_item_is_emitted`
    - `cargo test -p codex-core --test all
    image_generation_call_event_is_emitted`
    - `cargo test -p codex-app-server-protocol`
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/20514).
    * #18748
    * #18747
    * #17090
    * #17089
    * __->__ #20514
  • Make realtime sideband startup async (#20715)
    ## Summary
    
    Moves the WebRTC realtime sideband websocket join out of the voice start
    critical path. Call creation still posts the SDP offer and session
    config synchronously so the client gets the SDP answer, but the sideband
    websocket now connects in the input task async and doesn't block
    conversation state installation.
    
    This lets the normal realtime input channels buffer text, handoff
    output, and audio while the WebRTC sideband websocket is connecting. If
    the sideband join fails while the conversation is still active, the task
    sends a RealtimeEvent::Error through the existing events_tx / fanout
    path.
    
    To rephrase this:
    * No longer blocked on sideband: the client can receive the SDP answer
    earlier, set up the WebRTC peer connection, and let the media leg
    progress while the sideband websocket joins.
    * Still blocked on sideband: queued text, handoff output, and sideband
    server events cannot flow until connect_webrtc_sideband(...).await
    finishes and then run_realtime_input_task(...) starts
    
    ## Validation
    
    - `env CODEX_SKIP_VENDORED_BWRAP=1 cargo test --manifest-path
    codex-rs/Cargo.toml -p codex-core --test all
    conversation_webrtc_start_posts_generated_session`
    
    `CODEX_SKIP_VENDORED_BWRAP=1` is needed in this local environment
    because `libcap.pc` is not installed for the vendored bubblewrap build.
    
    ## Testing
    I tested this locally by running `cargo run -p codex-cli --bin codex --
    --enable realtime_conversation` and invoking `/realtime`. Then, we get
    logs emitted in `~/.codex/log/codex-tui.log`.
    
    ### Before the Change
    Logging commit
    (https://github.com/openai/codex/commit/c0299e6edf1222fa0c43c1796e4811976c26fecd)
    ```
    2026-05-04T16:06:09.251956Z  INFO session_loop{thread_id=019df3b9-e3d8-7271-b13a-b880119aa4c2}:submission_dispatch{otel.name="op.dispatch.realtime_conversation_start" submission.id="019df3bd-65df-7ee2-8125-1d6701fe39d2" codex.op="realtime_conversation_start"}: codex_core::realtime_conversation: starting realtime conversation
    2026-05-04T16:06:09.251980Z  INFO session_loop{thread_id=019df3b9-e3d8-7271-b13a-b880119aa4c2}:submission_dispatch{otel.name="op.dispatch.realtime_conversation_start" submission.id="019df3bd-65df-7ee2-8125-1d6701fe39d2" codex.op="realtime_conversation_start"}: codex_core::realtime_conversation: creating realtime call transport="webrtc"
    2026-05-04T16:06:10.365722Z  INFO session_loop{thread_id=019df3b9-e3d8-7271-b13a-b880119aa4c2}:submission_dispatch{otel.name="op.dispatch.realtime_conversation_start" submission.id="019df3bd-65df-7ee2-8125-1d6701fe39d2" codex.op="realtime_conversation_start"}: codex_core::realtime_conversation: realtime call created; sdp answer ready transport="webrtc" call_id=rtc_u0_Dbq65nhak5eLjQZ73yhAy elapsed_ms=1113 total_elapsed_ms=1113
    2026-05-04T16:06:10.365843Z  INFO session_loop{thread_id=019df3b9-e3d8-7271-b13a-b880119aa4c2}:submission_dispatch{otel.name="op.dispatch.realtime_conversation_start" submission.id="019df3bd-65df-7ee2-8125-1d6701fe39d2" codex.op="realtime_conversation_start"}: codex_core::realtime_conversation: connecting realtime sideband websocket call_id=rtc_u0_Dbq65nhak5eLjQZ73yhAy
    2026-05-04T16:06:10.784528Z  INFO session_loop{thread_id=019df3b9-e3d8-7271-b13a-b880119aa4c2}:submission_dispatch{otel.name="op.dispatch.realtime_conversation_start" submission.id="019df3bd-65df-7ee2-8125-1d6701fe39d2" codex.op="realtime_conversation_start"}: codex_core::realtime_conversation: connected realtime sideband websocket call_id=rtc_u0_Dbq65nhak5eLjQZ73yhAy elapsed_ms=418 total_elapsed_ms=1532
    2026-05-04T16:06:10.784665Z  INFO session_loop{thread_id=019df3b9-e3d8-7271-b13a-b880119aa4c2}:submission_dispatch{otel.name="op.dispatch.realtime_conversation_start" submission.id="019df3bd-65df-7ee2-8125-1d6701fe39d2" codex.op="realtime_conversation_start"}: codex_core::realtime_conversation: realtime conversation started
    ```
    
    ### After the Change
    Logging commit
    (https://github.com/openai/codex/commit/c8b00ac21adf4f8dd1fe3a81403a2bb6183fe13b)
    ```
    2026-05-04T15:41:24.080363Z  INFO ... codex_core::realtime_conversation: starting realtime conversation
    2026-05-04T15:41:24.080434Z  INFO ... codex_core::realtime_conversation: creating realtime call transport="webrtc"
    2026-05-04T15:41:25.106906Z  INFO ... codex_core::realtime_conversation: realtime call created; sdp answer ready transport="webrtc" call_id=rtc_u0_Dbpi8nhak5eLjQZ73yhAy elapsed_ms=1026 total_elapsed_ms=1026
    2026-05-04T15:41:25.107067Z  INFO ... codex_core::realtime_conversation: spawned realtime sideband connection task transport="webrtc" total_elapsed_ms=1026
    2026-05-04T15:41:25.107160Z  INFO ... codex_core::realtime_conversation: realtime conversation started
    2026-05-04T15:41:25.107185Z  INFO codex_core::realtime_conversation: connecting realtime sideband websocket call_id=rtc_u0_Dbpi8nhak5eLjQZ73yhAy
    2026-05-04T15:41:25.107352Z  INFO ... codex_core::realtime_conversation: sent realtime sdp answer to client
    2026-05-04T15:41:26.076685Z  INFO codex_core::realtime_conversation: connected realtime sideband websocket call_id=rtc_u0_Dbpi8nhak5eLjQZ73yhAy elapsed_ms=969 total_elapsed_ms=1996
    2026-05-04T15:41:26.573893Z  INFO codex_core::realtime_conversation: realtime session updated realtime_session_id=sess_u0_Dbpi8nhak5eLjQZ73yhAy
    2026-05-04T15:41:26.573970Z  INFO codex_core::realtime_conversation: received realtime conversation event event=SessionUpdated { ... }
    ```
    
    ### Conclusion
    Here we see that we saved about a half a second in conversation startup
    (1532ms -> 969ms). This also checks out with my sanity tests; I was
    seeing at most a second of saving.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Add reasoning effort to turn tracing spans (#20060)
    Why
    #19432 added token usage to the turn and response spans. This follow-up
    adds the configured reasoning effort so performance traces can be
    filtered by model effort.
    
    [example
    trace](https://openai.datadoghq.com/apm/trace/1ff708a87159ff4898bdc8bd6091ec18?graphType=waterfall&shouldShowLegend=true&spanID=6596351544047485652&traceQuery=)
    <img width="533" height="434" alt="Screenshot 2026-04-28 at 3 52 12 PM"
    src="https://github.com/user-attachments/assets/77ef32fc-d7cd-4eec-87b4-26c6798f1af8"
    />
    
    
    What Changed
    - Adds `codex.turn.reasoning_effort` to the turn span.
    - Adds `codex.request.reasoning_effort` to `handle_responses`.
    - Extends the span test to cover explicit `high` effort with token
    usage.
    
    Testing
    - `cargo test -p codex-core
    turn_and_completed_response_spans_record_token_usage`
    - `cargo test -p codex-otel`
    - `just fmt`
    - `just fix -p codex-core`
    - `just fix -p codex-otel`
  • core: fix apply_patch request permissions test (#21060)
    ## Why
    
    The Bazel test coverage change exposed
    `approved_folder_write_request_permissions_unblocks_later_apply_patch`,
    and `rust-ci-full.yml` showed the same test failing on `main` on macOS.
    There were two separate classes of problems here.
    
    ### Clean CI failure
    
    The test emits an `apply_patch` tool call, but its config did not enable
    the `apply_patch` tool, so the mocked response completed without an
    `apply-patch-call` output. After enabling the tool, the same path also
    needs the aggregate `codex-core` test binary to dispatch
    `--codex-run-as-fs-helper`; sandboxed `apply_patch` uses that helper
    under macOS Seatbelt.
    
    The test now also canonicalizes the temporary patch target before
    building the patch payload so the path matches normalized grants on
    macOS, where `/var` paths often normalize to `/private/var`.
    
    ### Local/enterprise config isolation
    
    The core test harness now builds its default test config with managed
    config disabled, so host-managed enterprise config cannot alter these
    tests. The request-permissions turns in this test also explicitly use
    the user reviewer path, keeping the assertions focused on
    `request_permissions` behavior rather than reviewer defaults from the
    host.
    
    ## What Changed
    
    - Enable `apply_patch` in
    `approved_folder_write_request_permissions_unblocks_later_apply_patch`.
    - Teach the core integration test binary to dispatch
    `CODEX_FS_HELPER_ARG1`, matching the existing apply-patch and
    linux-sandbox dispatch paths.
    - Canonicalize the tempdir-backed patch target before creating the
    patch.
    - Ignore managed config in default core test configs and explicitly pin
    this test to `ApprovalsReviewer::User`.
    
    ## Verification
    
    Run outside the Codex app sandbox because these macOS tests
    intentionally spawn Seatbelt:
    
    - `cargo test -p codex-core
    approved_folder_write_request_permissions_unblocks_later_apply_patch`
    - `cargo test -p codex-core
    approved_folder_write_request_permissions_unblocks_later_exec_without_sandbox_args`
  • core: preserve last model ids in feedback tags (#21026)
    ## Why
    
    Feedback reports do not currently surface a direct pointer to the last
    model call, so investigations may require searching through many
    requests in a session to find the bad response. Preserve the last
    model-side IDs at response-stream time so immediate feedback reports
    carry that breadcrumb.
    
    ## What changed
    
    - Record `last_model_request_id` when a Responses stream exposes an
    upstream request ID.
    - Record `last_model_response_id` when the model response completes.
    - Add unit coverage for the emitted feedback tags.
    
    ## Verification
    
    - `cargo test -p codex-core
    client::tests::response_stream_records_last_model_feedback_ids`
  • Use MCP server instructions in deferred namespace descriptions (#21053)
    ## Why
    
    MCP servers can provide `instructions` that explain what their tools are
    for. Directly exposed MCP namespaces already use those instructions when
    a connector description is not available, but deferred `tool_search`
    results did not preserve that fallback. The direct path falls back from
    connector metadata to server instructions, while the deferred path only
    carried `connector_description` and otherwise fell back to generic
    namespace text.
    
    That meant a plain MCP server could provide useful model-facing guidance
    and still appear as `Tools in the X namespace.` whenever it was
    discovered lazily through `tool_search`.
    
    ## What changed
    
    - Store one model-facing `namespace_description` on `ToolInfo`, using
    connector descriptions for connector-backed tools and server
    instructions for plain MCP servers.
    - Thread that namespace description through the `tool_search` source
    list, search indexing, and returned namespace metadata.
    - Add an end-to-end regression test for deferred non-app MCP search
    results exposing server instructions as the namespace description.
    
    ## Verification
    
    - `cargo test -p codex-tools
    search_tool_description_lists_each_mcp_source_once --lib`
    - `cargo test -p codex-core --test all
    tool_search_uses_non_app_mcp_server_instructions_as_namespace_description`
  • feat(tui): improve TUI keymap coverage (#20798)
    ## Summary
    - normalize terminal-emitted C0 control characters through configurable
    editor keymaps, covering raw control-key fallbacks like
    Shift+Enter-as-LF in terminals from #20555 and #20898, plus part of the
    modified-Enter behavior in #20580
    - add default-unbound keymap actions for toggling Fast mode and killing
    the current composer line, giving #20698 users a configurable zsh-style
    Ctrl+U option without changing the existing default Ctrl+U behavior
    - wire the new actions through gated /keymap picker entries, schema
    generation, and snapshot coverage
    
    Fixes #20555.
    Fixes #20898.
    
    ## Testing
    - just write-config-schema
    - just fmt
    - cargo test -p codex-config
    - cargo test -p codex-tui keymap::tests
    - cargo test -p codex-tui bottom_pane::textarea::tests
    - cargo test -p codex-tui keymap_setup::tests
    - cargo insta pending-snapshots
    - just fix -p codex-tui
    - git diff --check
    - just argument-comment-lint
  • state: pass state db handles through consumers (#20561)
    ## Why
    
    SQLite state was still being opened from consumer paths, including lazy
    `OnceCell`-backed thread-store call sites. That let one process
    construct multiple state DB connections for the same Codex home, which
    makes SQLite lock contention and `database is locked` failures much
    easier to hit.
    
    State DB lifetime should be chosen by main-like entrypoints and tests,
    then passed through explicitly. Consumers should use the supplied
    `Option<StateDbHandle>` or `StateDbHandle` and keep their existing
    filesystem fallback or error behavior when no handle is available.
    
    The startup path also needs to keep the rollout crate in charge of
    SQLite state initialization. Opening `codex_state::StateRuntime`
    directly bypasses rollout metadata backfill, so entrypoints should
    initialize through `codex_rollout::state_db` and receive a handle only
    after required rollout backfills have completed.
    
    ## What Changed
    
    - Initialize the state DB in main-like entrypoints for CLI, TUI,
    app-server, exec, MCP server, and the thread-manager sample.
    - Pass `Option<StateDbHandle>` through `ThreadManager`,
    `LocalThreadStore`, app-server processors, TUI app wiring, rollout
    listing/recording, personality migration, shell snapshot cleanup,
    session-name lookup, and memory/device-key consumers.
    - Remove the lazy local state DB wrapper from the thread store so
    non-test consumers use only the supplied handle or their existing
    fallback path.
    - Make `codex_rollout::state_db::init` the local state startup path: it
    opens/migrates SQLite, runs rollout metadata backfill when needed, waits
    for concurrent backfill workers up to a bounded timeout, verifies
    completion, and then returns the initialized handle.
    - Keep optional/non-owning SQLite helpers, such as remote TUI local
    reads, as open-only paths that do not run startup backfill.
    - Switch app-server startup from direct
    `codex_state::StateRuntime::init` to the rollout state initializer so
    app-server cannot skip rollout backfill.
    - Collapse split rollout lookup/list APIs so callers use the normal
    methods with an optional state handle instead of `_with_state_db`
    variants.
    - Restore `getConversationSummary(ThreadId)` to delegate through
    `ThreadStore::read_thread` instead of a LocalThreadStore-specific
    rollout path special case.
    - Keep DB-backed rollout path lookup keyed on the DB row and file
    existence, without imposing the filesystem filename convention on
    existing DB rows.
    - Verify readable DB-backed rollout paths against `session_meta.id`
    before returning them, so a stale SQLite row that points at another
    thread's JSONL falls back to filesystem search and read-repairs the DB
    row.
    - Keep `debug prompt-input` filesystem-only so a one-off debug command
    does not initialize or backfill SQLite state just to print prompt input.
    - Keep goal-session test Codex homes alive only in the goal-specific
    helper, rather than leaking tempdirs from the shared session test
    helper.
    - Update tests and call sites to pass explicit state handles where DB
    behavior is expected and explicit `None` where filesystem-only behavior
    is intended.
    
    ## Validation
    
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo check -p
    codex-rollout -p codex-thread-store -p codex-app-server -p codex-core -p
    codex-tui -p codex-exec -p codex-cli --tests`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p
    codex-rollout state_db_`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p
    codex-rollout find_thread_path`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p
    codex-rollout find_thread_path -- --nocapture`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p
    codex-rollout try_init_ -- --nocapture`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p
    codex-rollout`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo clippy -p
    codex-rollout --lib -- -D warnings`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p
    codex-thread-store
    read_thread_falls_back_when_sqlite_path_points_to_another_thread --
    --nocapture`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p
    codex-thread-store`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p codex-core
    shell_snapshot`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p codex-core
    --test all personality_migration`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p codex-core
    --test all rollout_list_find`
    - `RUST_MIN_STACK=8388608 CODEX_SKIP_VENDORED_BWRAP=1
    CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p codex-core
    --test all rollout_list_find::find_prefers_sqlite_path_by_id --
    --nocapture`
    - `RUST_MIN_STACK=8388608 CODEX_SKIP_VENDORED_BWRAP=1
    CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p codex-core
    --test all rollout_list_find -- --nocapture`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p codex-core
    interrupt_accounts_active_goal_before_pausing`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p
    codex-app-server get_auth_status -- --test-threads=1`
    - `CODEX_SKIP_VENDORED_BWRAP=1
    CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p
    codex-app-server --lib`
    - `CODEX_SKIP_VENDORED_BWRAP=1
    CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo check -p codex-rollout
    -p codex-app-server --tests`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db just fix -p codex-rollout
    -p codex-thread-store -p codex-core -p codex-app-server -p codex-tui -p
    codex-exec -p codex-cli`
    - `CODEX_SKIP_VENDORED_BWRAP=1
    CARGO_TARGET_DIR=/tmp/codex-target-state-db just fix -p codex-rollout -p
    codex-app-server`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db just fix -p
    codex-rollout`
    - `CODEX_SKIP_VENDORED_BWRAP=1
    CARGO_TARGET_DIR=/tmp/codex-target-state-db just fix -p codex-core`
    - `just argument-comment-lint -p codex-core`
    - `just argument-comment-lint -p codex-rollout`
    
    Focused coverage added in `codex-rollout`:
    
    - `recorder::tests::state_db_init_backfills_before_returning` verifies
    the rollout metadata row exists before startup init returns.
    - `state_db::tests::try_init_waits_for_concurrent_startup_backfill`
    verifies startup waits for another worker to finish backfill instead of
    disabling the handle for the process.
    -
    `state_db::tests::try_init_times_out_waiting_for_stuck_startup_backfill`
    verifies startup does not hang indefinitely on a stuck backfill lease.
    -
    `tests::find_thread_path_accepts_existing_state_db_path_without_canonical_filename`
    verifies DB-backed lookup accepts valid existing rollout paths even when
    the filename does not include the thread UUID.
    -
    `tests::find_thread_path_falls_back_when_db_path_points_to_another_thread`
    verifies DB-backed lookup ignores a stale row whose existing path
    belongs to another thread and read-repairs the row after filesystem
    fallback.
    
    Focused coverage updated in `codex-core`:
    
    - `rollout_list_find::find_prefers_sqlite_path_by_id` now uses a
    DB-preferred rollout file with matching `session_meta.id`, so it still
    verifies that valid SQLite paths win without depending on stale/empty
    rollout contents.
    
    `cargo test -p codex-app-server thread_list_respects_search_term_filter
    -- --test-threads=1 --nocapture` was attempted locally but timed out
    waiting for the app-server test harness `initialize` response before
    reaching the changed thread-list code path.
    
    `bazel test //codex-rs/thread-store:thread-store-unit-tests
    --test_output=errors` was attempted locally after the thread-store fix,
    but this container failed before target analysis while fetching `v8+`
    through BuildBuddy/direct GitHub. The equivalent local crate coverage,
    including `cargo test -p codex-thread-store`, passes.
    
    A plain local `cargo check -p codex-rollout -p codex-app-server --tests`
    also requires system `libcap.pc` for `codex-linux-sandbox`; the
    follow-up app-server check above used `CODEX_SKIP_VENDORED_BWRAP=1` in
    this container.
  • Prepare selected environment plumbing (#20669)
    ## Why
    This is a prep PR in the multi-environment process-tool stack. It
    separates ownership/config cleanup from the behavior change that teaches
    process tools to route by selected environment, so the follow-up PR can
    focus on model-facing `environment_id` behavior.
    
    ## Stack
    1. https://github.com/openai/codex/pull/20646 - `EnvironmentContext`
    rendering for selected environments
    2. https://github.com/openai/codex/pull/20669 - selected-environment
    ownership and tool config prep (this PR)
    3. https://github.com/openai/codex/pull/20647 - process-tool
    `environment_id` routing
    
    ## What Changed
    - keep the resolved turn environment list wrapped in
    `ResolvedTurnEnvironments` through `TurnContext` instead of unwrapping
    it back to a raw `Vec`
    - add `TurnContext::resolve_path_against` so cwd-relative path
    resolution has one shared helper
    - replace the old tool config boolean with `ToolEnvironmentMode::{None,
    Single, Multiple}`
    
    ## Testing
    - Tests not run locally; this prep refactor is covered by GitHub CI for
    the stack.
    
    Co-authored-by: Codex <noreply@openai.com>
  • Unify skip-review handling for approval_mode = "approve" (#20750)
    ## Summary
    - Treat `approval_mode = "approve"` as skip-review across all permission
    modes.
    - Remove the mode-specific split in the MCP auto-approval gate so
    approved tools bypass review consistently.
    - Expand regression coverage in the shared MCP helper and the core
    tool-call flow.
    
    ## Testing
    - `just fmt`
    - `cargo test -p codex-mcp`
    - `cargo test -p codex-core
    approve_mode_skips_arc_and_guardian_in_every_permission_mode`
    - `git diff --check`
    - Full `cargo test -p codex-core` was also attempted, but the suite hit
    an unrelated pre-existing stack overflow in an existing multi-agent test
  • core: share responses request builder with compact requests (#20989)
    ## Why
    
    `ModelClientSession` and `compact_conversation_history()` were still
    rebuilding the same `ResponsesApiRequest` fields separately. That
    duplication makes it easy for normal `/responses` turns and compact
    requests to drift when request-shape changes land later, which is
    exactly the kind of cache-affecting divergence we want to avoid.
    
    This follow-up keeps the scope small by extracting the shared
    request-construction logic into one helper and using it from both paths.
    
    ## What changed
    
    - move `ResponsesApiRequest` construction into a shared
    `ModelClient::build_responses_request(...)` helper in
    `core/src/client.rs`
    - update the normal `/responses` streaming path to call that helper
    instead of the old `ModelClientSession`-local implementation
    - update `compact_conversation_history()` to derive its compact payload
    from the same helper so `model`, `instructions`, `input`, `tools`,
    `parallel_tool_calls`, `reasoning`, and `text` stay aligned with normal
    request building
    - add a unit test covering the shared helper's prompt cache key,
    installation metadata, and `service_tier` behavior
    
    ## Verification
    
    - `cargo test -p codex-core
    build_responses_request_sets_shared_cache_and_metadata_fields`
    - `cargo test -p codex-core --test all
    remote_compact_v2_reuses_context_compaction_for_followups`
    
    ## Docs
    
    No docs update needed.
  • Keep paused goals paused on thread resume (#20790)
    ## Summary
    
    Early adopters of the `/goal` feature have provided feedback that they
    expect a goal they explicitly paused to remain paused when they resume a
    thread. Previously, resuming a thread would reactivate a paused goal.
    
    This PR keeps persisted goal status unchanged during thread resume. This
    honors the user feedback while also simplifying the core goal logic.
    
    Rather than have the core logic automatically resume a paused goal, that
    responsibility is transferred to the client. The TUI now detects a
    resumed thread with a paused goal and asks the user whether to `Resume
    goal` or `Leave paused`. The prompt appears only for quiet resume flows,
    so users who resume with an immediate prompt are not interrupted.
    
    <img width="544" height="111" alt="image"
    src="https://github.com/user-attachments/assets/0ac9de1c-6ee6-47ba-b223-c03c8eb4c192"
    />
  • feat: add remote compaction v2 Responses client path (#20773)
    ## Why
    
    This adds the `remote_compaction_v2` client path so remote compaction
    can run through the normal Responses stream and install a
    `context_compaction` item that trigger a compaction.
    
    The goal is to migrate some of the compaction logic on the client side
    
    We keeps the v2 transport behind a feature flag while letting follow-up
    requests reuse the compacted context instead of falling back to the
    legacy compaction item shape.
    
    ## What changed
    
    - add `ResponseItem::ContextCompaction` and refresh the generated
    app-server / schema / TypeScript fixtures that expose response items on
    the wire
    - add `core/src/compact_remote_v2.rs` to send compaction through the
    standard streamed Responses client, require exactly one
    `context_compaction` output item, and install that item into compacted
    history
    - route manual compact and auto-compaction through the v2 path when
    `remote_compaction_v2` is enabled, while keeping the existing remote
    compaction path as the fallback
    - preserve the new item type across history retention, follow-up request
    construction, telemetry, rollout persistence, and rollout-trace
    normalization
    - add targeted coverage for the feature flag, `context_compaction`
    serialization, rollout-trace normalization, and remote-compaction
    follow-up behavior
    
    ## Verification
    
    - added protocol tests for `context_compaction`
    serialization/deserialization in `protocol/src/models.rs`
    - added rollout-trace coverage for `context_compaction` normalization in
    `rollout-trace/src/reducer/conversation_tests.rs`
    - added remote compaction integration coverage for v2 follow-up reuse
    and mixed compaction output streams in
    `core/tests/suite/compact_remote.rs`
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • feat: support template interpolation in multi-agent usage hints (#20973)
    ## Why
    
    `multi_agent_v2` usage hints sometimes need to reference resolved config
    values such as the effective thread limit. Those values only exist after
    config layering, defaulting, and feature materialization, so the raw
    TOML alone was not enough to render them.
    
    ## What changed
    
    - allow
    `features.multi_agent_v2.{usage_hint_text,root_agent_usage_hint_text,subagent_usage_hint_text}`
    to use `{{ ... }}` placeholders backed by the materialized effective
    config
    - fail config loading with a targeted error when a referenced
    placeholder does not exist or does not resolve to a scalar value
    - move resolved-config materialization into a shared helper so config
    interpolation and config-lock export/replay both serialize the same
    resolved feature, memory, and agent settings
    
    ## Example
    ```
    [features.multi_agent_v2]
    enabled = true
    usage_hint_text = "lorem {{ features.multi_agent_v2.max_concurrent_threads_per_session }} ipsum"
    ```
    gets rendered as 
    ```
            "description": String("... \lorem 4 ipsum"),
    ```
  • [codex] Emit MCP tool calls as turn items (#20677)
    ## Why
    
    `McpToolCall` was still an app-server item synthesized from deprecated
    legacy begin/end events. Recent item migrations moved this ownership
    into core `TurnItem`s, so MCP tool calls now follow the same canonical
    lifecycle and leave legacy events as compatibility fanout.
    
    Keeping the core item close to the v2 `ThreadItem::McpToolCall` shape
    also avoids spreading MCP result semantics across app-server conversion
    code. Core now owns whether a completed call is `completed` or `failed`,
    and whether the payload is a tool result or an error.
    
    ## What changed
    
    - Added core `TurnItem::McpToolCall` with flattened `server`, `tool`,
    `arguments`, `status`, `result`, and `error` fields.
    - Updated MCP tool call emitters, including MCP resource tools, to emit
    `ItemStarted`/`ItemCompleted` around directly constructed core MCP
    items.
    - Updated app-server v2 conversion to project the core MCP item into
    `ThreadItem::McpToolCall` without deriving status or splitting `Result`
    locally.
    - Ignored live deprecated MCP legacy fanout in app-server v2 to avoid
    duplicate item notifications, while keeping thread history replay on the
    legacy event path.
    
    ## Verification
    
    - `cargo test -p codex-protocol`
    - `cargo test -p codex-app-server-protocol`
    - `cargo test -p codex-core --lib mcp_tool_call`
    - `cargo check -p codex-app-server`
    - `cargo test -p codex-app-server
    mcp_tool_call_completion_notification_contains_truncated_large_result`
  • [tool_suggest] More prompt polishes. (#20566)
    Tool suggest still misfires when model needs tool_search, updating the
    prompts to further disambiguate it:
    
    - [x] rename it from `tool_suggest` to `request_plugin_install`
    - [x] rephrase "suggestion" to "install" in the tool descriptions.
    - [x] disambiguate "the tool" vs "the plugin/connector". 
    
    Tested with the Codex App and verified it still works.
  • Prune unused code-mode globals (#20542)
    Hide Atomics, SharedArrayBuffer, and WebAssembly from the code-mode
    runtime since the harness does not expose worker support or need those
    APIs.
  • Surface multi-environment choices in environment context (#20646)
    ## Why
    The model needs a way to see which environments are available during a
    multi-environment turn without changing the legacy single-environment
    prompt surface or pulling replay/persistence changes into the same
    review.
    
    ## Stack
    1. https://github.com/openai/codex/pull/20646 - `EnvironmentContext`
    rendering for selected environments (this PR)
    2. https://github.com/openai/codex/pull/20669 - selected-environment
    ownership and tool config prep
    3. https://github.com/openai/codex/pull/20647 - process-tool
    `environment_id` routing
    
    ## What Changed
    - extend `environment_context` so multi-environment turns render an
    `<environments>` block with the selected environment ids and cwd values
    - keep zero- and single-environment turns on the existing cwd-only
    render path
    - keep replay and persistence paths on the legacy surface for now so
    this PR stays scoped to live prompt rendering
    - add focused coverage in
    `codex-rs/core/src/context/environment_context_tests.rs`
    
    ## Testing
    - CI
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • [codex] Emit image view as core item (#20512)
    ## Why
    
    Image-view results should be represented as a core-produced turn item
    instead of being reconstructed by app-server. At the same time, existing
    rollout/history paths still understand the legacy `ViewImageToolCall`
    event, so this keeps that event as compatibility output generated from
    the new item lifecycle.
    
    ## What changed
    
    - Added `TurnItem::ImageView` to `codex-protocol`.
    - Emitted image-view item start/completion directly from the core
    `view_image` handler.
    - Kept `ViewImageToolCall` as a legacy event and generate it from
    completed `TurnItem::ImageView` items.
    - Kept `thread_history.rs` on the legacy `ViewImageToolCall` replay
    path, with `ImageView` item lifecycle events ignored there.
    - Updated app-server protocol conversion, rollout persistence, and
    affected exhaustive event matches for the new item plus legacy fan-out
    shape.
    
    ## Verification
    
    - `cargo test -p codex-protocol -p codex-app-server-protocol -p
    codex-rollout -p codex-rollout-trace -p codex-mcp-server -p
    codex-app-server --lib`
    - `cargo test -p codex-core --test all
    view_image_tool_attaches_local_image`
    - `just fix -p codex-protocol -p codex-core -p codex-app-server-protocol
    -p codex-app-server -p codex-rollout -p codex-rollout-trace -p
    codex-mcp-server`
    - `git diff --check`
  • fix: reduce ConfigBuilder::build stack usage (#20650)
    ## Why
    
    `ConfigBuilder::build` performs a large amount of async config loading.
    Leaving that entire future on the caller stack makes config startup more
    fragile on small runtime worker stacks.
    
    ## What changed
    
    - keep `ConfigBuilder::build` as a thin wrapper that boxes the
    config-loading future before awaiting it
    - move the existing implementation into a private `build_inner` method
    so the large async state machine lives on the heap instead of the
    runtime thread stack
    
    ## Testing
    
    - Not run locally
  • Use selected turn environments for runtime context (#20281)
    ## Summary
    - make selected turn environments the source of truth for session
    runtime cwd and MCP runtime environment selection
    - keep local/no-selection fallback behavior intact
    - add coverage for duplicate selected environments, cwd resolution, and
    MCP runtime environment selection
    
    ## Validation
    - git diff --check
    - rustfmt was run on touched Rust files during the implementation
    workflow
    
    CI should provide the full Bazel/test signal.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • deprecate legacy notify (#20524)
    # Why
    
    `notify` is the remaining compatibility surface from the legacy hook
    implementation. The newer lifecycle hook engine now owns the active hook
    system, so we should start steering users away from adding new `notify`
    configs before removing the old path entirely. This also adds a
    lightweight watchpoint for the deprecation so we can see how much legacy
    usage remains before the clean drop.
    
    # What
    
    - emit a startup deprecation notice when a non-empty `notify` command is
    configured
    - emit `codex.notify.configured` when a session starts with legacy
    `notify` configured
    - emit `codex.notify.run` when the legacy notify path fires after a
    completed turn
    - mark `notify` as deprecated in the config schema and repo docs
    - remove the orphaned `codex-rs/hooks/src/user_notification.rs` file
    that is no longer compiled
    - add regression coverage for the new deprecation notice
    
    # Next steps
    
    A follow-up PR can remove the legacy notify path entirely once we are
    ready for the clean drop. Before then, we can watch
    `codex.notify.configured` and `codex.notify.run` to understand the
    deprecation impact and remaining active usage. The cleanup PR should
    then delete the `notify` config field, the `legacy_notify`
    implementation, the old compatibility dispatch types and callsites that
    only exist for the legacy path, and the remaining compatibility
    docs/tests.
    
    # Testing
    
    - `cargo test -p codex-hooks`
    - `cargo test -p codex-config`
    - `cargo test -p codex-core emits_deprecation_notice_for_notify`
  • Remove no-tool goal continuation suppression (#20523)
    ## Why
    
    `/goal` is supposed to keep Codex working until the goal is actually
    done. The previous continuation logic had two ways to stop early: the
    continuation prompt told the model to wait for new input when it felt
    blocked, and the runtime suppressed another continuation turn after a
    continuation finished without any tool calls.
    
    That made goals stop short even when the agent could still keep making
    progress (I received a few reports of this from users). It also relied
    on a brittle heuristic that treated "no registry tool calls" as
    equivalent to "should stop."
    
    ## What changed
    
    - removed the continuation prompt sentence that told the model to stop
    and wait for new input when it could not continue productively
    - removed the goal runtime suppression heuristic that stopped
    auto-continuation after a no-tool continuation turn
    - deleted the continuation-activity bookkeeping and left `tool_calls` as
    telemetry only
    - added focused regressions for the two intended behaviors: completed
    no-tool continuation turns still continue, while `request_user_input`
    keeps the existing turn open instead of spawning a new continuation
  • Move apply-patch file changes into turn items (#20540)
    ## Why
    
    Apply-patch file changes are now part of the core turn item stream, so
    v2 clients can consume the same first-class item lifecycle path used by
    other turn items instead of relying on app-server-specific remapping
    from legacy patch events.
    
    ## What changed
    
    - Added a core `TurnItem::FileChange` carrying apply-patch changes and
    completion metadata.
    - Updated the apply-patch tool emitter to send `ItemStarted` /
    `ItemCompleted` with the new `FileChange` item while preserving legacy
    `PatchApplyBegin` / `PatchApplyEnd` fan-out.
    - Updated app-server v2 conversion to render the new core item directly
    and stopped `event_mapping` from remapping old patch begin/end events
    into item notifications.
    - Kept thread history reconstruction based on the existing old
    apply-patch events for rollout compatibility.
    
    ## Verification
    
    - `cargo test -p codex-protocol -p codex-app-server-protocol`
    - `cargo test -p codex-core --test all
    apply_patch_tool_executes_and_emits_patch_events`
    - `cargo test -p codex-app-server bespoke_event_handling`
  • feat: export and replay effective config locks (#20405)
    ## Why
    
    For reproducibility. A hand-written `config.toml` is not enough to
    recreate what a Codex session actually ran with because layered config,
    CLI overrides, defaults, feature aliases, resolved feature config,
    prompt setup, and model-catalog/session values can all affect the final
    runtime behavior.
    
    This PR adds an effective config lockfile path: one run can export the
    resolved session config, and a later run can replay that lockfile and
    fail early if the regenerated effective config drifts.
    
    ## What Changed
    
    - Add a dedicated `ConfigLockfileToml` wrapper with top-level lockfile
    metadata plus the replayable config:
    
      ```toml
      version = 1
      codex_version = "..."
    
      [config]
      # effective ConfigToml fields
      ```
    
    - Keep lockfile metadata out of regular `ConfigToml`; replay loads
    `ConfigLockfileToml` and then uses its nested `config` as the
    authoritative config layer.
    - Add `debug.config_lockfile.export_dir` to write
    `<thread_id>.config.lock.toml` when a root session starts.
    - Add `debug.config_lockfile.load_path` to replay a saved lockfile and
    validate the regenerated session lockfile against it.
    - Add `debug.config_lockfile.allow_codex_version_mismatch` to optionally
    tolerate Codex binary version drift while still comparing the rest of
    the lockfile.
    - Add `debug.config_lockfile.save_fields_resolved_from_model_catalog` so
    lock creation can either save model-catalog/session-resolved fields or
    intentionally leave those fields dynamic.
    - Build lockfiles from the effective config plus resolved runtime values
    such as model selection, reasoning settings, prompts, service tier, web
    search mode, feature states/config, memories config, skill instructions,
    and agent limits.
    - Materialize feature aliases and custom feature config into the
    lockfile so replay compares canonical resolved behavior instead of
    user-authored alias shape.
    - Strip profile/debug/file-include/environment-specific inputs from
    generated lockfiles so they contain replayable values rather than the
    inputs that produced those values.
    - Surface JSON-RPC server error code/data in app-server client and TUI
    bootstrap errors so config-lock replay failures include the actual TOML
    diff.
    - Regenerate the config schema for the new debug config keys.
    
    ## Review Notes
    
    The main flow is split across these files:
    
    - `config/src/config_toml.rs`: lockfile/debug TOML shapes.
    - `core/src/config/mod.rs`: loading `debug.config_lockfile.*`, replaying
    a lockfile as a config layer, and preserving the expected lockfile for
    validation.
    - `core/src/session/config_lock.rs`: exporting the current session
    lockfile and materializing resolved session/config values.
    - `core/src/config_lock.rs`: lockfile parsing, metadata/version checks,
    replay comparison, and diff formatting.
    
    ## Usage
    
    Export a lockfile from a normal session:
    
    ```sh
    codex -c 'debug.config_lockfile.export_dir="/tmp/codex-locks"'
    ```
    
    Export a lockfile without saving model-catalog/session-resolved fields:
    
    ```sh
    codex -c 'debug.config_lockfile.export_dir="/tmp/codex-locks"' \
      -c 'debug.config_lockfile.save_fields_resolved_from_model_catalog=false'
    ```
    
    Replay a saved lockfile in a later session:
    
    ```sh
    codex -c 'debug.config_lockfile.load_path="/tmp/codex-locks/<thread_id>.config.lock.toml"'
    ```
    
    If replay resolves to a different effective config, startup fails with a
    TOML diff.
    
    To tolerate Codex binary version drift during replay:
    
    ```sh
    codex -c 'debug.config_lockfile.load_path="/tmp/codex-locks/<thread_id>.config.lock.toml"' \
      -c 'debug.config_lockfile.allow_codex_version_mismatch=true'
    ```
    
    ## Limitations
    
    This does not support custom rules/network policies.
    
    ## Verification
    
    - `cargo test -p codex-core config_lock`
    - `cargo test -p codex-config`
    - `cargo test -p codex-thread-manager-sample`
  • Color TUI statusline from active theme (#19631)
    ## Why
    
    Users have shared that the TUI can feel too visually flat because themes
    mostly show up in code syntax highlighting. The configurable statusline
    is a natural place to make the active theme more visible, while still
    letting users keep the existing monotone statusline if they prefer it.
    
    ## What Changed
    
    - Added a statusline styling helper that builds the rendered statusline
    from `(StatusLineItem, text)` segments, preserving item identity while
    keeping the plain text output unchanged.
    - Derived foreground accent colors from the active syntax theme by
    looking up TextMate scopes through the existing syntax highlighter, with
    conservative ANSI fallbacks when a scope does not provide a foreground.
    - Tuned theme-derived colors to keep the accents visible without making
    the statusline feel overly bright.
    - Added `[tui].status_line_use_colors`, defaulting to `true`, plus a
    separated `/statusline` toggle so users can enable or disable
    theme-derived statusline colors from the setup UI.
    - Updated the live statusline and `/statusline` preview to use the same
    styled builder, while keeping terminal-title preview text plain.
    - Kept statusline separators and active-agent add-ons subdued while
    removing blanket dimming from the whole passive statusline.
    
    ## Verification
    
    - `cargo test -p codex-tui status_line`
    - `cargo test -p codex-tui theme_picker`
    - `cargo test -p codex-tui foreground_style_for_scopes`
    - `cargo test -p codex-tui`
    - `cargo test -p codex-config`
    - `cargo test -p codex-core status_line_use_colors`
    - `cargo insta pending-snapshots --manifest-path tui/Cargo.toml`
    
    ## Visual
    
    <img width="369" height="23" alt="Screenshot 2026-04-30 at 6 16 08 PM"
    src="https://github.com/user-attachments/assets/11d03efb-8e4f-4450-8f4d-00a9659ef4cd"
    />
    
    <img width="385" height="23" alt="Screenshot 2026-04-30 at 6 16 02 PM"
    src="https://github.com/user-attachments/assets/a3d89f36-bdc1-42e8-8e84-61350e3999e2"
    />
  • Make thread store process-scoped (#19474)
    - Build one app-server process ThreadStore from startup config and share
    it with ThreadManager and CodexMessageProcessor.
    - Remove per-thread/fork store reconstruction so effective thread config
    cannot switch the persistence backend.
    - Add params to ThreadStore create/resume for specifying thread
    metadata, since otherwise the metadata from store creation would be used
    (incorrectly).
  • [codex] Remove unused event messages (#20511)
    ## Why
    
    Several legacy `EventMsg` variants were still emitted or mapped even
    though clients either ignored them or had moved to item/lifecycle
    events. `Op::Undo` had also degraded to an unavailable shim, so this
    removes that dead task path instead of preserving a command that cannot
    do useful work.
    
    `McpStartupComplete`, `WebSearchBegin`, and `ImageGenerationBegin` are
    intentionally kept because useful consumers still depend on them: MCP
    startup completion drives readiness behavior, and the begin events let
    app-server/core consumers surface in-progress web-search and
    image-generation items before the final payload arrives.
    
    ## What Changed
    
    - Removed weak legacy event variants and payloads from `codex-protocol`,
    including legacy agent deltas, background events, and undo lifecycle
    events.
    - Kept/restored `EventMsg::McpStartupComplete`,
    `EventMsg::WebSearchBegin`, and `EventMsg::ImageGenerationBegin` with
    serializer and emission coverage.
    - Updated core, rollout, MCP server, app-server thread history,
    review/delegate filtering, and tests to rely on the useful replacement
    events that remain.
    - Removed `Op::Undo`, `UndoTask`, the undo test module, and stale TUI
    slash-command comments.
    - Stopped agent job/background progress and compaction retry notices
    from emitting `BackgroundEvent` payloads.
    
    ## Verification
    
    - `cargo check -p codex-protocol -p codex-app-server-protocol -p
    codex-core -p codex-rollout -p codex-rollout-trace -p codex-mcp-server`
    - `cargo test -p codex-protocol -p codex-app-server-protocol -p
    codex-rollout -p codex-rollout-trace -p codex-mcp-server`
    - `cargo test -p codex-core --test all suite::items`
    - `just fix -p codex-protocol -p codex-app-server-protocol -p codex-core
    -p codex-rollout -p codex-rollout-trace -p codex-mcp-server`
    - Earlier coverage on this PR also included `codex-mcp`, `codex-tui`,
    core library tests, MCP/plugin/delegate/review/agent job tests, and MCP
    startup TUI tests.
  • fix(exec_policy) heredoc parsing file_redirect (#20113)
    ## Summary
    Fixes a regression introduced in #10941 so that heredocs do not permit
    file redirects to be approved by rules, and adds scenario tests to cover
    this behavior.
    
    
    Previously, heredoc command parsing would allow redirects and
    environment variables:
    ```bash
    # commands_for_exec_policy() would parse this via parse_shell_lc_single_command_prefix
    PATH=/tmp/bad:$PATH cat <<'EOF' > /tmp/bad/hello.txt
    hello
    EOF
    ```
    This conflicts with the Codex Rules documentation; heredoc parsing logic
    should abide by the same strictness of parsing.
    
    
    ## Tests
    - [x] Updated unit tests accordingly
    - [x] Added scenario tests for these cases
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • execpolicy: unwrap PowerShell -Command wrappers on Windows (#20336)
    ## Why
    On Windows, Codex runs shell commands through a top-level
    `powershell.exe -NoProfile -Command ...` wrapper. `execpolicy` was
    matching that wrapper instead of the inner command, so prefix rules like
    `["git", "push"]` did not fire for PowerShell-wrapped commands even
    though the same normalization already happens for `bash -lc` on Unix.
    
    This change makes the Windows shell wrapper transparent to rule matching
    while preserving the existing Windows unmatched-command safelist and
    dangerous-command heuristics.
    
    ## What changed
    - add `parse_powershell_command_plain_commands()` in
    `shell-command/src/powershell.rs` to unwrap the top-level PowerShell
    `-Command` body with `extract_powershell_command()` and parse it with
    the existing PowerShell AST parser
    - update `core/src/exec_policy.rs` so `commands_for_exec_policy()`
    treats top-level PowerShell wrappers like `bash -lc` and evaluates rules
    against the parsed inner commands
    - carry a small `ExecPolicyCommandOrigin` through unmatched-command
    evaluation and expose `is_safe_powershell_words()` /
    `is_dangerous_powershell_words()` so Windows safelist and
    dangerous-command checks still work after unwrap
    - add Windows-focused tests for wrapped PowerShell prompt/allow matches,
    wrapper parsing, and unmatched safe/dangerous inner commands, and
    re-enable the end-to-end `execpolicy_blocks_shell_invocation` test on
    Windows
    
    ## Testing
    - `cargo test -p codex-shell-command`
  • Alias codex_hooks feature as hooks (#20522)
    # Why
    
    The hooks feature flag should use the concise canonical name `hooks`,
    while existing configs that still use `codex_hooks` continue to work
    during the rename.
    
    # What
    
    - change the canonical `Feature::CodexHooks` key from `codex_hooks` to
    `hooks`
    - register `codex_hooks` through the existing legacy-alias path
    - update the config schema and canonical config fixtures to prefer
    `hooks`
    - add regression coverage that both `hooks` and `codex_hooks` resolve to
    `Feature::CodexHooks`
    
    # Verification
    
    - `cargo test -p codex-features`
    - `cargo test -p codex-core config::schema_tests`
    - `cargo test -p codex-core
    pre_tool_use_blocks_shell_when_defined_in_config_toml`
    - `cargo test -p codex-app-server
    hooks_list_uses_each_cwds_effective_feature_enablement`
  • feat(tui): add vim composer mode (#18595)
    ## Why
    
    Codex now has configurable TUI keymaps, but the composer still behaves
    like a plain text field. Users who prefer modal editing need a way to
    keep Vim muscle memory while drafting prompts, and the keymap picker
    needs to expose Vim-specific actions if those bindings are configurable
    instead of hardcoded.
    
    ## What Changed
    
    - Adds composer Vim mode with insert/normal state, common normal-mode
    movement and editing commands, `d`/`y` operator-pending flows, and
    mode-aware footer and cursor indicators.
    - Adds `/vim`, an optional global `toggle_vim_mode` binding, and
    `tui.vim_mode_default` so Vim mode can be toggled per session or enabled
    as the default composer state.
    - Extends runtime and config keymaps with `vim_normal` and
    `vim_operator` contexts, exposes those contexts in `/keymap`, refreshes
    the config schema, and validates Vim bindings separately.
    - Integrates Vim normal mode with existing composer behavior: `/` opens
    slash command entry, `!` enters shell mode, `j`/`k` navigate history at
    history boundaries, successful submissions reset back to normal mode,
    and paste burst handling remains insert-mode only.
    - Teaches the TUI render path to apply and restore cursor style so Vim
    insert mode can use a bar cursor without leaving the terminal in that
    state after exit.
    
    ## Validation
    
    - `cargo test -p codex-tui keymap -- --nocapture` on the keymap/Vim
    coverage
    - `cargo insta pending-snapshots`
    
    ## Docs
    
    This introduces user-facing `/vim`, `tui.vim_mode_default`, and Vim
    keymap contexts under `tui.keymap`, so the public CLI configuration and
    slash-command docs should be updated before the feature ships.
  • Bypass review for always-allow MCP tools in auto-review (#20069)
    ## Why
    
    When an MCP or app tool is configured with approval mode `approve`
    (always allow), users expect that decision to be authoritative. In
    guardian auto-review mode, ARC could still return `ask-user`, which then
    routed the approval question into guardian with the ARC reason as
    context. That meant a tool explicitly configured as always allowed still
    went through both safety monitors before running.
    
    This change keeps the existing ARC behavior for non-auto-review
    sessions, but avoids the ARC-to-guardian sequence when
    `approvals_reviewer = auto_review` and the tool approval mode is
    `approve`.
    
    ## What changed
    
    - Short-circuit MCP tool approval handling when `approval_mode ==
    approve` and `approvals_reviewer == auto_review`.
    - Updated the MCP approval regression test so the auto-review case
    asserts neither ARC nor guardian is called.
    - Preserved existing tests that verify ARC can still block always-allow
    MCP tools outside guardian auto-review mode.
    
    ## Verification
    
    - `cargo test -p codex-core --lib mcp_tool_call`
  • fix: ignore dangerous project-level config keys (#20098)
    ## Description
    Ignore these top-level config keys when loading project-scoped
    config.toml files:
    ```
        "openai_base_url",
        "chatgpt_base_url",
        "model_provider",
        "model_providers",
        "profile",
        "profiles",
        "experimental_realtime_ws_base_url",
    ```
    
    ## What changed
    
    - Add a project-local config denylist for credential-routing fields such
    as `openai_base_url`, `chatgpt_base_url`, `model_provider`,
    `model_providers`, `profile`, `profiles`, and
    `experimental_realtime_ws_base_url`.
    - Strip those fields from project config layers before they participate
    in effective config merging, while leaving safe project-local settings
    intact.
    - Track ignored project-local keys on config layers and surface a
    startup warning telling users to move those settings to user-level
    `config.toml` if they intentionally need them.
    - Update profile behavior coverage so project-local `profile` /
    `profiles` entries are ignored instead of overriding user-level profile
    selection.
    
    ## Verification
    
    - `cargo test -p codex-config`
    - `cargo test -p codex-core
    project_layer_ignores_unsupported_config_keys`
    - `cargo test -p codex-core project_profiles_are_ignored`
    - `cargo test -p codex-core config::config_loader_tests`
  • [codex] Migrate thread turns list to thread store (#19280)
    - migrate `thread/turns/list` to ThreadStore. Uses ThreadStore for most
    data now but merges in the in-memory state from thread manager
    - keep v2 `thread/list` pathless-store friendly by converting
    `StoredThread` directly to API `Thread`
    - add regression coverage for pathless store history/listing