Commit Graph

5946 Commits

  • core tests: migrate more turns to permission profiles (#20013)
    ## Summary
    - Migrate another batch of direct `Op::UserTurn` test construction from
    legacy `SandboxPolicy` values to `PermissionProfile` inputs via
    `turn_permission_fields()`.
    - Replace a one-off read-only `SandboxPolicy` bridge in the macOS exec
    test with `PermissionProfile::read_only()`.
    - Reduce `SandboxPolicy` references in `codex-rs/core/tests` from 32
    files at the start of the cleanup stack to 27 files.
    
    ## Testing
    - `cargo check -p codex-core --tests`
    - `just fmt`
    - `just fix -p codex-core`
  • core tests: build user turns from permission profiles (#20011)
    ## Summary
    - Add `turn_permission_fields()` so tests that construct `Op::UserTurn`
    directly can provide a canonical `PermissionProfile` while still filling
    the required legacy `sandbox_policy` compatibility field.
    - Migrate direct user-turn construction in core integration tests from
    `SandboxPolicy::DangerFullAccess` to `PermissionProfile::Disabled`.
    - Continue reducing direct `SandboxPolicy` usage in
    `codex-rs/core/tests`, from 41 files after #20010 to 32 files in this
    PR.
    
    ## Testing
    - `cargo check -p codex-core --tests`
    - `just fmt`
    - `just fix -p core_test_support`
    - `just fix -p codex-core`
  • Refine Codex issue digest summaries (#20097)
    ## Why
    
    The `codex-issue-digest` skill was producing more detail than the daily
    digest needed, and broad all-area digests could miss active issues. In
    particular, issue #16088 had substantial recent comments and reactions
    but did not appear in the weekly all-areas output because GitHub search
    was using default relevance ranking and the collector could exhaust its
    candidate cap before later search queries got a fair sample.
    
    That made the digest look quieter than the underlying user activity and
    made threshold tuning misleading.
    
    ## What changed
    
    - Make the digest summary headline-first and summary-only by default.
    - Add an explicit opt-in flow for `## Details`, so the issue table is
    shown only when requested or when the prompt asks for details upfront.
    - Update the collector to request GitHub issue search results with
    `sort=updated` and `order=desc`.
    - Apply the search candidate cap per query instead of globally across
    all queries.
    - Bump the collector script version to `3`.
    - Add tests that cover updated sorting and per-query candidate limits.
    
    ## Verification
    
    - `pytest
    .codex/skills/codex-issue-digest/scripts/test_collect_issue_digest.py`
    - `ruff check
    .codex/skills/codex-issue-digest/scripts/collect_issue_digest.py
    .codex/skills/codex-issue-digest/scripts/test_collect_issue_digest.py`
    - `git diff --check`
    - Reran the all-areas weekly collector and confirmed #16088 is now
    included with `55` interactions.
  • app-server: notify clients of remote-control status changes (#19919)
    ## Why
    
    Remote-control app-server enrollments have both an internal server id
    and the environment id exposed to remote-control clients. App-server
    clients need one current status snapshot that says whether remote
    control is usable and which environment id, if any, is exposed.
    
    A temporary websocket disconnect is not itself an identity change.
    Account changes, stale enrollment invalidation, successful
    re-enrollment, and missing ChatGPT auth are meaningful status changes.
    Disabled remote control remains `disabled` regardless of auth or SQLite
    state. SQLite startup failure disablement and enrollment persistence
    failures are handled in #20068; this PR reports the resulting effective
    status to clients.
    
    ## What changed
    
    - Adds v2 `remoteControl/status/changed` carrying `state` and
    `environmentId`.
    - Adds `RemoteControlConnectionState` values: `disabled`, `connecting`,
    `connected`, and `errored`.
    - Exposes remote-control status updates through `RemoteControlHandle`
    using a Tokio watch channel.
    - Always sends the current remote-control status snapshot to newly
    initialized app-server clients.
    - Broadcasts status changes to initialized app-server clients when state
    or environment id changes.
    - Treats missing ChatGPT auth as an `errored` status while leaving it
    retryable because auth can change at runtime.
    - Clears `environmentId` when enrollment is cleared for account changes,
    auth loss, stale backend invalidation, or disabled remote control.
    - Updates app-server protocol schema fixtures, generated TypeScript,
    app-server README, remote-control tests, and TUI exhaustive notification
    matches.
    
    ## Stack
    
    - Builds on #20068.
    
    ## Verification
    
    - `just write-app-server-schema`
    - `cargo test -p codex-app-server-protocol`
    - `cargo test -p codex-app-server transport::remote_control --lib`
    - `cargo check -p codex-tui`
    - `just fix -p codex-app-server-protocol`
    - `just fix -p codex-app-server`
    - `just fix -p codex-tui`
  • Return None when auth refresh fails (#20092)
    Right now, if Codex winds up in a state with auth but it can't refresh
    the token, the user is left with an unhelpful message that says to log
    out and log back in again.
    
    Ultimately, we should prevent that from happening but if it does,
    returning None will allow the caller to redirect the user back to the
    login page
  • core tests: submit turns with permission profiles (#20010)
    ## Summary
    
    - Add `PermissionProfile`-based turn submission helpers to
    `core_test_support`, while keeping the legacy `SandboxPolicy` helper for
    tests that intentionally exercise legacy fallback behavior.
    - Switch the default `TestCodex::submit_turn()` path to send a real
    `PermissionProfile` plus the required legacy compatibility projection in
    `Op::UserTurn`.
    - Migrate straightforward app/search/shell/truncation tests from
    `SandboxPolicy::{DangerFullAccess, ReadOnly}` to
    `PermissionProfile::{Disabled, read_only}`.
    - Add a TUI compatibility projection helper for legacy app-server fields
    so non-legacy writable roots are preserved instead of being downgraded
    to read-only.
    - Fix remote start/resume/fork sandbox-mode projection to classify any
    managed profile with writable roots as workspace-write, not only
    profiles that can write `cwd`.
    - Reduce `SandboxPolicy` references in `codex-rs/core/tests` from 47
    files to 41 files without changing production behavior.
    
    ## Testing
    
    - `cargo check -p codex-core --tests`
    - `cargo test -p codex-tui
    compatibility_profile_preserves_unbridgeable_write_roots`
    - `cargo test -p codex-tui
    sandbox_mode_preserves_non_cwd_write_roots_for_remote_sessions`
    - `just fmt`
    - `just fix -p core_test_support`
    - `just fix -p codex-core`
  • fix(network-proxy): normalize network proxy host matching (#19995)
    ## Why
    The proxy matches allow and deny rules against normalized host strings.
    Scoped IPv6 literals can arrive in equivalent forms, such as
    `fd00::1%eth0`, `[fd00::1%eth0]`, or `[fd00::1%25eth0]`. Policy should
    canonicalize those spellings without erasing scope granularity: an
    unscoped rule like `fd00::1` should still cover scoped requests for that
    address, while a scoped rule like `fd00::1%eth0` should remain exact to
    that scope.
    
    ## What changed
    - preserve IPv6 scope IDs during host normalization and canonicalize
    `%25scope` to `%scope`
    - match policy against the exact normalized host plus the unscoped IP
    base for scoped literals
    - keep local-address explicit allow checks aligned with the same
    scoped/unscoped semantics
    - add focused coverage for scoped IPv6 normalization, scoped allow
    rules, and scoped deny rules in `network-proxy`
    
    ## Security impact
    A request cannot bypass a broad deny rule by adding an IPv6 scope
    suffix. At the same time, scoped policy remains precise:
    `deny=fd00::1%eth0` affects that scoped spelling without collapsing
    `fd00::1%eth1` onto the same key, and `allow=fe80::1%eth0` does not
    implicitly allow other scopes.
    
    ## Verification
    - `just fmt`
    - `cargo test -p codex-network-proxy`
    - `just fix -p codex-network-proxy`
    - `git diff --check`
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
    Co-authored-by: evawong-oai <evawong@openai.com>
  • Fix flaky plugin hook env test (#20088)
    The test was flaky because it was checking the right thing in a
    roundabout way.
    
    What it wanted to prove:
    - plugin hooks receive the right environment variables.
    
    What it actually did:
    1. Run a plugin hook.
    2. Have that hook write those env vars into a temporary `env.json` file.
    3. After the hook finished, read `env.json` back from disk.
    
    On Windows, that last file was sometimes not there when the test tried
    to read it, so the test failed with `read env log: file not found`. The
    hook system itself was not what the test failure was directly proving;
    the test was failing on the extra filesystem side effect it introduced.
    
    The fix is to stop using a temp file as the proof mechanism. The hook
    now prints the env values in its normal structured output, and the test
    asserts on the output that the hook engine already captures. So we still
    verify the same behavior, but without depending on a separate file being
    created and read back correctly on Windows.
  • fix: don't auto approve git -C ... (#20085)
    It's safer to make sure these commands go through approval flows.
  • /plugins: add marketplace install flow (#18704)
    This PR adds a new feature to the `/plugins` menu that gives users the
    ability to add new plugin marketplaces. It introduces an Add Marketplace
    tab to the right of installed marketplaces, a source prompt, loading and
    error states, and the app-server request flow needed to perform the
    install. After a successful `marketplace/add`, the popup refreshes back
    into the newly added marketplace tab so the new plugins are immediately
    visible.
    
    - Add an Add Marketplace tab to the `/plugins` menu
    - Prompt for marketplace source input from git repo, URL, or local path
    - Show loading and error states during `marketplace/add`
    - Refresh plugin data after success and switch into the newly added
    marketplace tab
    - Add tests and snapshot updates
  • Discover hooks bundled with plugins (#19705)
    ## Why
    
    Plugins can bundle lifecycle hooks, but Codex previously only discovered
    hooks from user, project, and managed config layers. This adds the
    plugin discovery and runtime plumbing needed for plugin-bundled hooks
    while keeping execution behind the `plugin_hooks` feature flag.
    
    ## What
    
    - Discovers plugin hook sources from each plugin's default
    `hooks/hooks.json`.
    - Supports `plugin.json` manifest `hooks` entries as either relative
    paths or inline hook objects.
    - Plumbs discovered plugin hook sources through plugin loading into the
    hook runtime when `plugin_hooks` is enabled.
    - Marks plugin-originated hook runs as `HookSource::Plugin`.
    - Injects `PLUGIN_ROOT` and `CLAUDE_PLUGIN_ROOT` into plugin hook
    command environments.
    - Updates generated schemas and hook source metadata for the plugin hook
    source.
    
    ## Stack
    
    1. This PR - openai/codex#19705
    2. openai/codex#19778
    3. openai/codex#19840
    4. openai/codex#19882
    
    ## Reviewer Notes
    
    - Core logic is in `codex-rs/core-plugins/src/loader.rs` and
    `codex-rs/hooks/src/engine/discovery.rs`
    - Moved existing / adding new tests to
    `codex-rs/core-plugins/src/loader_tests.rs` hence the large diff there
    - Otherwise mostly plumbing and minor schema updates
    
    ### Core Changes
    
    The `codex-rs/core` changes are limited to wiring plugin hook support
    into existing core flows:
    
    - `core/src/session/session.rs` conditionally pulls effective plugin
    hook sources and plugin hook load warnings from `PluginsManager` when
    `plugin_hooks` is enabled, then passes them into `HooksConfig`.
    - `core/src/hook_runtime.rs` adds the `plugin` metric tag for
    `HookSource::Plugin`.
    - `core/config.schema.json` picks up the new `plugin_hooks` feature
    flag, and `core/src/plugins/manager_tests.rs` updates fixtures for the
    added plugin hook fields.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • [rollout-trace] Include x-request-id in rollout trace. (#20066)
    ## Why
    
    Rollout traces need an identifier that can be used to correlate a Codex
    inference with upstream Responses API, proxy, and engine logs. The
    reduced trace model already exposed `upstream_request_id`, but it was
    being populated from the Responses API `response.id`. That value is
    useful for `previous_response_id` chaining, but it is not the transport
    request id that upstream systems key on.
    
    This PR separates those concepts so trace consumers can reliably answer
    both questions:
    
    - which Responses API response did this inference produce?
    - which upstream request handled it?
    
    ## Structure
    
    The change keeps the upstream request id at the same lifecycle level as
    the provider stream:
    
    - `codex-api` captures the `x-request-id` HTTP response header when the
    SSE stream is created and exposes it on `ResponseStream`. Fixture and
    websocket streams set the field to `None` because they do not have that
    HTTP response header.
    - `codex-core` carries that stream-level id into `InferenceTraceAttempt`
    when recording terminal stream outcomes. Completed, failed, cancelled,
    dropped-stream, and pre-response error paths all record the id when it
    is available.
    - `rollout-trace` now records both identifiers in raw terminal inference
    events and response payloads: `response_id` for the Responses API
    `response.id`, and `upstream_request_id` for `x-request-id`.
    - The reducer stores both fields on `InferenceCall`. It also uses
    `response_id` for `previous_response_id` conversation linking, which
    removes the old accidental dependency on the misnamed
    `upstream_request_id` field.
    - Terminal inference reduction now consumes the full terminal payload
    (`InferenceCompleted`, `InferenceFailed`, or `InferenceCancelled`) in
    one place. That keeps status, partial payloads, response ids, and
    upstream request ids consistent across success, failure, cancellation,
    and late stream-mapper events.
    
    ## Why This Shape
    
    `x-request-id` is a property of the HTTP/provider response envelope, not
    an SSE event. Capturing it once in `codex-api` and plumbing it through
    terminal trace recording avoids trying to infer the value from stream
    contents, and it preserves the id even when the stream fails or is
    cancelled after only partial output.
    
    Keeping `response_id` separate from `upstream_request_id` also makes the
    reduced trace model less surprising: `response_id` remains the
    conversation-continuation id, while `upstream_request_id` is the
    operational correlation id for upstream debugging.
    
    ## Validation
    
    The PR updates trace and reducer coverage for:
    
    - reading `x-request-id` from SSE response headers;
    - storing the true upstream request id on completed inference calls;
    - preserving upstream request ids for cancelled and late-cancelled
    inference streams;
    - keeping `previous_response_id` reconstruction tied to `response_id`
    rather than transport request ids.
  • app-server: disable remote control without sqlite (#20068)
    ## Why
    
    Remote control depends on the app-server SQLite state DB for persisted
    enrollment identity. If the state DB cannot be opened at startup,
    continuing with remote control enabled leaves the process in a
    misleading state where enrollment identity cannot be read or persisted.
    
    Feature-disabled remote control remains disabled regardless of SQLite
    state. This only changes the case where remote control is requested but
    the SQLite state DB is unavailable.
    
    ## What changed
    
    - Logs SQLite state DB initialization failures instead of dropping the
    error silently.
    - Treats remote control as effectively disabled when the SQLite state DB
    is unavailable.
    - Prevents `RemoteControlHandle::set_enabled(true)` from enabling remote
    control later in the same process if the state DB was unavailable at
    startup.
    - Keeps the existing behavior that disabled remote control does not
    validate or connect to the remote-control URL.
    - Makes persisted enrollment load/update failures propagate as
    remote-control errors instead of silently falling back to in-memory
    state.
    - Makes the direct websocket connection path fail when called without a
    SQLite state DB.
    - Adds coverage for startup without a state DB, later handle enablement
    with no state DB, and direct websocket connection without a state DB.
    
    ## Verification
    
    - `cargo test -p codex-app-server transport::remote_control --lib`
    - `just fix -p codex-app-server`
  • tui: use permission profiles for sandbox state (#20008)
    ## Summary
    - Move TUI permission state from legacy `SandboxPolicy` values to
    canonical `PermissionProfile` values across presets, app events, chat
    widget state, app commands, thread routing, and cached thread session
    state.
    - Keep app-server compatibility boundaries explicit: embedded sessions
    send `permissionProfile`, while remote sessions send only a legacy
    `sandbox` projection and fall back to read-only when a custom profile
    cannot be projected.
    - Update status/add-dir UI summaries and snapshots to render the active
    permission profile, including workspace profiles selected by the new
    built-in defaults.
    
    ## Verification
    - `rg '\bSandboxPolicy\b' codex-rs/tui -n` returns no matches.
    - `cargo test -p codex-tui`
    - `cargo check -p codex-tui --tests`
    - `cargo test -p codex-tui additional_dirs`
    - `just fmt`
    - `just fix -p codex-tui`
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/20008).
    * #20041
    * #20040
    * #20037
    * #20035
    * #20034
    * #20033
    * #20032
    * #20030
    * #20028
    * #20027
    * #20026
    * #20024
    * #20021
    * #20018
    * #20016
    * #20015
    * #20013
    * #20011
    * #20010
    * __->__ #20008
  • Make MultiAgentV2 wait minimum configurable (#20052)
    ## Why
    
    MultiAgentV2 `wait_agent` currently clamps short waits to a fixed 10
    second minimum. That default is still useful for preventing tight
    polling loops, but it is too rigid for environments that need faster
    mailbox wake-up checks or a larger minimum to discourage frequent
    polling.
    
    This PR makes the minimum wait timeout configurable from the existing
    MultiAgentV2 feature config section, so operators can tune the behavior
    without changing the legacy multi-agent tool surface.
    
    ## What Changed
    
    - Added `features.multi_agent_v2.min_wait_timeout_ms`.
    - Defaulted the new setting to the existing 10 second floor.
    - Validated the configured value as `1..=3600000`, matching the existing
    one hour maximum wait bound.
    - Applied the configured minimum to MultiAgentV2 `wait_agent` runtime
    clamping.
    - Plumbed the configured minimum into the `wait_agent` tool schema,
    including the effective default when the minimum is above the normal 30
    second default.
    - Regenerated `core/config.schema.json`.
    
    ## Verification
    
    - `cargo test -p codex-features`
    - `cargo test -p codex-tools`
    - `cargo test -p codex-core --lib multi_agent_v2`
    - `just fix -p codex-core`
  • fix(network-proxy): recheck network proxy connect targets (#19999)
    ## Why
    The proxy checks the requested host before opening the upstream
    connection, but DNS can resolve an allowed hostname to a loopback,
    private, or other non-public address after that first decision. Without
    a final check on the actual socket target, a request that looks
    acceptable at the hostname layer can still connect to a local service
    once resolution completes.
    
    ## What changed
    - add a shared TCP connector check for direct proxy egress
    - use that path for HTTP, `CONNECT`, SOCKS5, and MITM upstream
    connections
    - keep configured upstream proxy hops on the existing proxy path
    - add direct-connector coverage for allowed and rejected local targets
    
    ## Security impact
    Direct proxy egress now rechecks the resolved socket address before
    connecting, closing the gap between hostname policy evaluation and the
    final network target.
    
    ## Verification
    - `cargo test -p codex-network-proxy`
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Load cloud requirements for agent identity (#19708)
    ## Why
    
    Agent Identity sessions can represent Business and Enterprise ChatGPT
    workspaces, but cloud requirements were skipped before fetch. That meant
    workspace-managed requirements were not loaded for Agent Identity even
    when the JWT carried the same account identity and plan information that
    normal ChatGPT token auth exposes.
    
    This PR now sits on top of the Agent Identity stack through
    [#19764](https://github.com/openai/codex/pull/19764). Because
    [#19763](https://github.com/openai/codex/pull/19763) moved task
    registration into Agent Identity auth loading, cloud requirements no
    longer needs a separate runtime-initialization step before building the
    backend client.
    
    ## What changed
    
    - Stop skipping `CodexAuth::AgentIdentity` in the cloud requirements
    loader.
    - Share the cloud requirements eligibility check between startup load
    and background cache refresh.
    - Rely on eagerly loaded Agent Identity auth so backend requests can
    attach task-scoped `AgentAssertion` headers.
    - Decode Agent Identity JWT `plan_type` as the auth-layer plan type,
    then convert it through a shared `auth::PlanType` -> `account::PlanType`
    mapping.
    - Add the missing serde alias for the `education` plan string and add
    coverage for raw Agent Identity plan aliases such as `hc` and
    `education`.
    
    ## Testing
    
    - `cargo test -p codex-agent-identity -p codex-login -p
    codex-cloud-requirements -p codex-protocol`
  • app-server: run initialized rpcs with keyed serialization (#17373)
    ## Why
    
    Initialized app-server RPCs no longer need to bottleneck behind one
    request processor path. Running them concurrently improves
    responsiveness, but several request families still mutate shared state
    or depend on ordered side effects. Those stateful families need an
    auditable serialization contract so concurrency does not reorder thread,
    config, auth, command, watcher, MCP, or similar state transitions.
    
    This PR keeps that boundary explicit: stateful work is serialized by the
    smallest useful key, while intentionally read-only or externally
    concurrent work remains unkeyed. In particular, `thread/list` and
    `thread/turns/list` explicitly have no serialization because they
    primarily read append-only rollout storage and should continue to be
    served concurrently.
    
    ## What changed
    
    - Adds `ClientRequest::serialization_scope()` in `app-server-protocol`
    and requires every client request definition to declare its
    serialization behavior.
    - Introduces keyed request scopes for thread, thread path, command exec
    process, fuzzy search session, fs watch, MCP OAuth, and global state
    buckets such as config, account auth, memory, and device keys.
    - Routes initialized app-server RPCs through per-key FIFO serialization
    while allowing unkeyed initialized requests to run concurrently.
    - Cancels in-flight initialized RPC work when the connection disconnects
    or the app-server exits so spawned request tasks do not outlive their
    session.
    - Adds focused coverage for representative keyed and unkeyed
    serialization scopes, including explicitly concurrent
    `thread/turns/list` behavior.
    
    ## Validation
    
    - Added protocol tests for representative keyed serialization scopes and
    intentionally unkeyed request families.
    - Added app-server request serialization tests covering per-key FIFO
    behavior, concurrent unkeyed execution, disconnect shutdown, and config
    read-after-write ordering.
    - Local focused protocol validation after the latest rebase is currently
    blocked by packageproxy failing to resolve locked `rustls-webpki
    0.103.13`; CI is expected to provide the full validation signal.
  • Fix log db batch flush flake (#19959)
    ## Why
    
    The log DB writer batches tracing events before inserting them into
    SQLite, but `tokio::time::interval` produces an immediate first tick.
    That meant the inserter could flush the first accepted log entry before
    `batch_size` was reached, making
    `configured_batch_size_flushes_without_explicit_flush` timing-sensitive
    in CI.
    
    ## What Changed
    
    - Consume the interval's startup tick before entering the inserter loop,
    so interval flushing starts after the configured delay.
    - Remove the test's startup sleep, which was masking the race instead of
    proving the batch-size behavior.
    
    ## Validation
    
    - `cargo test -p codex-state`
    - `cargo test -p codex-state
    configured_batch_size_flushes_without_explicit_flush` passed 3
    consecutive focused runs
    - PR checks passed across `rust-ci`, Bazel, `ci`, `sdk`, `cargo-deny`,
    Codespell, blob-size policy, and CLA
  • fix(network-proxy): harden linux proxy bridge helpers (#20001)
    ## Why
    The Linux managed-proxy bridge helpers are long-lived child processes in
    the sandbox networking path. Before this change they stayed dumpable and
    the network seccomp profile did not block cross-process memory syscalls,
    so another same-user process could potentially inspect or modify bridge
    memory instead of interacting only through the intended proxy interface.
    
    ## What changed
    - reuse the shared `codex-process-hardening` helper to mark bridge
    helper children non-dumpable before they begin serving
    - deny `process_vm_readv` and `process_vm_writev` in the existing
    network seccomp filter
    
    ## Security impact
    Bridge helpers are less exposed to same-user cross-process inspection or
    memory writes, which reduces the chance that sandboxed code can
    interfere with proxy support processes outside the intended IPC path.
    
    ## Verification
    - `cargo test -p codex-process-hardening`
    - `cargo test -p codex-linux-sandbox`
    - attempted `cargo check -p codex-linux-sandbox --target
    x86_64-unknown-linux-gnu`; blocked on missing `x86_64-linux-gnu-gcc` on
    this macOS host
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • [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.
  • Fix plan mode nudge test after task completion signature change (#20045)
    Updates the plan mode nudge test to pass the new `duration_ms` argument
    to task completion.
    
    Co-authored-by: Codex <noreply@openai.com>
  • permissions: add built-in default profiles (#19900)
    ## Why
    
    The migration away from `SandboxPolicy` needs new configs to start from
    permissions profiles instead of deriving profiles from legacy sandbox
    modes. Existing users can have empty `config.toml` files, and we should
    not rewrite user-owned config files that may live in shared
    repositories.
    
    This PR introduces built-in profile names so an empty config can resolve
    to a canonical `PermissionProfile`, while explicit named `[permissions]`
    profiles still behave predictably.
    
    ## What changed
    
    - Adds built-in `default_permissions` profile names:
      - `:read-only` maps to `PermissionProfile::read_only()`.
    - `:workspace` maps to the workspace-write profile, including
    project-root metadata carveouts.
    - `:danger-no-sandbox` maps to `PermissionProfile::Disabled`, preserving
    the distinction between no sandbox and a broad managed sandbox.
    - Reserves the `:` prefix for built-in profiles so user-defined
    `[permissions]` profiles cannot collide with future built-ins.
    - Allows `default_permissions` to reference a built-in profile without
    requiring a `[permissions]` table.
    - Makes an otherwise empty config choose a built-in profile by
    trust/platform context: trusted or untrusted project roots use
    `:workspace` when the platform supports that sandbox, while roots
    without a trust decision use `:read-only`.
    - Keeps legacy `sandbox_mode` configs on the legacy path, and still
    rejects user-defined `[permissions]` profiles that omit
    `default_permissions` so we do not silently guess among custom profiles.
    - Preserves compatibility behavior for implicit defaults: bare
    `network.enabled = true` allows runtime network without starting the
    managed proxy, explicit profile proxy policy still starts the proxy, and
    implicit workspace/add-dir roots keep legacy metadata carveouts.
    
    ## Verification
    
    - `cargo test -p codex-core builtin --lib`
    - `cargo test -p codex-core profile_network_proxy_config`
    - `cargo test -p codex-core
    implicit_builtin_workspace_profile_preserves_add_dir_metadata_carveouts`
    - `cargo test -p codex-core
    permissions_profiles_network_enabled_allows_runtime_network_without_proxy`
    - `cargo test -p codex-core
    permissions_profiles_proxy_policy_starts_managed_network_proxy`
    
    ## Documentation
    
    Public Codex config docs should mention these built-in names when the
    `[permissions]` config format is ready to document as stable.
    
    
    
    
    
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19900).
    * #20041
    * #20040
    * #20037
    * #20035
    * #20034
    * #20033
    * #20032
    * #20030
    * #20028
    * #20027
    * #20026
    * #20024
    * #20021
    * #20018
    * #20016
    * #20015
    * #20013
    * #20011
    * #20010
    * #20008
    * __->__ #19900
  • fix(network-proxy): tighten network proxy bypass defaults (#20002)
    ## Why
    Managed sessions use `NO_PROXY` to keep a small set of destinations on
    the direct path by default. The old default also bypassed all IPv4
    link-local addresses in `169.254.0.0/16`, which includes metadata
    endpoints such as `169.254.169.254`. Because `NO_PROXY` is evaluated by
    the client before the request reaches the managed proxy, requests to
    that range could skip proxy-side allowlist and local-binding checks
    entirely. On hosts where a link-local metadata service is reachable,
    that creates a path to sensitive environment metadata or credentials
    outside the intended enforcement point.
    
    ## What changed
    - remove the default IPv4 link-local `169.254.0.0/16` bypass from the
    managed proxy environment
    - keep the existing loopback and private-network defaults unchanged
    - update the regression assertion to lock in the narrower default
    
    ## Security impact
    Link-local requests now stay on the managed-proxy path by default, so
    the proxy can apply configured policy before they reach metadata-style
    endpoints or other link-local services.
    
    ## Verification
    - `cargo test -p codex-network-proxy`
    
    Co-authored-by: Codex <noreply@openai.com>
  • External agent session support (#19895)
    ## Summary
    
    This extends external agent detection/import beyond config artifacts so
    Codex can detect recent sessions files from the external agent home and
    import them into Codex rollout history.
    
    ## What changed
    
    - Added a focused `external_agent_sessions` module for:
      - session discovery
      - source-record parsing
      - rollout construction
      - import ledger tracking
    - Wired session detection/import into the app-server external agent
    config API.
    - Added compaction handling so large imported sessions can be resumed
    safely before the first follow-up turn.
    
    ## Testing
    
    Added coverage for:
    - recent-session detection
    - custom-title handling
    - recency filtering
    - dedupe and re-detect-after-source-change behavior
    - visible imported turn construction
    - backward-compatible import payload deserialization
    - end-to-end RPC import flow
    - rejection of undetected session paths
    - repeat-import behavior
    - large-session compaction before first follow-up
    
    Ran:
    - `cargo test -p codex-app-server external_agent_config_import_ --test
    all`
  • fix(tui): let esc exit empty shell mode (#19986)
    ## Summary
    
    - exit shell mode when `Esc` is pressed while the absorbed `!` is the
    only input
    - add direct regression coverage plus a composer snapshot for the
    restored normal prompt state
    
    ## Root cause
    
    Shell mode stores the leading `!` outside the editable textarea. After
    typing only `!`, the textarea is empty but the composer is still in bash
    mode, so the existing empty-composer `Esc` handling never runs.
    
    ## Validation
    
    - `just fmt`
    - `cargo test -p codex-tui
    bottom_pane::chat_composer::tests::esc_exits_empty_shell_mode`
    - `cargo test -p codex-tui
    bottom_pane::chat_composer::tests::footer_mode_snapshots`
    - `cargo insta pending-snapshots`
    
    `cargo test -p codex-tui` still reports unrelated existing `/status`
    snapshot drift in this local environment because the rendered
    permissions text is `workspace-write with network access` instead of the
    older `read-only` fixture text.
  • Move local /resume cwd filtering into thread/list (#19931)
    Move local resume and fork cwd filtering to `thread/list` instead of
    filtering in the TUI. This makes the `/resume` menu feel slightly faster
    to load when working in repos with many historical threads, and
    centralizes the cwd filtering in app-server.
    
    **Affected:**
    - /resume from inside the TUI.
    - codex resume with no session ID and without --last
    - codex resume --all
    - codex fork with no session ID and without --last
    - codex fork --all
    
    **Not affected:**
    - codex resume <id>
    - codex fork <id>
    - codex resume --last
    - codex fork --last
    
    Steps to test performance improvement in a real Codex environment:
    - Launch `codex resume` using compiled binary in a directory that has
    seen many threads.
    - Launch `codex resume` using release binary in same directory.
    - Observe difference in time-to-full-page as threads load.
  • feat(tui): suggest plan mode from composer drafts (#19901)
    ## Summary
    
    - suggest Plan mode when the current composer draft contains the
    standalone word `plan`
    - shares the Codex App heuristics for detection
    - excludes things line `/plan` and the word plan in shell mode
    - reuse the existing `Shift+Tab` mode cycle and add thread-scoped
    dismissal with `Esc`
    - replace the normal footer hint while the reminder is visible so the
    statusline stays anchored
    
    
    https://github.com/user-attachments/assets/01123ae8-cee6-4e95-b563-44655c071cde
    
    ## Why
    
    The desktop app already nudges users toward Plan mode when their draft
    clearly signals planning intent. The TUI had the underlying `/plan` and
    `Shift+Tab` flows, but no equivalent reminder at the moment the user was
    most likely to benefit from them.
    
    ## Details
    
    The reminder is shown only when Plan mode is available, the draft
    contains standalone `plan`, the user is not already in Plan mode, the
    composer is actionable, and the current thread has not dismissed the
    reminder. Slash-command and shell-command drafts are excluded.
    
    The first implementation used an extra composer row, but that moved the
    statusline whenever the heuristic fired. This version keeps the layout
    stable by rendering the reminder in the existing footer row instead.
    
    ## Validation
    
    - `INSTA_UPDATE=always cargo test -p codex-tui
    chatwidget::tests::plan_mode::plan_mode_nudge -- --nocapture`
    - `just fmt`
    - `just fix -p codex-tui`
    - `./tools/argument-comment-lint/run.py -p codex-tui`
    - `cargo insta pending-snapshots`
    - `git diff --check`
  • Clarify network approval auto-review prompts (#19907)
    ## Why
    
    Network access approval prompts were showing the generic retry reason,
    which made auto-review focus on the blocked connection instead of the
    command that caused it. This makes network approvals easier to assess by
    telling the reviewer to evaluate whether the triggering command was
    authorised by the user and within policy, and to treat the network call
    as acceptable when it is a reasonable consequence of that command.
    
    ## What changed
    
    - Split guardian approval request prompt rendering so `NetworkAccess`
    has a dedicated branch.
    - For network requests, show `Network approval context` and `Network
    access JSON` instead of `Retry reason` / `Planned action JSON`.
    - Added regression coverage for the network approval prompt wording and
    for omitting retry reason in this case.
    
    ## Verification
    
    - `cargo test -p codex-core
    guardian::tests::build_guardian_prompt_items_explains_network_access_review_scope`
  • Record MCP result telemetry on mcp.tools.call spans (#19509)
    ## Why
    - Without change: MCP tool call spans include request-side details such
    as server, tool, call ID, connector, session, and turn.
    - Issue: Some useful telemetry is only known by the MCP server after it
    handles the tool call, such as target identity or whether the call
    triggered a user-facing flow.
    
    ## What Changed
    - With change: Codex reads allowlisted telemetry from
    `_meta["codex/telemetry"]["span"]` and records it on the
    `mcp.tools.call` span.
    - Adds span fields for `codex.mcp.target.id` and
    `codex.mcp.user_flow.triggered`, with strict type checks and bounded
    target ID length.
    
    
    ## Verification
    `codex-rs/core/src/mcp_tool_call_tests.rs`
  • Enforce workspace metadata protections in Seatbelt (#19847)
    ## Summary
    
    Translate FileSystemSandboxPolicy project root metadata carveouts into
    macOS Seatbelt rules.
    
    ## Scope
    
    1. Thread protected metadata names into Seatbelt access roots.
    2. Ask FileSystemSandboxPolicy whether each metadata carveout is
    writable.
    3. Emit Seatbelt deny rules that block creating or replacing protected
    metadata names under writable roots.
    4. Add coverage for first time metadata creation and read only
    carveouts.
    
    ## Reviewer Focus
    
    1. This PR only covers the macOS sandbox adapter.
    2. The policy decision comes from FileSystemSandboxPolicy.
    3. Read only subpath carveouts and metadata protection checks should
    compose cleanly.
    
    ## Stack
    
    1. Policy primitive: #19846
    2. macOS Seatbelt adapter: this PR
    3. Shell preflight UX: #19848
    4. Runtime profile propagation: #19849
    5. Linux bubblewrap adapter: #19852
    
    ## Validation
    
    1. formatting for codex sandboxing
    2. codex sandboxing package tests
  • Strip connector provenance metadata from custom MCP tools (#19875)
    # Summary 
    This prevents non-codex_apps MCP servers from spoofing connector
    provenance metadata.
  • 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`
  • Terminate stdio MCP servers on shutdown to avoid process leaks (#19753)
    ## Why
    
    Several bug reports describe thread shutdown (including subagent
    threads) leaving stdio MCP server processes behind. These reports all
    point at the same lifecycle gap: Codex launches stdio MCP servers, but
    the session-level shutdown path does not explicitly close MCP clients or
    terminate the server process tree.
    
    Fixes #12491
    Fixes #12976
    Fixes #18881
    Fixes #19469
    
    ## History
    
    This is best understood as a regression/coverage gap in MCP session
    lifecycle management, not as stdio MCP cleanup being absent all along.
    #10710 added process-group cleanup for stdio MCP servers, but that
    cleanup only runs when the `RmcpClient`/transport is dropped. The older
    reports (#12491 and #12976) came after that cleanup existed, which
    suggests the remaining problem was that some higher-level shutdown paths
    kept the MCP manager alive or replaced it without explicitly draining
    clients. The newer reports (#18881 and #19469) exposed the same family
    around manager replacement and shutdown.
    
    ## What changed
    
    - Added an explicit stdio MCP process handle in `codex-rmcp-client` so
    local MCP servers terminate their process group and executor-backed MCP
    servers call the executor process terminator.
    - Added `RmcpClient::shutdown()` and manager-level MCP shutdown draining
    so session shutdown, channel-close fallback, MCP refresh, and connector
    probing stop owned MCP clients.
    - Added regression coverage that starts a stdio MCP server, begins an
    in-flight blocking tool call, shuts down the client, and asserts the
    server process exits.
    
    ## Verification
    
    - `cargo test -p codex-rmcp-client`
    - `cargo test -p codex-mcp`
    - `just fix -p codex-rmcp-client`
    - `just fix -p codex-mcp`
    - `just fix -p codex-core`
    
    - Manual before/after validation with a temporary repro script:
    - Pre-fix binary from `HEAD^` (`fed0a8f4fa`): reproduced the leak with
    surviving MCP server and child PIDs, `survivors=[77583, 77592]`,
    `leaked=true`.
    - Post-fix binary from this branch (`67e318148b`): verified both MCP
    processes were gone after interrupting `codex exec`, `survivors=[]`,
    `leaked=false`.
  • TUI: use cumulative turn duration for worked-for separator (#19929)
    ## Why
    
    Fixes #19814.
    
    The TUI's current `Worked for ...` timing behavior is a leftover from
    #9599. At that point, models could emit multiple assistant messages in
    one turn for preambles/commentary, but the TUI did not yet have a
    reliable signal that an assistant message was the final answer when it
    started streaming. To avoid showing an ever-growing elapsed time on each
    preamble separator, #9599 made the separator timer incremental by
    tracking elapsed time since the previous separator.
    
    That workaround is no longer the right model for the final
    completed-turn display. Since then, #16638 added protocol-native turn
    timing, including `duration_ms` on turn completion. With that cumulative
    duration available at the point where the TUI renders the completed-turn
    separator, the UI can show the actual turn duration directly instead of
    carrying per-separator timing state.
    
    ## What Changed
    
    - Thread `duration_ms` into `ChatWidget::on_task_complete` from both
    legacy `TurnCompleteEvent` handling and app-server `TurnCompleted`
    notifications.
    - Use `duration_ms` for the final `Worked for ...` separator, falling
    back to the status indicator timer only when the protocol duration is
    unavailable.
    - Keep mid-turn separators before later assistant text as plain visual
    dividers instead of clocked `Worked for ...` separators.
    - Remove the old incremental separator timer state and helper
    (`last_separator_elapsed_secs` / `worked_elapsed_from`).
    - Add a snapshot regression test for a turn that runs a command and then
    completes with a final answer, verifying the final separator uses the
    cumulative turn duration.
    
    ## Verification
    
    - `cargo test -p codex-tui
    final_worked_for_uses_cumulative_turn_duration_snapshot`
    - `just fix -p codex-tui`
    
    Manual repro prompt:
    
    ```text
    Manual timing repro. First send a short preamble/commentary sentence before using tools. Then run exactly this shell command: sleep 75; echo MANUAL_TIMING_DONE. After the command finishes, give a final answer that says "done". Do not skip the preamble.
    ```
    
    After this change, the mid-turn break before the final answer should be
    a plain divider, and the final completed-turn separator should show
    `Worked for ...` using the cumulative turn duration.
    
    Before:
    <img width="414" height="102" alt="Screenshot 2026-04-27 at 10 09 01 PM"
    src="https://github.com/user-attachments/assets/b9e2ce01-2460-40e4-a5c4-c9ba8add2557"
    />
    
    
    After:
    <img width="485" height="149" alt="Screenshot 2026-04-27 at 10 09 07 PM"
    src="https://github.com/user-attachments/assets/d24089ae-d4e2-41b6-b966-07c98706ead4"
    />
  • feat: house-keeping memories 3 (#20005)
    Move stuff in memories, no behavioural change expected
  • [sandbox] Enforce protected workspace metadata paths (#19846)
    ## Summary
    
    Make FileSystemSandboxPolicy the semantic source of truth for project
    root metadata protection. Under writable roots, `.git`, `.codex`, and
    `.agents` stay protected unless user policy grants an explicit write
    rule for that metadata path.
    
    ## Scope
    
    1. Add `protected_metadata_names` to `WritableRoot`.
    2. Teach `FileSystemSandboxPolicy::can_write_path_with_cwd` to reject
    protected metadata writes under writable roots unless explicitly
    allowed.
    3. Default workspace write profiles to protect `.git`, `.codex`, and
    `.agents`.
    4. Add the Linux fallback setup needed before Linux enforcement lands
    later in the stack.
    
    ## Reviewer Focus
    
    1. The policy decision belongs in FileSystemSandboxPolicy, not shell
    command parsing.
    2. Legacy SandboxPolicy remains a compatibility projection, not the
    source of the new rule.
    3. Explicit user write rules can still opt into these metadata paths.
    
    ## Stack
    
    1. Policy primitive: this PR
    2. macOS Seatbelt adapter: #19847
    3. Shell preflight UX: #19848
    4. Runtime profile propagation: #19849
    5. Linux bubblewrap adapter: #19852
    
    ## Validation
    
    1. codex protocol permissions tests
    2. formatting for codex protocol and codex linux sandbox
    3. diff whitespace check
  • feat(tui): add configurable keymap support (#18593)
    ## Why
    
    The TUI currently handles keyboard shortcuts as hard-coded event matches
    spread across app, composer, pager, list, approval, and navigation code.
    That makes shortcuts hard to customize, makes displayed hints easy to
    drift from actual behavior, and makes future keymap work riskier because
    there is no central action inventory.
    
    This PR adds the foundation for configurable, action-based keymaps
    without adding the interactive remapping UI yet. Onboarding
    intentionally stays on fixed startup shortcuts because users cannot
    reasonably configure keymaps before completing onboarding.
    
    This is PR1 in the keymap stack:
    
    - PR1: #18593: configurable keymap foundation
    - PR2: #18594: `/keymap` picker and guided remapping UI
    - PR3: #18595: Vim composer mode and the remap option
    
    ## Design Notes
    
    The new model resolves named actions into concrete runtime bindings once
    from config, then passes those bindings to the UI surfaces that handle
    input or render shortcut hints.
    
    The main concepts are:
    
    - **Context**: a scope where an action is active, such as `global`,
    `chat`, `composer`, `editor`, `pager`, `list`, or `approval`.
    - **Action**: a named operation inside a context, such as
    `global.open_transcript`, `composer.submit`, or `pager.close`.
    - **Binding**: one or more single-key shortcuts assigned to an action,
    written as config strings such as `ctrl-t`, `alt-backspace`, or
    `page-down`. Multi-step sequences such as `ctrl-x ctrl-s`, `g g`, or
    leader-key flows are not part of this PR.
    - **Resolution order**: context-specific config wins first, supported
    global fallbacks come next, and built-in defaults fill in anything
    unset.
    - **Explicit unbinding**: an empty array removes an action binding in
    that scope and does not fall through to a fallback binding.
    - **Conflict validation**: a resolved keymap rejects duplicate active
    bindings inside the same scope so one keypress cannot dispatch two
    actions.
    
    ## What Changed
    
    - Added `TuiKeymap` config support under `[tui.keymap]`, including typed
    contexts/actions, key alias normalization, generated schema coverage,
    and user-facing config errors.
    - Added `RuntimeKeymap` resolution in `codex-rs/tui/src/keymap.rs`,
    including fallback precedence, built-in defaults, explicit unbinding,
    and per-context conflict validation.
    - Rewired existing TUI handlers to consume resolved keymap actions
    instead of directly matching hard-coded keys in each component.
    - Updated key hint rendering and footer/pager/list surfaces so displayed
    shortcuts follow the resolved keymap.
    - Kept onboarding shortcuts fixed in
    `codex-rs/tui/src/onboarding/keys.rs` instead of exposing them through
    `[tui.keymap]`.
    
    ## Validation
    
    The branch includes focused coverage for config parsing, key
    normalization, runtime fallback resolution, explicit unbinding,
    duplicate-key conflict validation, default keymap consistency,
    onboarding startup key behavior, and UI hint snapshots affected by
    resolved key bindings.
  • Reset TUI keyboard reporting on exit (#19625)
    ## Why
    
    Codex enables enhanced keyboard reporting while the TUI owns the
    terminal. In iTerm2, exiting the TUI with Ctrl+C can intermittently
    leave the parent shell receiving raw CSI-u / `modifyOtherKeys` fragments
    instead of normal key input.
    
    Final terminal cleanup should put the parent shell back into normal
    keyboard reporting even if the terminal misses the usual stack pop.
    
    Fixes #19553.
    
    ## What Changed
    
    - Move TUI keyboard enhancement setup and detection into
    `tui/src/tui/keyboard_modes.rs`.
    - Add an exit-only `restore_after_exit()` path that performs the normal
    keyboard enhancement pop plus unconditional keyboard enhancement and
    `modifyOtherKeys` resets.
    - Keep temporary restore paths, such as external-editor handoff, using
    the balanced stack pop behavior.
    
    ## Confidence
    
    Medium. This is a speculative fix: I was not able to reproduce the
    reported iTerm2 behavior manually, but the symptoms line up with
    terminal keyboard reporting state surviving Codex exit. The added reset
    sequences are scoped to final TUI shutdown and should be harmless when
    the terminal is already clean.
  • feat: house-keeping memories 2 (#20000)
    Just move metrics in a dedicated file
  • feat: house-keeping memories 1 (#19998)
    Just move metrics in a dedicated file
  • feat: skip memory startup when Codex rate limits are low (#19990)
    ## Why
    
    Memory startup runs in the background after an eligible turn, but it can
    consume Codex backend quota at exactly the wrong time: when the user is
    already near a rate-limit boundary. This PR adds a guard so the memory
    pipeline backs off when the Codex rate-limit snapshot says the remaining
    budget is too low.
    
    ## What Changed
    
    - Added `memories.min_rate_limit_remaining_percent` with a default of
    `25`, clamped to `0..=100`, and regenerated `core/config.schema.json`.
    - Added `codex-rs/memories/write/src/guard.rs`, which fetches Codex
    backend rate limits before memory startup and skips phase 1 / phase 2
    when the Codex limit is reached or either tracked window is above the
    configured usage ceiling.
    - Keeps startup best-effort: non-Codex auth or rate-limit fetch/client
    failures preserve the existing memory startup behavior.
    - Records a `codex.memory.startup` counter with
    `status=skipped_rate_limit` when startup is skipped.
    - Added config parsing/clamping coverage and guard unit tests.
    
    ## Verification
    
    - Added `codex-rs/memories/write/src/guard_tests.rs` for threshold,
    primary/secondary window, and reached-limit behavior.
    - Added config tests for TOML parsing and clamping.
  • fix: configure AgentIdentity AuthAPI base URL (#19904)
    ## Summary
    
    AgentIdentity runtime loading currently registers tasks against a single
    hardcoded AuthAPI base URL. That works for production, but local and
    staging validation may need registration to target a different
    authapi-login-provider without baking internal staging service URLs into
    the OSS binary.
    
    This PR adds a small config surface for
    `agent_identity_authapi_base_url` and threads it through the existing
    auth-loading path as a direct argument. Explicit config wins. Without
    config, task registration keeps using the production AuthAPI URL,
    matching the current default behavior.
    
    ## Stack
    
    1. openai/codex#19762 - `refactor: make auth loading async` (merged)
    2. openai/codex#19763 - `refactor: load agent identity runtime eagerly`
    3. This PR - `fix: configure AgentIdentity AuthAPI base URL`
    4. openai/codex#19764 - `feat: verify agent identity JWTs with JWKS`
    
    ## Design decisions
    
    - Keep the existing auth-loading shape and pass the new value as an
    argument. This avoids another wrapper loader and keeps the call path
    readable.
    - Add config instead of embedding internal staging URLs. Environments
    that need a non-production AuthAPI can configure it explicitly.
    - Keep the default AuthAPI registration URL as production.
    `chatgpt_base_url` remains separate and is used by the follow-up JWKS
    verification PR for fetching public keys from the ChatGPT backend route.
    - Resolve the AuthAPI base URL inside AgentIdentity loading, because
    task registration is the only consumer of this value.
    
    ## Testing
    
    Tests: targeted Rust checks, AgentIdentity auth tests, config schema
    regeneration, formatter/fix pass, and whitespace diff check.
  • feat: trigger memories from user turns with cooldown (#19970)
    ## Why
    
    Memory startup was tied to thread lifecycle events such as create, load,
    and fork. That can run memory work before a thread receives real user
    input, and it makes startup cost scale with thread management instead of
    actual turns. Moving the trigger to `thread/sendInput` keeps memory
    startup aligned with the first real user turn and lets it use the
    current thread config at turn time.
    
    The idea is to prevent ghost cost due to pre-warm triggered by the app
    
    Turn-based startup can also make global phase-2 consolidation easier to
    request repeatedly, so this adds a success cooldown and tightens the
    default startup scan window.
    
    ## What Changed
    
    - Start `codex_memories_write::start_memories_startup_task` after a
    non-empty `thread/sendInput` turn is submitted, instead of from thread
    create/load/fork paths:
    https://github.com/openai/codex/blob/d4a6885b7829e2fd2ec7a09355e4f75ebe1d1fe3/codex-rs/app-server/src/codex_message_processor.rs#L6477-L6487
    - Expose `CodexThread::config()` so app-server can pass the live config
    into memory startup at turn time.
    - Add a six-hour successful-run cooldown for global phase-2
    consolidation via `SkippedCooldown`:
    https://github.com/openai/codex/blob/d4a6885b7829e2fd2ec7a09355e4f75ebe1d1fe3/codex-rs/state/src/runtime/memories.rs#L963-L966
    - Reduce memory startup defaults to at most 2 rollouts over 10 days:
    https://github.com/openai/codex/blob/d4a6885b7829e2fd2ec7a09355e4f75ebe1d1fe3/codex-rs/config/src/types.rs#L31-L34
    
    ## Verification
    
    Updated the memory runtime coverage around phase-2 reclaim behavior,
    including `phase2_global_lock_respects_success_cooldown`.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Stabilize memory Phase 2 input ordering (#19967)
    ## Why
    
    Phase 2 still needs to choose the most relevant stage-1 memory outputs
    by usage and recency, but exposing that ranking as the rendered
    `raw_memories.md` order creates unnecessary large diff. Usage-count or
    timestamp changes can reshuffle otherwise unchanged memories, making the
    workspace diff noisy and giving the consolidation prompt a misleading
    recency signal from file position.
    This fix will reduce token consumption
    
    ## What Changed
    
    - Keep the existing top-N Phase 2 selection ranking by `usage_count`,
    `last_usage`, `source_updated_at`, and `thread_id`.
    - Return the selected rows in stable ascending `thread_id` order before
    syncing Phase 2 filesystem inputs.
    - Update the memory README, raw memories header, and consolidation
    prompt so they describe the stable order and tell the prompt to use
    metadata and workspace diffs instead of file order as the recency
    signal.
    - Adjust the memory runtime tests to use deterministic thread IDs and
    assert the stable return order separately from the ranked selection
    semantics.
    
    ## Test Coverage
    
    - Existing memory runtime tests in
    `codex-rs/state/src/runtime/memories.rs` now cover the stable returned
    ordering for Phase 2 inputs.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>