Commit Graph

231 Commits

  • Introduce rollout items (#3380)
    This PR introduces Rollout items. This enable us to rollout eventmsgs
    and session meta.
    
    This is mostly #3214 with rebase on main
  • Replace config.responses_originator_header_internal_override with CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR (#3388)
    The previous config approach had a few issues:
    1. It is part of the config but not designed to be used externally
    2. It had to be wired through many places (look at the +/- on this PR
    3. It wasn't guaranteed to be set consistently everywhere because we
    don't have a super well defined way that configs stack. For example, the
    extension would configure during newConversation but anything that
    happened outside of that (like login) wouldn't get it.
    
    This env var approach is cleaner and also creates one less thing we have
    to deal with when coming up with a better holistic story around configs.
    
    One downside is that I removed the unit test testing for the override
    because I don't want to deal with setting the global env or spawning
    child processes and figuring out how to introspect their originator
    header. The new code is sufficiently simple and I tested it e2e that I
    feel as if this is still worth it.
  • feat: add ArchiveConversation to ClientRequest (#3353)
    Adds support for `ArchiveConversation` in the JSON-RPC server that takes
    a `(ConversationId, PathBuf)` pair and:
    
    - verifies the `ConversationId` corresponds to the rollout id at the
    `PathBuf`
    - if so, invokes
    `ConversationManager.remove_conversation(ConversationId)`
    - if the `CodexConversation` was in memory, send `Shutdown` and wait for
    `ShutdownComplete` with a timeout
    - moves the `.jsonl` file to `$CODEX_HOME/archived_sessions`
    
    ---------
    
    Co-authored-by: Gabriel Peal <gabriel@openai.com>
  • fix: include rollout_path in NewConversationResponse (#3352)
    Adding the `rollout_path` to the `NewConversationResponse` makes it so a
    client can perform subsequent operations on a `(ConversationId,
    PathBuf)` pair. #3353 will introduce support for `ArchiveConversation`.
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/3352).
    * #3353
    * __->__ #3352
  • feat: Run cargo shear during CI (#3338)
    Run cargo shear as part of the CI to ensure no unused dependencies
  • Generate more typescript types and return conversation id with ConversationSummary (#3219)
    This PR does multiple things that are necessary for conversation resume
    to work from the extension. I wanted to make sure everything worked so
    these changes wound up in one PR:
    1. Generate more ts types
    2. Resume rollout history files rather than create a new one every time
    it is resumed so you don't see a duplicate conversation in history for
    every resume. Chatted with @aibrahim-oai to verify this
    3. Return conversation_id in conversation summaries
    4. [Cleanup] Use serde and strong types for a lot of the rollout file
    parsing
  • Add a getUserAgent MCP method (#3320)
    This will allow the extension to pass this user agent + a suffix for its
    requests
  • Use ConversationId instead of raw Uuids (#3282)
    We're trying to migrate from `session_id: Uuid` to `conversation_id:
    ConversationId`. Not only does this give us more type safety but it
    unifies our terminology across Codex and with the implementation of
    session resuming, a conversation (which can span multiple sessions) is
    more appropriate.
    
    I started this impl on https://github.com/openai/codex/pull/3219 as part
    of getting resume working in the extension but it's big enough that it
    should be broken out.
  • Never store requests (#3212)
    When item ids are sent to Responses API it will load them from the
    database ignoring the provided values. This adds extra latency.
    
    Not having the mode to store requests also allows us to simplify the
    code.
    
    ## Breaking change
    
    The `disable_response_storage` configuration option is removed.
  • chore: improve serialization of ServerNotification (#3193)
    This PR introduces introduces a new
    `OutgoingMessage::AppServerNotification` variant that is designed to
    wrap a `ServerNotification`, which makes the serialization more
    straightforward compared to
    `OutgoingMessage::Notification(OutgoingNotification)`. We still use the
    latter for serializing an `Event` as a `JSONRPCMessage::Notification`,
    but I will try to get away from that in the near future.
    
    With this change, now the generated TypeScript type for
    `ServerNotification` is:
    
    ```typescript
    export type ServerNotification =
      | { "method": "authStatusChange", "params": AuthStatusChangeNotification }
      | { "method": "loginChatGptComplete", "params": LoginChatGptCompleteNotification };
    ```
    
    whereas before it was:
    
    ```typescript
    export type ServerNotification =
      | { type: "auth_status_change"; data: AuthStatusChangeNotification }
      | { type: "login_chat_gpt_complete"; data: LoginChatGptCompleteNotification };
    ```
    
    Once the `Event`s are migrated to the `ServerNotification` enum in Rust,
    it should be considerably easier to work with notifications on the
    TypeScript side, as it will be possible to `switch (message.method)` and
    check for exhaustiveness.
    
    Though we will probably need to introduce:
    
    ```typescript
    export type ServerMessage = ServerRequest | ServerNotification;
    ```
    
    and then we still need to group all of the `ServerResponse` types
    together, as well.
  • MCP: add session resume + history listing; (#3185)
    # External (non-OpenAI) Pull Request Requirements
    
    Before opening this Pull Request, please read the dedicated
    "Contributing" markdown file or your PR may be closed:
    https://github.com/openai/codex/blob/main/docs/contributing.md
    
    If your PR conforms to our contribution guidelines, replace this text
    with a detailed and high quality description of your changes.
  • [mcp-server] Update read config interface (#3093)
    ## Summary
    Follow-up to #3056
    
    This PR updates the mcp-server interface for reading the config settings
    saved by the user. At risk of introducing _another_ Config struct, I
    think it makes sense to avoid tying our protocol to ConfigToml, as its
    become a bit unwieldy. GetConfigTomlResponse was a de-facto struct for
    this already - better to make it explicit, in my opinion.
    
    This is technically a breaking change of the mcp-server protocol, but
    given the previous interface was introduced so recently in #2725, and we
    have not yet even started to call it, I propose proceeding with the
    breaking change - but am open to preserving the old endpoint.
    
    ## Testing
    - [x] Added additional integration test coverage
  • Dividing UserMsgs into categories to send it back to the tui (#3127)
    This PR does the following:
    
    - divides user msgs into 3 categories: plain, user instructions, and
    environment context
    - Centralizes adding user instructions and environment context to a
    degree
    - Improve the integration testing
    
    Building on top of #3123
    
    Specifically this
    [comment](https://github.com/openai/codex/pull/3123#discussion_r2319885089).
    We need to send the user message while ignoring the User Instructions
    and Environment Context we attach.
  • Replay EventMsgs from Response Items when resuming a session with history. (#3123)
    ### Overview
    
    This PR introduces the following changes:
    	1.	Adds a unified mechanism to convert ResponseItem into EventMsg.
    2. Ensures that when a session is initialized with initial history, a
    vector of EventMsg is sent along with the session configuration. This
    allows clients to re-render the UI accordingly.
    	3. 	Added integration testing
    
    ### Caveats
    
    This implementation does not send every EventMsg that was previously
    dispatched to clients. The excluded events fall into two categories:
    	•	“Arguably” rolled-out events
    Examples include tool calls and apply-patch calls. While these events
    are conceptually rolled out, we currently only roll out ResponseItems.
    These events are already being handled elsewhere and transformed into
    EventMsg before being sent.
    	•	Non-rolled-out events
    Certain events such as TurnDiff, Error, and TokenCount are not rolled
    out at all.
    
    ### Future Directions
    
    At present, resuming a session involves maintaining two states:
    	•	UI State
    Clients can replay most of the important UI from the provided EventMsg
    history.
    	•	Model State
    The model receives the complete session history to reconstruct its
    internal state.
    
    This design provides a solid foundation. If, in the future, more precise
    UI reconstruction is needed, we have two potential paths:
    1. Introduce a third data structure that allows us to derive both
    ResponseItems and EventMsgs.
    2. Clearly divide responsibilities: the core system ensures the
    integrity of the model state, while clients are responsible for
    reconstructing the UI.
  • MCP sandbox call (#3128)
    I have read the CLA Document and I hereby sign the CLA
  • Include originator in authentication URL parameters (#3117)
    Associates the client with an authentication session.
  • Add a common way to create HTTP client (#3110)
    Ensure User-Agent and originator are always sent.
  • Move CodexAuth and AuthManager to the core crate (#3074)
    Fix a long standing layering issue.
  • fix: remove unnecessary flush() calls (#2873)
    Because we are writing to a pipe, these `flush()` calls are unnecessary,
    so removing these saves us one syscall per write in these two cases.
  • fix: switch to unbounded channel (#2874)
    #2747 encouraged me to audit our codebase for similar issues, as now I
    am particularly suspicious that our flaky tests are due to a racy
    deadlock.
    
    I asked Codex to audit our code, and one of its suggestions was this:
    
    > **High-Risk Patterns**
    >
    > All `send_*` methods await on a bounded
    `mpsc::Sender<OutgoingMessage>`. If the writer blocks, the channel fills
    and the processor task blocks on send, stops draining incoming requests,
    and stdin reader eventually blocks on its send. This creates a
    backpressure deadlock cycle across the three tasks.
    >
    > **Recommendations**
    > * Server outgoing path: break the backpressure cycle
    > * Option A (minimal risk): Change `OutgoingMessageSender` to use an
    unbounded channel to decouple producer from stdout. Add rate logging so
    floods are visible.
    > * Option B (bounded + drop policy): Change `send_*` to try_send and
    drop messages (or coalesce) when the queue is full, logging a warning.
    This prevents processor stalls at the cost of losing messages under
    extreme backpressure.
    > * Option C (two-stage buffer): Keep bounded channel, but have a
    dedicated “egress” task that drains an unbounded internal queue, writing
    to stdout with retries and a shutdown timeout. This centralizes
    backpressure policy.
    
    So this PR is Option A.
    
    Indeed, we previously used a bounded channel with a capacity of `128`,
    but as we discovered recently with #2776, there are certainly cases
    where we can get flooded with events.
    
    That said, `test_shell_command_approval_triggers_elicitation` just
    failed one one build when I put up this PR, so clearly we are not out of
    the woods yet...
    
    **Update:** I think I found the true source of the deadlock! See
    https://github.com/openai/codex/pull/2876
  • Bug fix: clone of incoming_tx can lead to deadlock (#2747)
    POC code
    
    ```rust
    use tokio::sync::mpsc;
    use std::time::Duration;
    
    #[tokio::main]
    async fn main() {
        println!("=== Test 1: Simulating original MCP server pattern ===");
        test_original_pattern().await;
    }
    
    async fn test_original_pattern() {
        println!("Testing the original pattern from MCP server...");
        
        // Create channel - this simulates the original incoming_tx/incoming_rx
        let (tx, mut rx) = mpsc::channel::<String>(10);
        
        // Task 1: Simulates stdin reader that will naturally terminate
        let stdin_task = tokio::spawn({
            let tx_clone = tx.clone();
            async move {
                println!("  stdin_task: Started, will send 3 messages then exit");
                for i in 0..3 {
                    let msg = format!("Message {}", i);
                    if tx_clone.send(msg.clone()).await.is_err() {
                        println!("  stdin_task: Receiver dropped, exiting");
                        break;
                    }
                    println!("  stdin_task: Sent {}", msg);
                    tokio::time::sleep(Duration::from_millis(300)).await;
                }
                println!("  stdin_task: Finished (simulating EOF)");
                // tx_clone is dropped here
            }
        });
        
        // Task 2: Simulates message processor
        let processor_task = tokio::spawn(async move {
            println!("  processor_task: Started, waiting for messages");
            while let Some(msg) = rx.recv().await {
                println!("  processor_task: Processing {}", msg);
                tokio::time::sleep(Duration::from_millis(100)).await;
            }
            println!("  processor_task: Finished (channel closed)");
        });
        
        // Task 3: Simulates stdout writer or other background task
        let background_task = tokio::spawn(async move {
            for i in 0..2 {
                tokio::time::sleep(Duration::from_millis(500)).await;
                println!("  background_task: Tick {}", i);
            }
            println!("  background_task: Finished");
        });
        
        println!("  main: Original tx is still alive here");
        println!("  main: About to call tokio::join! - will this deadlock?");
        
        // This is the pattern from the original code
        let _ = tokio::join!(stdin_task, processor_task, background_task);
    }
    
    ```
    
    ---------
    
    Co-authored-by: Michael Bolin <bolinfest@gmail.com>
  • Following up on #2371 post commit feedback (#2852)
    - Introduce websearch end to complement the begin 
    - Moves the logic of adding the sebsearch tool to
    create_tools_json_for_responses_api
    - Making it the client responsibility to toggle the tool on or off 
    - Other misc in #2371 post commit feedback
    - Show the query:
    
    <img width="1392" height="151" alt="image"
    src="https://github.com/user-attachments/assets/8457f1a6-f851-44cf-bcca-0d4fe460ce89"
    />
  • Custom /prompts (#2696)
    Adds custom `/prompts` to `~/.codex/prompts/<command>.md`.
    
    <img width="239" height="107" alt="Screenshot 2025-08-25 at 6 22 42 PM"
    src="https://github.com/user-attachments/assets/fe6ebbaa-1bf6-49d3-95f9-fdc53b752679"
    />
    
    ---
    
    Details:
    
    1. Adds `Op::ListCustomPrompts` to core.
    2. Returns `ListCustomPromptsResponse` with list of `CustomPrompt`
    (name, content).
    3. TUI calls the operation on load, and populates the custom prompts
    (excluding prompts that collide with builtins).
    4. Selecting the custom prompt automatically sends the prompt to the
    agent.
  • chore: print stderr from MCP server to test output using eprintln! (#2849)
    Related to https://github.com/openai/codex/pull/2848, I don't see the
    stderr from `codex mcp` colocated with the other stderr from
    `test_shell_command_approval_triggers_elicitation()` when it fails even
    though we have `RUST_LOG=debug` set when we spawn `codex mcp`:
    
    
    https://github.com/openai/codex/blob/1e9e703b969d3f0965b31d1cc3d70fed3ebdd6f6/codex-rs/mcp-server/tests/common/mcp_process.rs#L65
    
    Let's try this new logic which should be more explicit.
  • chore: try to make it easier to debug the flakiness of test_shell_command_approval_triggers_elicitation (#2848)
    `test_shell_command_approval_triggers_elicitation()` is one of a number
    of integration tests that we have observed to be flaky on GitHub CI, so
    this PR tries to reduce the flakiness _and_ to provide us with more
    information when it flakes. Specifically:
    
    - Changed the command that we use to trigger the elicitation from `git
    init` to `python3 -c 'import pathlib; pathlib.Path(r"{}").touch()'`
    because running `git` seems more likely to invite variance.
    - Increased the timeout to wait for the task response from 10s to 20s.
    - Added more logging.
  • chore: require uninlined_format_args from clippy (#2845)
    - added `uninlined_format_args` to `[workspace.lints.clippy]` in the
    `Cargo.toml` for the workspace
    - ran `cargo clippy --tests --fix`
    - ran `just fmt`
  • Add "View Image" tool (#2723)
    Adds a "View Image" tool so Codex can find and see images by itself:
    
    <img width="1772" height="420" alt="Screenshot 2025-08-26 at 10 40
    04 AM"
    src="https://github.com/user-attachments/assets/7a459c7b-0b86-4125-82d9-05fbb35ade03"
    />
  • [mcp-server] Add GetConfig endpoint (#2725)
    ## Summary
    Adds a GetConfig request to the MCP Protocol, so MCP clients can
    evaluate the resolved config.toml settings which the harness is using.
    
    ## Testing
    - [x] Added an end to end test of the endpoint
  • send context window with task started (#2752)
    - Send context window with task started
    - Accounting for changing the model per turn
  • test: faster test execution in codex-core (#2633)
    this dramatically improves time to run `cargo test -p codex-core` (~25x
    speedup).
    
    before:
    ```
    cargo test -p codex-core  35.96s user 68.63s system 19% cpu 8:49.80 total
    ```
    
    after:
    ```
    cargo test -p codex-core  5.51s user 8.16s system 63% cpu 21.407 total
    ```
    
    both tests measured "hot", i.e. on a 2nd run with no filesystem changes,
    to exclude compile times.
    
    approach inspired by [Delete Cargo Integration
    Tests](https://matklad.github.io/2021/02/27/delete-cargo-integration-tests.html),
    we move all test cases in tests/ into a single suite in order to have a
    single binary, as there is significant overhead for each test binary
    executed, and because test execution is only parallelized with a single
    binary.
  • Add web search tool (#2371)
    Adds web_search tool, enabling the model to use Responses API web_search
    tool.
    - Disabled by default, enabled by --search flag
    - When --search is passed, exposes web_search_request function tool to
    the model, which triggers user approval. When approved, the model can
    use the web_search tool for the remainder of the turn
    <img width="1033" height="294" alt="image"
    src="https://github.com/user-attachments/assets/62ac6563-b946-465c-ba5d-9325af28b28f"
    />
    
    ---------
    
    Co-authored-by: easong-openai <easong@openai.com>
  • fork conversation from a previous message (#2575)
    This can be the underlying logic in order to start a conversation from a
    previous message. will need some love in the UI.
    
    Base for building this: #2588
  • Fix flakiness in shell command approval test (#2547)
    ## Summary
    - read the shell exec approval request's actual id instead of assuming
    it is always 0
    - use that id when validating and responding in the test
    
    ## Testing
    - `cargo test -p codex-mcp-server
    test_shell_command_approval_triggers_elicitation`
    
    ------
    https://chatgpt.com/codex/tasks/task_i_68a6ab9c732c832c81522cbf11812be0
  • Add AuthManager and enhance GetAuthStatus command (#2577)
    This PR adds a central `AuthManager` struct that manages the auth
    information used across conversations and the MCP server. Prior to this,
    each conversation and the MCP server got their own private snapshots of
    the auth information, and changes to one (such as a logout or token
    refresh) were not seen by others.
    
    This is especially problematic when multiple instances of the CLI are
    run. For example, consider the case where you start CLI 1 and log in to
    ChatGPT account X and then start CLI 2 and log out and then log in to
    ChatGPT account Y. The conversation in CLI 1 is still using account X,
    but if you create a new conversation, it will suddenly (and
    unexpectedly) switch to account Y.
    
    With the `AuthManager`, auth information is read from disk at the time
    the `ConversationManager` is constructed, and it is cached in memory.
    All new conversations use this same auth information, as do any token
    refreshes.
    
    The `AuthManager` is also used by the MCP server's GetAuthStatus
    command, which now returns the auth method currently used by the MCP
    server.
    
    This PR also includes an enhancement to the GetAuthStatus command. It
    now accepts two new (optional) input parameters: `include_token` and
    `refresh_token`. Callers can use this to request the in-use auth token
    and can optionally request to refresh the token.
    
    The PR also adds tests for the login and auth APIs that I recently added
    to the MCP server.
  • Added new auth-related methods and events to mcp server (#2496)
    This PR adds the following:
    * A getAuthStatus method on the mcp server. This returns the auth method
    currently in use (chatgpt or apikey) or none if the user is not
    authenticated. It also returns the "preferred auth method" which
    reflects the `preferred_auth_method` value in the config.
    * A logout method on the mcp server. If called, it logs out the user and
    deletes the `auth.json` file — the same behavior in the cli's `/logout`
    command.
    * An `authStatusChange` event notification that is sent when the auth
    status changes due to successful login or logout operations.
    * Logic to pass command-line config overrides to the mcp server at
    startup time. This allows use cases like `codex mcp -c
    preferred_auth_method=apikey`.
  • [tui] Support /mcp command (#2430)
    ## Summary
    Adds a `/mcp` command to list active tools. We can extend this command
    to allow configuration of MCP tools, but for now a simple list command
    will help debug if your config.toml and your tools are working as
    expected.
  • fix: remove shutdown_flag param to run_login_server() (#2399)
    In practice, this was always passed in as `None`, so eliminated the
    param and updated all the call sites.
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/2399).
    * __->__ #2399
    * #2398
    * #2396
    * #2395
    * #2394
    * #2393
    * #2389
  • fix: make ShutdownHandle a private field of LoginServer (#2396)
    Folds the top-level `shutdown()` function into a method of
    `ShutdownHandle` and then simply stores `ShutdownHandle` on
    `LoginServer` since the two fields it contains were always being used
    together, anyway.
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/2396).
    * #2399
    * #2398
    * __->__ #2396
    * #2395
    * #2394
    * #2393
    * #2389
  • fix: eliminate ServerOptions.login_timeout and have caller use tokio::time::timeout() instead (#2395)
    https://github.com/openai/codex/pull/2373 introduced
    `ServerOptions.login_timeout` and `spawn_timeout_watcher()` to use an
    extra thread to manage the timeout for the login server. Now that we
    have asyncified the login stack, we can use `tokio::time::timeout()`
    from "outside" the login library to manage the timeout rather than
    having to a commit to a specific "timeout" concept from within.
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/2395).
    * #2399
    * #2398
    * #2396
    * __->__ #2395
    * #2394
    * #2393
    * #2389
  • fix: async-ify login flow (#2393)
    This replaces blocking I/O with async/non-blocking I/O in a number of
    cases. This facilitates the use of `tokio::sync::Notify` and
    `tokio::select!` in #2394.
    
    
    
    
    
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/2393).
    * #2399
    * #2398
    * #2396
    * #2395
    * #2394
    * __->__ #2393
    * #2389
  • chore: move mcp-server/src/wire_format.rs to protocol/src/mcp_protocol.rs (#2423)
    The existing `wire_format.rs` should share more types with the
    `codex-protocol` crate (like `AskForApproval` instead of maintaining a
    parallel `CodexToolCallApprovalPolicy` enum), so this PR moves
    `wire_format.rs` into `codex-protocol`, renaming it as
    `mcp-protocol.rs`. We also de-dupe types, where appropriate.
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/2423).
    * #2424
    * __->__ #2423
  • fix: introduce EventMsg::TurnAborted (#2365)
    Introduces `EventMsg::TurnAborted` that should be sent in response to
    `Op::Interrupt`.
    
    In the MCP server, updates the handling of a
    `ClientRequest::InterruptConversation` request such that it sends the
    `Op::Interrupt` but does not respond to the request until it sees an
    `EventMsg::TurnAborted`.
  • Added MCP server command to enable authentication using ChatGPT (#2373)
    This PR adds two new APIs for the MCP server: 1) loginChatGpt, and 2)
    cancelLoginChatGpt. The first starts a login server and returns a local
    URL that allows for browser-based authentication, and the second
    provides a way to cancel the login attempt. If the login attempt
    succeeds, a notification (in the form of an event) is sent to a
    subscriber.
    
    I also added a timeout mechanism for the existing login server. The
    loginChatGpt code path uses a 10-minute timeout by default, so if the
    user fails to complete the login flow in that timeframe, the login
    server automatically shuts down. I tested the timeout code by manually
    setting the timeout to a much lower number and confirming that it works
    as expected when used e2e.
  • feat: introduce ClientRequest::SendUserTurn (#2345)
    This adds a new request type, `SendUserTurn`, that makes it possible to
    submit a `Op::UserTurn` operation (introduced in #2329) to a
    conversation. This PR also adds a new integration test that verifies
    that changing from `AskForApproval::UnlessTrusted` to
    `AskForApproval::Never` mid-conversation ensures that an elicitation is
    no longer sent for running `python3 -c print(42)`.
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/2345).
    * __->__ #2345
    * #2329
    * #2343
    * #2340
    * #2338
  • fix: try to fix flakiness in test_shell_command_approval_triggers_elicitation (#2344)
    I still see flakiness in
    `test_shell_command_approval_triggers_elicitation()` on occasion where
    `MockServer` claims it has not received all of its expected requests.
    
    I recently introduced a similar type of test in #2264,
    `test_codex_jsonrpc_conversation_flow()`, which I have not seen flake
    (yet!), so this PR pulls over two things I did in that test:
    
    - increased `worker_threads` from `2` to `4`
    - added an assertion to make sure the `task_complete` notification is
    received
    
    Honestly, I'm still not sure why `MockServer` claims it sometimes does
    not receive all its expected requests given that we assert that the
    final `JSONRPCResponse` is read on the stream, but let's give this a
    shot.
    
    Assuming this fixes things, my hypothesis is that the increase in
    `worker_threads` helps because perhaps there are async tasks in
    `MockServer` that do not reliably complete fully when there are not
    enough threads available? If that is correct, it seems like the test
    would still be flaky, though perhaps with lower frequency?