Commit Graph

92 Commits

  • [codex] Move pending input into input queue (#22728)
    ## Why
    
    Pending model input was split across `Session`, `TurnState`, and the
    agent mailbox. That made it easy for new paths to manage queued user
    input or mailbox delivery outside the intended ownership boundary.
    
    This PR consolidates the model-facing input lifecycle behind the session
    input queue so turn-local pending input, next-turn queued items, and
    mailbox delivery coordination are owned in one place.
    
    ## What Changed
    
    - Added `session/input_queue.rs` to own pending input queues and mailbox
    delivery coordination.
    - Removed the standalone `agent/mailbox.rs` channel wrapper and store
    mailbox items directly in the input queue.
    - Moved pending-input mutations off `TurnState`; `TurnState` now exposes
    the queue-owned storage directly for now.
    - Routed abort cleanup, mailbox delivery phase changes, next-turn queued
    items, and active-turn pending input through `InputQueue`.
    - Boxed stack-heavy agent resume/fork startup futures that the refactor
    pushed over the default test stack.
    - Updated session, task, goal, stream-event, and multi-agent call sites
    and tests to use the new queue ownership.
    
    ## Verification
    
    - `cargo test -p codex-core --lib agent::control::tests`
    - `cargo test -p codex-core --lib
    agent::control::tests::resume_closed_child_reopens_open_descendants --
    --exact`
    - `cargo test -p codex-core --lib
    agent::control::tests::spawn_agent_fork_last_n_turns_keeps_only_recent_turns
    -- --exact`
    - `cargo test -p codex-core --lib
    agent::control::tests::resume_thread_subagent_restores_stored_nickname_and_role
    -- --exact`
    - `cargo test -p codex-core` was also run; it completed with 1814
    passed, 4 ignored, and one timeout in
    `agent::control::tests::resume_thread_subagent_restores_stored_nickname_and_role`,
    which passed when rerun in isolation.
  • goals: keep pause transitions explicit (#23088)
    ## Problem
    
    This addresses several user-reported cases where active goals were
    paused even though the user had not explicitly asked for that
    transition:
    
    - the guardian approval-review circuit breaker interrupted a turn and
    implicitly paused the goal
    - a shutdown in one app-server instance could pause a goal while a
    second instance was still actively running the same thread
    - steering-style interrupts could also pause the goal even though they
    are meant to redirect work, not stop the goal lifecycle
    
    The common problem was that core treated `TurnAbortReason::Interrupted`
    as an implicit request to transition the persisted goal to `paused`.
    That made unrelated interrupt paths mutate goal state as a side effect,
    and in the multi-app-server case it allowed stale process teardown to
    pause a live goal owned by another running client.
    
    After this change, transitioning a goal to `paused` is always an
    explicit action performed by a client or another intentional goal-state
    mutation. It is never an implicit transition triggered by generic
    interrupt handling.
    
    Refs #22884.
    
    ## What changed
    
    - Remove the goal runtime path that paused active goals after
    interrupted task aborts.
    - Drop the now-unused abort reason from `GoalRuntimeEvent::TaskAborted`.
    - Update the focused regression coverage so an interrupted active goal
    still accounts usage but remains `active`.
  • Make extension lifecycle hooks async (#23291)
    ## Why
    
    Extension lifecycle hooks sit on the host/extension boundary, but the
    current trait surface only allows synchronous callbacks. That forces
    extensions that need to seed, rehydrate, observe, or flush
    extension-owned state during thread and turn transitions to either block
    inside the callback or move async work into separate host plumbing.
    
    This PR makes those lifecycle callbacks awaitable so extension
    implementations can perform async work directly at the lifecycle point
    where the host already has the relevant session, thread, or turn stores
    available.
    
    ## What changed
    
    - Makes `ThreadLifecycleContributor` and `TurnLifecycleContributor`
    async in `codex-extension-api`.
    - Awaits thread start/resume/stop and turn start/stop/abort lifecycle
    callbacks from `codex-core`.
    - Updates the guardian and memories extensions to implement the async
    lifecycle trait surface.
    - Updates the existing lifecycle tests to use async contributor
    implementations.
    - Adds `async-trait` to the crates that now expose or implement these
    async object-safe lifecycle traits.
    
    ## Testing
    
    - Existing `codex-core` lifecycle tests were updated to cover async
    implementations for thread stop and turn abort ordering.
  • Fix turn extension data task plumbing (#22646)
    ## Summary
    - carry the per-turn extension data through RunningTask so abort
    handling can rebuild SessionTaskContext
    - update stale test ExtensionData::new() callsites to pass the turn id
    
    ## Testing
    - Not run after PR branch creation; CI will cover.
  • Wire turn item contributors into stream output (#22494)
    ## Summary
    - run registered TurnItemContributor hooks for parsed stream output
    items
    - plumb the active turn extension store into stream item handling
    - preserve existing memory citation parsing as fallback after
    contributors run
    
    ## Tests
    - cargo test -p codex-core stream_events_utils -- --nocapture
    - just fmt
    - just fix -p codex-core
    - git diff --check
  • feat: move extension scope ids into ExtensionData (#22490)
    ## Summary
    - add a scoped level_id to ExtensionData and expose it through
    level_id()
    - remove thread_id/turn_id parameters from extension contributor inputs
    where the scoped ExtensionData already carries that identity
    - move turn-scoped extension data onto TurnContext so token usage and
    lifecycle contributors can share the same turn store
    
    ## Testing
    - cargo check -p codex-extension-api -p codex-core --tests
    - cargo test -p codex-extension-api
    - cargo test -p codex-guardian
    - cargo test -p codex-core --lib
    record_token_usage_info_notifies_extension_contributors
    - cargo test -p codex-core --lib
    submission_loop_channel_close_emits_thread_stop_lifecycle
    - cargo test -p codex-core --lib
    submission_loop_channel_close_aborts_active_turn_before_thread_stop_lifecycle
    - just fix -p codex-extension-api
    - just fix -p codex-guardian
    - just fix -p codex-core
    - just fmt
    
    ## Note
    - Attempted cargo test -p codex-core; it aborted in
    agent::control::tests::spawn_agent_fork_last_n_turns_keeps_only_recent_turns
    with the existing stack overflow before the full suite completed.
  • feat: add turn lifecycle contributors (#22480)
    ## Why
    
    Extensions can already contribute prompt, tool, turn-item, and
    thread-lifecycle behavior, but there was no explicit host-owned hook for
    per-turn setup and cleanup. That makes extension-private turn state
    awkward: an extension either has to stash it outside the turn lifecycle
    or depend on core runtime objects.
    
    This adds a small turn lifecycle boundary. Extensions receive stable
    identifiers plus the existing session, thread, and turn `ExtensionData`
    stores, while core keeps owning task scheduling, cancellation, and turn
    teardown.
    
    ## What Changed
    
    - Added `TurnLifecycleContributor` with `on_turn_start`, `on_turn_stop`,
    and `on_turn_abort` callbacks in `codex-rs/ext/extension-api`.
    - Added typed `TurnStartInput`, `TurnStopInput`, and `TurnAbortInput`
    payloads that expose `thread_id`, `turn_id`, `session_store`,
    `thread_store`, and `turn_store`.
    - Registered and re-exported turn lifecycle contributors through
    `ExtensionRegistry` and `ExtensionRegistryBuilder`.
    - Wired `Session` to emit turn start, stop, and abort callbacks from the
    existing turn/task lifecycle paths.
    - Carried the turn-scoped `ExtensionData` through `RunningTask` and
    `RemovedTask` so stop/abort callbacks receive the same turn store
    created at turn start.
    
    ## Verification
    
    - Not run locally.
  • 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`
  • 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
  • [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.
  • [codex] Add token usage to turn tracing spans (#19432)
    ## Why
    
    Slow Codex turns are easier to debug when token usage is visible in the
    trace itself, without joining against separate analytics. This adds
    token usage to existing turn-handling spans for regular user turns only.
    
    [Example
    turn](https://openai.datadoghq.com/apm/trace/9d353efa2cb5de1f4c5b93dc33c3df04?colorBy=service&graphType=flamegraph&shouldShowLegend=true&sort=time&spanID=3555541504891512675&spanViewType=metadata&traceQuery=)
    <img width="1447" height="967" alt="Screenshot 2026-04-24 at 3 03 07 PM"
    src="https://github.com/user-attachments/assets/ab7bb187-e7fc-41f0-a366-6c44610b2b2c"
    />
    
    ## What Changed
    
    Added response-level token fields on completed handle_responses spans:
    
    gen_ai.usage.input_tokens
    gen_ai.usage.cache_read.input_tokens
    gen_ai.usage.output_tokens
    codex.usage.reasoning_output_tokens
    codex.usage.total_tokens
    Added aggregate token fields on regular turn spans:
    
    codex.turn.token_usage.*
    Added an explicit regular-turn opt-in via
    SessionTask::records_turn_token_usage_on_span() so this is not coupled
    to span-name strings.
    
    ## Testing
    
    - `cargo test -p codex-otel`
    - `cargo test -p codex-core
    turn_and_completed_response_spans_record_token_usage`
    - `just fmt`
    - `just fix -p codex-core`
    - `just fix -p codex-otel`
    - Manual local Electron/app-server smoke test: regular user turn emits
    the new span fields
    
    Known status: `cargo test -p codex-core` was attempted and failed in
    unrelated existing areas: config approvals, request-permissions,
    git-info ordering, and subagent metadata persistence.
  • Add turn start timestamp to turn metadata (#19473)
    ## Why
    - Without change: MCP tool calls receive
    `_meta["x-codex-turn-metadata"]` with `session_id` and `turn_id`.
    - Issue: MCP servers may want the turn start timestamp to measure
    internal latency relative to turn start.
    
    ## What Changed
    - With change: turn metadata now includes `turn_started_at_unix_ms`,
    which is propagated to MCP tool calls in
    `_meta["x-codex-turn-metadata"]`.
    
    ## Verification
    - `codex-rs/core/src/mcp_tool_call_tests.rs`
    - `codex-rs/core/src/turn_metadata_tests.rs`
    - `codex-rs/core/src/turn_timing_tests.rs`
    - `codex-rs/core/tests/responses_headers.rs`
    - `codex-rs/core/tests/suite/search_tool.rs`
  • Remove ghost snapshots (#19481)
    ## Summary
    - Remove `ghost_snapshot` / `GhostCommit` from the Responses API surface
    and generated SDK/schema artifacts.
    - Keep legacy config loading compatible, but make undo a no-op that
    reports the feature is unavailable.
    - Clean up core history, compaction, telemetry, rollout, and tests to
    stop carrying ghost snapshot items.
    
    ## Testing
    - Unit tests passed for `codex-protocol`, `codex-core` targeted undo and
    compaction flows, `codex-rollout`, and `codex-app-server-protocol`.
    - Regenerated config and app-server schemas plus Python SDK artifacts
    and verified they match the checked-in outputs.
  • Add goal core runtime (4 / 5) (#18076)
    Adds the core runtime behavior for active goals on top of the model
    tools from PR 3.
    
    ## Why
    
    A long-running goal should be a core runtime concern, not something
    every client has to implement. Core owns the turn lifecycle, tool
    completion boundaries, interruptions, resume behavior, and token usage,
    so it is the right place to account progress, enforce budgets, and
    decide when to continue work.
    
    ## What changed
    
    - Centralized goal lifecycle side effects behind
    `Session::goal_runtime_apply(GoalRuntimeEvent::...)`.
    - Starts goal continuation turns only when the session is idle; pending
    user input and mailbox work take priority.
    - Accounts token and wall-clock usage at turn, tool, mutation,
    interrupt, and resume boundaries; `get_thread_goal` remains read-only.
    - Preserves sub-second wall-clock remainder across accounting boundaries
    so long-running goals do not drift downward over time.
    - Treats token budget exhaustion as a soft stop by marking the goal
    `budget_limited` and injecting wrap-up steering instead of aborting the
    active turn.
    - Suppresses budget steering when `update_goal` marks a goal complete.
    - Pauses active goals on interrupt and auto-reactivates paused goals
    when a thread resumes outside plan mode.
    - Suppresses repeated automatic continuation when a continuation turn
    makes no tool calls.
    - Added continuation and budget-limit prompt templates.
    
    ## Verification
    
    - Added focused core coverage for continuation scheduling, accounting
    boundaries, budget-limit steering, completion accounting, interrupt
    pause behavior, resume auto-activation, and wall-clock remainder
    accounting.
  • Add agents.interrupt_message for interruption markers (#19351)
    ## Why
    
    Agent interruptions currently always persist a model-visible
    interrupted-turn marker before emitting `TurnAborted`. That marker is
    useful by default because it gives the next model turn context about a
    deliberately interrupted task, but some deployments need to suppress
    that history injection entirely while still keeping the client-visible
    interruption event.
    
    ## What changed
    
    - Add `[agents] interrupt_message = false` to disable the model-visible
    interrupted-turn marker.
    - Resolve the setting into `Config::agent_interrupt_message_enabled`,
    defaulting to `true` so existing behavior is unchanged.
    - Apply the setting to both live interrupted turns and interrupted fork
    snapshots.
    - Keep emitting `TurnAborted` even when the history marker is disabled.
    - Regenerate `core/config.schema.json` for the new
    `agents.interrupt_message` field.
    
    ## Testing
    
    - `cargo test -p codex-core load_config_resolves_agent_interrupt_message
    -- --nocapture`
    - `cargo test -p codex-core
    disabled_interrupted_fork_snapshot_appends_only_interrupt_event --
    --nocapture`
    - `cargo test -p codex-core
    multi_agent_v2_interrupted_marker_uses_developer_input_message --
    --nocapture`
    - `cargo test -p codex-core
    multi_agent_v2_followup_task_can_disable_interrupted_marker --
    --nocapture`
    - `cargo test -p codex-core
    multi_agent_v2_followup_task_interrupts_busy_child_without_losing_message
    -- --nocapture`
    - `cargo check -p codex-core`
  • Make MultiAgentV2 interruption markers assistant-authored (#19124)
    ## Why
    
    `MultiAgentV2` follow-up messages are delivered to agents as
    assistant-authored `InterAgentCommunication` envelopes. When
    `followup_task` used `interrupt: true`, the interrupted-turn guidance
    was still persisted as a contextual user message, so model-visible
    history made a system-generated interruption boundary look
    user-authored.
    
    This keeps interruption guidance consistent with the rest of the v2
    inter-agent message stream while preserving the legacy marker shape for
    non-v2 sessions.
    
    ## What changed
    
    - Make `interrupted_turn_history_marker` feature-aware.
    - Record the interrupted-turn marker as an assistant `OutputText`
    message when `Feature::MultiAgentV2` is enabled.
    - Keep the existing user contextual fragment for non-v2 sessions.
    - Apply the same feature-aware marker to interrupted fork snapshots.
    - Add coverage for the live `followup_task` interrupt path and the
    helper-level v2 marker shape.
    
    ## Testing
    
    - `cargo test -p codex-core
    multi_agent_v2_followup_task_interrupts_busy_child_without_losing_message
    -- --nocapture`
    - `cargo test -p codex-core
    multi_agent_v2_interrupted_marker_uses_assistant_output_message --
    --nocapture`
    - `cargo test -p codex-core interrupted_fork_snapshot -- --nocapture`
  • feat: let model providers own model discovery (#18950)
    ## Why
    
    `codex-models-manager` had grown to own provider-specific concerns:
    constructing OpenAI-compatible `/models` requests, resolving provider
    auth, emitting request telemetry, and deciding how provider catalogs
    should be sourced. That made the manager harder to reuse for providers
    whose model catalog is not fetched from the OpenAI `/models` endpoint,
    such as Amazon Bedrock.
    
    This change moves provider-specific model discovery behind
    provider-owned implementations, so the models manager can focus on
    refresh policy, cache behavior, picker ordering, and model metadata
    merging.
    
    ## What Changed
    
    - Introduced a `ModelsManager` trait with separate `OpenAiModelsManager`
    and `StaticModelsManager` implementations.
    - Added `ModelsEndpointClient` so OpenAI-compatible HTTP fetching lives
    outside `codex-models-manager`.
    - Moved `/models` request construction, provider auth resolution,
    timeout handling, and request telemetry into `codex-model-provider` via
    `OpenAiModelsEndpoint`.
    - Added provider-owned `models_manager(...)` construction so configured
    OpenAI-compatible providers use `OpenAiModelsManager`, while
    static/catalog-backed providers can return `StaticModelsManager`.
    - Added an Amazon Bedrock static model catalog for the GPT OSS Bedrock
    model IDs.
    - Updated core/session/thread manager code and tests to depend on
    `Arc<dyn ModelsManager>`.
    - Moved offline model test helpers into
    `codex_models_manager::test_support`.
    ## Metadata References
    
    The Bedrock catalog metadata is based on the official Amazon Bedrock
    OpenAI model documentation:
    
    - [Amazon Bedrock OpenAI
    models](https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-openai.html)
    lists the Bedrock model IDs, text input/output modalities, and `128,000`
    token context window for `gpt-oss-20b` and `gpt-oss-120b`.
    - [Amazon Bedrock `gpt-oss-120b` model
    card](https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-oss-120b.html)
    lists the `bedrock-runtime` model ID `openai.gpt-oss-120b-1:0`, the
    `bedrock-mantle` model ID `openai.gpt-oss-120b`, text-only modalities,
    and `128K` context window.
    - [OpenAI `gpt-oss-120b` model
    docs](https://developers.openai.com/api/docs/models/gpt-oss-120b)
    document configurable reasoning effort with `low`, `medium`, and `high`,
    plus text input/output modality.
    
    The display names, default reasoning effort, and priority ordering are
    Codex-local catalog choices.
    
    ## Test Plan
    - Manually verified app-server model listing with an AWS profile:
    
    ```shell
    CODEX_HOME="$(mktemp -d)" cargo run -p codex-app-server-test-client -- \
      --codex-bin ./target/debug/codex \
      -c 'model_provider="amazon-bedrock"' \
      -c 'model_providers.amazon-bedrock.aws.profile="codex-bedrock"' \
      -c 'model_providers.amazon-bedrock.aws.region="us-west-2"' \
      model-list
    ```
    
    The response returned the Bedrock catalog with `openai.gpt-oss-120b-1:0`
    as the default model and `openai.gpt-oss-20b-1:0` as the second listed
    model, both text-only and supporting low/medium/high reasoning effort.
  • feat(auto-review) short-circuit (#18890)
    ## Summary
    Short circuit the convo if auto-review hits too many denials
    
    ## Testing
    - [x] Added unit tests
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • [codex-analytics] guardian review TTFT plumbing and emission (#17696)
    ## Why
    
    Guardian analytics includes time-to-first-token, but the Guardian
    reviewer runs as a normal Codex session and `TurnCompleteEvent` did not
    expose TTFT. The timing needs to flow through the standard
    turn-completion protocol so Guardian review analytics can consume the
    same value as the rest of the session machinery.
    
    ## What changed
    
    Adds optional `time_to_first_token_ms` to `TurnCompleteEvent` and
    populates it from `TurnTiming`. The value is carried through app-server
    thread history, rollout reconstruction, TUI/app-server adapters, and
    Guardian review session handling.
    
    Guardian review analytics now captures TTFT from the reviewer
    turn-complete event when available. Existing tests and fixtures are
    updated to set the new optional field to `None` where TTFT is not
    relevant.
    
    ## Verification
    
    - `cargo clippy -p codex-tui --tests -- -D warnings`
    - `cargo clippy -p codex-core --lib --tests -- -D warnings`
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/17696).
    * __->__ #17696
    * #17695
    * #17693
    * #18278
    * #18953
  • Organize context fragments (#18794)
    Organize context fragments under `core/context`. Implement same trait on
    all of them.
  • feat: add metric to track the number of turns with memory usage (#18662)
    Add a metric `codex.turn.memory` to know if a turn used memories or not.
    This is not part of the other turn metrics as a label to limit
    cardinality
  • Move codex module under session (#18249)
    ## Summary
    - rename the core codex module root to session/mod.rs without using
    #[path]
    - move the codex module directory and tests under core/src/session
    - remove session/mod.rs reexports so call sites use explicit child
    module paths
    
    ## Testing
    - cargo test -p codex-core --lib
    - cargo check -p codex-core --tests
    - just fmt
    - just fix -p codex-core
    - git diff --check
  • [codex-analytics] feature plumbing and emittance (#16640)
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/16640).
    * #16870
    * #16706
    * #16641
    * __->__ #16640
  • 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.
  • [codex] reduce module visibility (#16978)
    ## Summary
    - reduce public module visibility across Rust crates, preferring private
    or crate-private modules with explicit crate-root public exports
    - update external call sites and tests to use the intended public crate
    APIs instead of reaching through module trees
    - add the module visibility guideline to AGENTS.md
    
    ## Validation
    - `cargo check --workspace --all-targets --message-format=short` passed
    before the final fix/format pass
    - `just fix` completed successfully
    - `just fmt` completed successfully
    - `git diff --check` passed
  • [codex-analytics] add protocol-native turn timestamps (#16638)
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/16638).
    * #16870
    * #16706
    * #16659
    * #16641
    * #16640
    * __->__ #16638
  • remove temporary ownership re-exports (#16626)
    Stacked on #16508.
    
    This removes the temporary `codex-core` / `codex-login` re-export shims
    from the ownership split and rewrites callsites to import directly from
    `codex-model-provider-info`, `codex-models-manager`, `codex-api`,
    `codex-protocol`, `codex-feedback`, and `codex-response-debug-context`.
    
    No behavior change intended; this is the mechanical import cleanup layer
    split out from the ownership move.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • core: cut codex-core compile time 48% with native async SessionTask (#16631)
    ## Why
    
    This continues the compile-time cleanup from #16630. `SessionTask`
    implementations are monomorphized, but `Session` stores the task behind
    a `dyn` boundary so it can drive and abort heterogenous turn tasks
    uniformly. That means we can move the `#[async_trait]` expansion off the
    implementation trait, keep a small boxed adapter only at the storage
    boundary, and preserve the existing task lifecycle semantics while
    reducing the amount of generated async-trait glue in `codex-core`.
    
    One measurement caveat showed up while exploring this: a warm
    incremental benchmark based on `touch core/src/tasks/mod.rs && cargo
    check -p codex-core --lib` was basically flat, but that was the wrong
    benchmark for this change. Using package-clean `codex-core` rebuilds,
    like #16630, shows the real win.
    
    Relevant pre-change code:
    
    - [`SessionTask` with
    `#[async_trait]`](https://github.com/openai/codex/blob/3c7f013f9735e67796c70d95f75f436b7f97e3ec/codex-rs/core/src/tasks/mod.rs#L129-L182)
    - [`RunningTask` storing `Arc<dyn
    SessionTask>`](https://github.com/openai/codex/blob/3c7f013f9735e67796c70d95f75f436b7f97e3ec/codex-rs/core/src/state/turn.rs#L69-L77)
    
    ## What changed
    
    - Switched `SessionTask::{run, abort}` to native RPITIT futures with
    explicit `Send` bounds.
    - Added a private `AnySessionTask` adapter that boxes those futures only
    at the `Arc<dyn ...>` storage boundary.
    - Updated `RunningTask` to store `Arc<dyn AnySessionTask>` and removed
    `#[async_trait]` from the concrete task impls plus test-only
    `SessionTask` impls.
    
    ## Timing
    
    Benchmarked package-clean `codex-core` rebuilds with dependencies left
    warm:
    
    ```shell
    cargo check -p codex-core --lib >/dev/null
    cargo clean -p codex-core >/dev/null
    /usr/bin/time -p cargo +nightly rustc -p codex-core --lib -- \
      -Z time-passes \
      -Z time-passes-format=json >/dev/null
    ```
    
    | revision | rustc `total` | process `real` | `generate_crate_metadata`
    | `MIR_borrow_checking` | `monomorphization_collector_graph_walk` |
    | --- | ---: | ---: | ---: | ---: | ---: |
    | parent `3c7f013f9735` | 67.21s | 67.71s | 24.61s | 23.43s | 22.43s |
    | this PR `2cafd783ac22` | 35.08s | 35.60s | 8.01s | 7.25s | 7.15s |
    | delta | -47.8% | -47.4% | -67.5% | -69.1% | -68.1% |
    
    For completeness, the warm touched-file benchmark stayed flat (`1.96s`
    parent vs `1.97s` this PR), which is why that benchmark should not be
    used to evaluate this refactor.
    
    ## Verification
    
    - Ran `cargo test -p codex-core`; this change compiled and task-related
    tests passed before hitting the same unrelated 5
    `config::tests::*guardian*` failures already present on the parent
    stack.
  • core: remove cross-crate re-exports from lib.rs (#16512)
    ## Why
    
    `codex-core` was re-exporting APIs owned by sibling `codex-*` crates,
    which made downstream crates depend on `codex-core` as a proxy module
    instead of the actual owner crate.
    
    Removing those forwards makes crate boundaries explicit and lets leaf
    crates drop unnecessary `codex-core` dependencies. In this PR, this
    reduces the dependency on `codex-core` to `codex-login` in the following
    files:
    
    ```
    codex-rs/backend-client/Cargo.toml
    codex-rs/mcp-server/tests/common/Cargo.toml
    ```
    
    ## What
    
    - Remove `codex-rs/core/src/lib.rs` re-exports for symbols owned by
    `codex-login`, `codex-mcp`, `codex-rollout`, `codex-analytics`,
    `codex-protocol`, `codex-shell-command`, `codex-sandboxing`,
    `codex-tools`, and `codex-utils-path`.
    - Delete the `default_client` forwarding shim in `codex-rs/core`.
    - Update in-crate and downstream callsites to import directly from the
    owning `codex-*` crate.
    - Add direct Cargo dependencies where callsites now target the owner
    crate, and remove `codex-core` from `codex-rs/backend-client`.
  • feat: add mailbox concept for wait (#16010)
    Add a mailbox we can use for inter-agent communication
    `wait` is now based on it and don't take target anymore
  • feat: disable notifier v2 and start turn on agent interaction (#15624)
    Make the inter-agent communication start a turn
    
    As part of this, we disable the v2 notifier to prevent some odd
    behaviour where the agent restart working while you're talking to it for
    example
  • Add fork snapshot modes (#15239)
    ## Summary
    - add `ForkSnapshotMode` to `ThreadManager::fork_thread` so callers can
    request either a committed snapshot or an interrupted snapshot
    - share the model-visible `<turn_aborted>` history marker between the
    live interrupt path and interrupted forks
    - update the small set of direct fork callsites to pass
    `ForkSnapshotMode::Committed`
    
    Note: this enables /btw to work similarly as Esc to interrupt (hopefully
    somewhat in distribution)
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • feat: new op type for sub-agents communication (#15556)
    Add `InterAgentCommunication` for v2 agent communication
  • feat: structured multi-agent output (#15515)
    Send input now sends messages as assistant message and with this format:
    
    ```
    author: /root/worker_a
    recipient: /root/worker_a/tester
    other_recipients: []
    Content: bla bla bla. Actual content. Only text for now
    ```
  • Split features into codex-features crate (#15253)
    - Split the feature system into a new `codex-features` crate.
    - Cut `codex-core` and workspace consumers over to the new config and
    warning APIs.
    
    Co-authored-by: Ahmed Ibrahim <219906144+aibrahim-oai@users.noreply.github.com>
    Co-authored-by: Codex <noreply@openai.com>
  • Forward session and turn headers to MCP HTTP requests (#15011)
    ## Summary
    - forward request-scoped task headers through MCP tool metadata lookups
    and tool calls
    - apply those headers to streamable HTTP initialize, tools/list, and
    tools/call requests
    - update affected rmcp/core tests for the new request_headers plumbing
    
    ## Testing
    - cargo test -p codex-rmcp-client
    - cargo test -p codex-core (fails on pre-existing unrelated error in
    core/src/auth_env_telemetry.rs: missing websocket_connect_timeout_ms in
    ModelProviderInfo initializer)
    - just fix -p codex-rmcp-client
    - just fix -p codex-core (hits the same unrelated auth_env_telemetry.rs
    error)
    - just fmt
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • [hooks] userpromptsubmit - hook before user's prompt is executed (#14626)
    - this allows blocking the user's prompts from executing, and also
    prevents them from entering history
    - handles the edge case where you can both prevent the user's prompt AND
    add n amount of additionalContexts
    - refactors some old code into common.rs where hooks overlap
    functionality
    - refactors additionalContext being previously added to user messages,
    instead we use developer messages for them
    - handles queued messages correctly
    
    Sample hook for testing - if you write "[block-user-submit]" this hook
    will stop the thread:
    
    example run
    ```
    › sup
    
    
    • Running UserPromptSubmit hook: reading the observatory notes
    
    UserPromptSubmit hook (completed)
      warning: wizard-tower UserPromptSubmit demo inspected: sup
      hook context: Wizard Tower UserPromptSubmit demo fired. For this reply only, include the exact
    phrase 'observatory lanterns lit' exactly once near the end.
    
    • Just riding the cosmic wave and ready to help, my friend. What are we building today? observatory
      lanterns lit
    
    
    › and [block-user-submit]
    
    
    • Running UserPromptSubmit hook: reading the observatory notes
    
    UserPromptSubmit hook (stopped)
      warning: wizard-tower UserPromptSubmit demo blocked the prompt on purpose.
      stop: Wizard Tower demo block: remove [block-user-submit] to continue.
    ```
    
    .codex/config.toml
    ```
    [features]
    codex_hooks = true
    ```
    
    .codex/hooks.json
    ```
    {
      "hooks": {
        "UserPromptSubmit": [
          {
            "hooks": [
              {
                "type": "command",
                "command": "/usr/bin/python3 .codex/hooks/user_prompt_submit_demo.py",
                "timeoutSec": 10,
                "statusMessage": "reading the observatory notes"
              }
            ]
          }
        ]
      }
    }
    ```
    
    .codex/hooks/user_prompt_submit_demo.py
    ```
    #!/usr/bin/env python3
    
    import json
    import sys
    from pathlib import Path
    
    
    def prompt_from_payload(payload: dict) -> str:
        prompt = payload.get("prompt")
        if isinstance(prompt, str) and prompt.strip():
            return prompt.strip()
    
        event = payload.get("event")
        if isinstance(event, dict):
            user_prompt = event.get("user_prompt")
            if isinstance(user_prompt, str):
                return user_prompt.strip()
    
        return ""
    
    
    def main() -> int:
        payload = json.load(sys.stdin)
        prompt = prompt_from_payload(payload)
        cwd = Path(payload.get("cwd", ".")).name or "wizard-tower"
    
        if "[block-user-submit]" in prompt:
            print(
                json.dumps(
                    {
                        "systemMessage": (
                            f"{cwd} UserPromptSubmit demo blocked the prompt on purpose."
                        ),
                        "decision": "block",
                        "reason": (
                            "Wizard Tower demo block: remove [block-user-submit] to continue."
                        ),
                    }
                )
            )
            return 0
    
        prompt_preview = prompt or "(empty prompt)"
        if len(prompt_preview) > 80:
            prompt_preview = f"{prompt_preview[:77]}..."
    
        print(
            json.dumps(
                {
                    "systemMessage": (
                        f"{cwd} UserPromptSubmit demo inspected: {prompt_preview}"
                    ),
                    "hookSpecificOutput": {
                        "hookEventName": "UserPromptSubmit",
                        "additionalContext": (
                            "Wizard Tower UserPromptSubmit demo fired. "
                            "For this reply only, include the exact phrase "
                            "'observatory lanterns lit' exactly once near the end."
                        ),
                    },
                }
            )
        )
        return 0
    
    
    if __name__ == "__main__":
        raise SystemExit(main())
    ```
  • fix(core): prevent hanging turn/start due to websocket warming issues (#14838)
    ## Description
    
    This PR fixes a bad first-turn failure mode in app-server when the
    startup websocket prewarm hangs. Before this change, `initialize ->
    thread/start -> turn/start` could sit behind the prewarm for up to five
    minutes, so the client would not see `turn/started`, and even
    `turn/interrupt` would block because the turn had not actually started
    yet.
    
    Now, we:
    - set a (configurable) timeout of 15s for websocket startup time,
    exposed as `websocket_startup_timeout_ms` in config.toml
    - `turn/started` is sent immediately on `turn/start` even if the
    websocket is still connecting
    - `turn/interrupt` can be used to cancel a turn that is still waiting on
    the websocket warmup
    - the turn task will wait for the full 15s websocket warming timeout
    before falling back
    
    ## Why
    
    The old behavior made app-server feel stuck at exactly the moment the
    client expects turn lifecycle events to start flowing. That was
    especially painful for external clients, because from their point of
    view the server had accepted the request but then went silent for
    minutes.
    
    ## Configuring the websocket startup timeout
    Can set it in config.toml like this:
    ```
    [model_providers.openai]
    supports_websockets = true
    websocket_connect_timeout_ms = 15000
    ```
  • Apply argument comment lint across codex-rs (#14652)
    ## Why
    
    Once the repo-local lint exists, `codex-rs` needs to follow the
    checked-in convention and CI needs to keep it from drifting. This commit
    applies the fallback `/*param*/` style consistently across existing
    positional literal call sites without changing those APIs.
    
    The longer-term preference is still to avoid APIs that require comments
    by choosing clearer parameter types and call shapes. This PR is
    intentionally the mechanical follow-through for the places where the
    existing signatures stay in place.
    
    After rebasing onto newer `main`, the rollout also had to cover newly
    introduced `tui_app_server` call sites. That made it clear the first cut
    of the CI job was too expensive for the common path: it was spending
    almost as much time installing `cargo-dylint` and re-testing the lint
    crate as a representative test job spends running product tests. The CI
    update keeps the full workspace enforcement but trims that extra
    overhead from ordinary `codex-rs` PRs.
    
    ## What changed
    
    - keep a dedicated `argument_comment_lint` job in `rust-ci`
    - mechanically annotate remaining opaque positional literals across
    `codex-rs` with exact `/*param*/` comments, including the rebased
    `tui_app_server` call sites that now fall under the lint
    - keep the checked-in style aligned with the lint policy by using
    `/*param*/` and leaving string and char literals uncommented
    - cache `cargo-dylint`, `dylint-link`, and the relevant Cargo
    registry/git metadata in the lint job
    - split changed-path detection so the lint crate's own `cargo test` step
    runs only when `tools/argument-comment-lint/*` or `rust-ci.yml` changes
    - continue to run the repo wrapper over the `codex-rs` workspace, so
    product-code enforcement is unchanged
    
    Most of the code changes in this commit are intentionally mechanical
    comment rewrites or insertions driven by the lint itself.
    
    ## Verification
    
    - `./tools/argument-comment-lint/run.sh --workspace`
    - `cargo test -p codex-tui-app-server -p codex-tui`
    - parsed `.github/workflows/rust-ci.yml` locally with PyYAML
    
    ---
    
    * -> #14652
    * #14651
  • Preserve background terminals on interrupt and rename cleanup command to /stop (#14602)
    ### Motivation
    - Interrupting a running turn (Ctrl+C / Esc) currently also terminates
    long‑running background shells, which is surprising for workflows like
    local dev servers or file watchers.
    - The existing cleanup command name was confusing; callers expect an
    explicit command to stop background terminals rather than a UI clear
    action.
    - Make background‑shell termination explicit and surface a clearer
    command name while preserving backward compatibility.
    
    ### Description
    - Renamed the background‑terminal cleanup slash command from `Clean`
    (`/clean`) to `Stop` (`/stop`) and kept `clean` as an alias in the
    command parsing/visibility layer, updated the user descriptions and
    command popup wiring accordingly.
    - Updated the unified‑exec footer text and snapshots to point to `/stop`
    (and trimmed corresponding snapshot output to match the new label).
    - Changed interrupt behavior so `Op::Interrupt` (Ctrl+C / Esc interrupt)
    no longer closes or clears tracked unified exec / background terminal
    processes in the TUI or core cleanup path; background shells are now
    preserved after an interrupt.
    - Updated protocol/docs to clarify that `turn/interrupt` (or
    `Op::Interrupt`) interrupts the active turn but does not terminate
    background terminals, and that `thread/backgroundTerminals/clean` is the
    explicit API to stop those shells.
    - Updated unit/integration tests and insta snapshots in the TUI and core
    unified‑exec suites to reflect the new semantics and command name.
    
    ### Testing
    - Ran formatting with `just fmt` in `codex-rs` (succeeded). 
    - Ran `cargo test -p codex-protocol` (succeeded). 
    - Attempted `cargo test -p codex-tui` but the build could not complete
    in this environment due to a native build dependency that requires
    `libcap` development headers (the `codex-linux-sandbox` vendored build
    step); install `libcap-dev` / make `libcap.pc` available in
    `PKG_CONFIG_PATH` to run the TUI test suite locally.
    - Updated and accepted the affected `insta` snapshots for the TUI
    changes so visual diffs reflect the new `/stop` wording and preserved
    interrupt behavior.
    
    ------
    [Codex
    Task](https://chatgpt.com/codex/tasks/task_i_69b39c44b6dc8323bd133ae206310fae)
  • [js_repl] Hard-stop active js_repl execs on explicit user interrupts (#13329)
    ## Summary
    - hard-stop `js_repl` only for `TurnAbortReason::Interrupted`,
    preserving the persistent REPL across replaced turns
    - track the current top-level exec by turn and only reset when the
    interrupted turn owns submitted work or a freshly started kernel for the
    current exec attempt
    - close both interrupt races: the write-window race by marking the exec
    as submitted before async pipe writes begin, and the startup-window race
    by tracking fresh-kernel ownership until submission
    - add regression coverage for interrupted in-flight execs and the
    pending-kernel-start window
    
    ## Why
    Stopping a turn previously surfaced `aborted by user after Xs` even
    though the underlying `js_repl` kernel could continue executing. Earlier
    fixes also risked resetting the session-scoped REPL too broadly or
    missing already-dispatched work. This change keeps cleanup scoped to
    explicit stop semantics and makes the interrupt path line up with both
    submitted execs and newly started kernels.
    
    ## Testing
    - `just fmt`
    - `cargo test -p codex-core`
    - `just fix -p codex-core`
    
    `cargo test -p codex-core` passes the updated `js_repl` coverage,
    including the new startup-window regression test, but still has
    unrelated integration failures in this environment outside `js_repl`.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • fix: move inline codex-rs/core unit tests into sibling files (#14444)
    ## Why
    PR #13783 moved the `codex.rs` unit tests into `codex_tests.rs`. This
    applies the same extraction pattern across the rest of `codex-rs/core`
    so the production modules stay focused on runtime code instead of large
    inline test blocks.
    
    Keeping the tests in sibling files also makes follow-up edits easier to
    review because product changes no longer have to share a file with
    hundreds or thousands of lines of test scaffolding.
    
    ## What changed
    - replaced each inline `mod tests { ... }` in `codex-rs/core/src/**`
    with a path-based module declaration
    - moved each extracted unit test module into a sibling `*_tests.rs`
    file, using `mod_tests.rs` for `mod.rs` modules
    - preserved the existing `cfg(...)` guards and module-local structure so
    the refactor remains structural rather than behavioral
    
    ## Testing
    - `cargo test -p codex-core --lib` (`1653 passed; 0 failed; 5 ignored`)
    - `just fix -p codex-core`
    - `cargo fmt --check`
    - `cargo shear`
  • feat(core): emit turn metric for network proxy state (#14250)
    ## Summary
    - add a per-turn `codex.turn.network_proxy` metric constant
    - emit the metric from turn completion using the live managed proxy
    enabled state
    - add focused tests for active and inactive tag emission
  • feat(otel): Centralize OTEL metric names and shared tag builders (#14117)
    This cleans up a bunch of metric plumbing that had started to drift.
    
    The main change is making `codex-otel` the canonical home for shared
    metric definitions and metric tag helpers. I moved the `turn/thread`
    metric names that were still duplicated into the OTEL metric registry,
    added a shared `metrics::tags` module for common tag keys and session
    tag construction, and updated `SessionTelemetry` to build its metadata
    tags through that shared path.
    
    On the codex-core side, TTFT/TTFM now use the shared metric-name
    constants instead of local string definitions. I also switched the
    obvious remaining turn/thread metric callsites over to the shared
    constants, and added a small helper so TTFT/TTFM can attach an optional
    sanitized client.name tag from TurnContext.
    
    This should make follow-on telemetry work less ad hoc:
    - one canonical place for metric names
    - one canonical place for common metric tag keys/builders
    - less duplication between `codex-core` and `codex-otel`
  • Stabilize interrupted task approval cleanup (#14102)
    ## Summary
    - drain the active turn tasks before clearing pending approvals during
    interruption
    - keep the turn in hand long enough for interrupted tasks to observe
    cancellation first
    
    ## Why this fixes the flake
    Interrupted turns could clear pending approvals too early, which let an
    in-flight approval wait surface as a model-visible rejection before the
    turn emitted `TurnAborted`. Reordering the cleanup removes that race
    without changing the steady-state task model.