Skip to content

feat(app-server): run tui and exec through in-process app-server#19

Open
fcoury wants to merge 51 commits intomainfrom
feat/tui-app-server
Open

feat(app-server): run tui and exec through in-process app-server#19
fcoury wants to merge 51 commits intomainfrom
feat/tui-app-server

Conversation

@fcoury
Copy link
Owner

@fcoury fcoury commented Mar 4, 2026

In-process app-server for TUI and exec

Problem

This PR routes codex-tui and codex-exec through app-server in-process so all conversational surfaces share one runtime and one protocol mapping layer. Before this integration, those surfaces mostly used core directly (ThreadManager/CodexThread) and carried parallel lifecycle/routing logic.

The in-process path keeps request/response/event flow typed over bounded mpsc channels, avoiding JSON round-trips inside a single process while preserving the JSON-RPC surface at transport boundaries.

Mental model

The app-server runtime is embedded via codex-app-server-client. codex-tui and codex-exec depend directly on codex-app-server-client (and codex-app-server-protocol); codex-app-server-client depends on codex-app-server.

Communication uses bounded mpsc channels carrying native Rust types. JSON serialization/deserialization is only used at external boundaries (stdio, websocket).

The layering is:

Surface (tui / exec)
  └─ InProcessAppServerClient   (app-server-client crate — Send-safe facade, backpressure, lag detection)
       └─ InProcessClientHandle  (app-server::in_process — raw runtime handle, oneshot correlation)
            └─ MessageProcessor  (app-server — shared processor used by transport runtimes)
                 └─ CodexThread  (core — turn execution, tool calls, model interaction)

The network server path (run_main_with_transport) remains in place. The in-process path is a parallel entry point that uses a synthetic ConnectionId(0) and the same route_outgoing_envelope broadcast/targeted-routing logic as transport-backed connections.

Non-goals

  • Removing the stdio/websocket transport runtime. stdio remains a primary integration path; websocket remains experimental/unsupported.
  • Changing the wire protocol. The JSON-RPC schema remains the same.
  • Multi-connection in-process support. Each InProcessClientHandle owns exactly one synthetic connection (ConnectionId(0)).

Tradeoffs

Decision Upside Downside
Typed CoreEvent variant in OutgoingMessage instead of transport-ready pre-serialized notifications Zero-copy event delivery in-process; eliminates serde round-trip on core events Network path still serializes for wire output
Two-layer client (InProcessAppServerClient wrapping InProcessClientHandle) Send-safe facade; automatic lag recovery; auto-reject of undeliverable ServerRequests Extra channel hop adds a small amount of latency
Thread listeners retained after last subscriber disconnects Enables resume without restarting a thread Leaked threads if the caller never shuts down explicitly
AgentCommand intent layer in TUI Decouples UI component events from protocol-level Op; centralizes approval dispatch One more enum to maintain
Backward-compatible method aliases (e.g., memory/drop alongside thread/memories/drop) Existing clients keep working Method table grows; clients need clear canonical-method guidance

Architecture

New crates

  • codex-app-server-client — Public facade for in-process app-server embedding. Provides InProcessAppServerClient with request, request_typed, notify, resolve_server_request, reject_server_request, next_event, and shutdown. Manages a worker task that decouples the caller from runtime internals, tracks dropped events, and emits Lagged events when the consumer falls behind.

New modules

  • app-server/src/in_process.rs — Low-level runtime. Spawns a Tokio task containing the MessageProcessor select loop. Uses channel pairs for client->runtime commands, runtime->client events, and outgoing envelope routing (shared with transport paths via route_outgoing_envelope). Request/response correlation uses HashMap<RequestId, oneshot::Sender>.
  • app-server/src/thread_state.rsThreadStateManager and per-thread ThreadState. Tracks connection subscriptions, listener generations, turn history builders, and pending interrupt/rollback queues across runtime paths.
  • tui/src/agent_command.rsAgentCommand enum. Intent-level actions (Interrupt, ExecApproval, PatchApproval, ResolveElicitation) that convert to protocol/core operations.

Protocol additions (v2)

Canonical RPCs:
thread/undo, thread/context/update, thread/config/reload, thread/shutdown, thread/memories/drop, thread/memories/update, thread/userShellCommand/run, history/append, history/entry/read, prompt/custom/list, mcp/tools/list.

Compatibility aliases (deprecated, still accepted):
thread/turnContext/set, memory/drop, memory/update, thread/shellCommand/run, history/add.

Server→client request: item/mcpToolCall/requestElicitation (MCP elicitation flow).

Notifications:
warning, backgroundEvent, turn/streamError, thread/undo/started, thread/undo/completed, thread/shutdown/completed, mcpServer/startup/updated, mcpServer/startup/completed, skills/updated.

Enriched thread bootstrap responses: ThreadStartResponse,
ThreadResumeResponse, and ThreadForkResponse include history_log_id, history_entry_count, forked_from_thread_id, and network_proxy.

Outgoing message refactor

OutgoingMessage carries typed core events via OutgoingMessage::CoreEvent { thread_id, event } and keeps app-server-native notifications in OutgoingMessage::AppServerNotification. send_core_event_to_connections handles core-event fanout, and legacy_event_method is used where transport writers need wire method names.

Tracing

Codex::submit captures W3C trace context (traceparent/tracestate) into each Submission. The submission dispatch loop re-parents spans from this context so run_turn and deeper core operations appear under the originating app-server request in distributed traces.

Observability

  • tracing::debug! logs on thread listener retention, teardown, and connection disconnect with thread_id, connection_id, and listener_generation fields.
  • tracing::warn! on dropped in-process events (queue full), unmatched response IDs, and failed envelope sends.
  • tracing::trace! on every outgoing notification with targeted connection count.
  • W3C trace context propagation means app-server request spans now parent core execution spans in OpenTelemetry backends.

Tests

  • in_process.rs unit tests — Initialize/typed-request round-trip, session source mapping, zero-capacity channel clamping.
  • app-server-client unit tests — Typed request round-trip, JSON-RPC error reporting, surface->session-source mapping, tiny channel capacity behavior.
  • Integration test suitesarchive_thread, codex_message_processor_flow, config, create_thread, fork_thread, interrupt, list_resume, login, output_schema, send_message, set_default_model, user_agent, user_info.
  • TUI chatwidget tests — in-process agent spawning, pending request scoping, lag recovery.

Summary by CodeRabbit

Release Notes

  • New Features

    • Plugin marketplace discovery and installation with app authentication awareness
    • MCP server elicitation support for interactive form-based requests
    • Enhanced command execution with real-time streaming, resize, and termination capabilities
    • Image generation with automatic file saving
    • Advanced web search configuration including location and context preferences
    • Artifact runtime management for improved JavaScript execution
  • Improvements

    • Refined macOS permission handling and seatbelt profile extensions
    • Startup context integration for realtime conversations
    • Thread name update broadcasting across connected clients
  • Platform Support

    • Windows Package Manager integration for automated releases

Copilot AI review requested due to automatic review settings March 4, 2026 19:31
@coderabbitai
Copy link

coderabbitai bot commented Mar 4, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces an in-process app-server client/runtime, expands the protocol (MCP elicitation, plugin list/install, command exec streaming/controls, macOS permissions, web search config), adds command execution manager, plugin marketplace/listing, artifacts runtime management, realtime startup context, tracing/signal handling, and extensive tests and schema/type updates.

Changes

Cohort / File(s) Summary
Workspace & Manifests
codex-rs/Cargo.toml, app-server-client/Cargo.toml, app-server-protocol/Cargo.toml, artifacts/Cargo.toml
Adds new crate app-server-client; wires workspace deps (codex-app-server-client, fd-lock, rmcp features); artifacts adds which.
New In-Process Client
app-server-client/src/lib.rs, app-server-client/README.md
Adds typed, backpressure-aware in-process client facade with async API (request/notify/events/shutdown) and typed request errors.
In-Process Runtime
app-server/src/in_process.rs, app-server/src/lib.rs, app-server/src/app_server_tracing.rs, app-server/src/message_processor.rs
Implements in-process runtime and typed processing paths; exposes session_source in args; adds tracing for typed requests; signal-based shutdown.
Command Execution Subsystem
app-server/src/command_exec.rs, app-server/src/codex_message_processor.rs, app-server/src/outgoing_message.rs
Adds CommandExecManager with PTY/streaming, resize/write/terminate, output delta notifications; integrates into message processor; global notification helper.
MCP Elicitation Flow
app-server/src/bespoke_event_handling.rs, app-server-protocol/src/protocol/common.rs, app-server-protocol/src/protocol/v2.rs, app-server-protocol/src/protocol/mappers.rs, app-server-protocol/src/protocol/v1.rs
Introduces server→client MCP elicitation request/response types and handling; maps new params; updates notifications and tests.
Protocol Schemas (JSON)
app-server-protocol/schema/json/*, .../v2/*
Adds/updates schemas: MCP elicitation params/response, plugin list, command exec (streaming/write/resize/terminate), macOS permissions model, web search config, event payloads (saved_path), and related refs.
Protocol Types (TypeScript)
app-server-protocol/schema/typescript/*, .../v2/*, .../index.ts
Generates/updates TS types to match schemas (elicitation, plugin list, command exec, macOS seatbelt extensions, web search config, notifications).
Core: MCP, Plugins, Marketplace
core/src/mcp_*, core/src/plugins/*, core/src/connectors.rs, core/src/mentions.rs
Adds ToolPluginProvenance; augments ToolInfo/AppInfo with plugin_display_names; marketplace listing/resolve by absolute path; plugin capability summaries; plugin mention collection and instruction injections.
Core: Command Exec & Sandboxing
core/src/exec.rs, core/src/sandboxing/*, core/src/tools/handlers/*, core/src/seatbelt.rs
Refactors exec APIs with after_spawn; introduces effective sandbox permissions and macOS seatbelt merging; updates additional permissions validation; socket policy refinements.
Core: Realtime/Context & Tracing
core/src/realtime_context.rs, core/src/realtime_conversation.rs, core/src/codex.rs, core/src/stream_events_utils.rs, core/src/event_mapping.rs, core/src/features.rs, core/src/state/turn.rs, core/src/models_manager/model_info.rs
Adds realtime startup context builder; propagates trace_id in TurnContext; saves image outputs; feature flag for MCP elicitation; pending elicitation tracking; model info updates.
Artifacts Runtime Management
artifacts/src/{client.rs,lib.rs}, artifacts/src/runtime/*, artifacts/src/tests.rs, artifacts/README.md
Introduces managed runtime download/cache/load (GitHub releases), JS runtime discovery (Node/Electron/Codex app), client uses unified JS runtime; adds tests and docs.
Server Docs & Config
app-server/README.md, app-server/src/config_api.rs, config/src/config_requirements.rs, core/config.schema.json, core/src/config/*
Updates docs for new endpoints/params and MCP elicitations; removes deprecated network admin fields; adds feature toggles; simplifies config flags.
Tests & Utilities
app-server/tests/**, app-server-test-client/src/lib.rs, app-server/src/bin/test_notify_capture.rs, app-server/tests/common/*, codex-api/tests/*
Adds extensive E2E tests for elicitation, command exec streaming/controls, plugin list/install, thread name broadcast, signals, windows sandbox; opt-out notification addition; notifier test binary.
Connectors & API Adjustments
chatgpt/src/connectors.rs, codex-api/src/endpoint/realtime_websocket/methods.rs
Propagates plugin_display_names; switches realtime voice value.
Dev/CI
.devcontainer/Dockerfile, .github/workflows/rust-release.yml
Adds libcap-dev; adds Winget publish job post-release.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Caller as Client App
  participant IPC as InProcessAppServerClient
  participant RT as InProcess Runtime
  participant MP as MessageProcessor
  participant DB as State DB

  Caller->>IPC: start(args)
  IPC->>RT: bootstrap channels
  RT->>MP: Initialize(request)
  MP->>DB: load state/config
  DB-->>MP: state/config
  MP-->>RT: Initialized(notification)
  RT-->>IPC: ready

  Caller->>IPC: request/notify(...)
  IPC->>RT: enqueue command
  RT->>MP: process (typed path)
  MP-->>RT: ServerNotification/ServerRequest
  RT-->>IPC: InProcessServerEvent(...)
  IPC-->>Caller: next_event()
  Note over IPC,RT: shutdown() drains and joins
Loading
sequenceDiagram
  autonumber
  actor Client
  participant S as App Server
  participant CEM as CommandExecManager
  participant OS as OS Process

  Client->>S: command/exec (params)
  S->>CEM: start(process params)
  CEM->>OS: spawn (pty/pipe)
  par Stream stdout/stderr
    CEM-->>S: command/exec/outputDelta (base64)
    S-->>Client: notification(outputDelta)
  and Control
    Client->>S: command/exec/write/resize/terminate
    S->>CEM: write/resize/terminate
    CEM->>OS: apply control
  end
  OS-->>CEM: exit code + buffers
  CEM-->>S: CommandExecResponse
  S-->>Client: response(exitCode, stdout, stderr)
Loading
sequenceDiagram
  autonumber
  actor User
  participant S as App Server
  participant MCP as Apps MCP Server
  actor CLI as Client Surface

  User->>S: Start turn calls MCP tool
  S->>MCP: call_tool(...)
  MCP-->>S: Elicitation(form/url)
  S-->>CLI: ServerRequest mcpServer/elicitation/request
  CLI-->>S: Resolve/Reject (action + content/meta)
  S->>MCP: create_elicitation_result(action, content)
  MCP-->>S: tool result
  S-->>CLI: turn/items, completed
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

I twitch my ears at streams that flow,
PTYs purr and deltas go—
Plugins hop in tidy rows,
Elicitations ask, “Who knows?”
In-process burrow, quiet, keen;
I thump: “Shipped!”—a silky sheen.
(\_/)<3

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/tui-app-server

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

🧹 Nitpick comments (10)
codex-rs/app-server/src/thread_state.rs (1)

45-55: Consider using doc comments for enum variants.

The ThreadListenerCommand enum and its variants use regular comments (//) while other structs in this file (e.g., TurnSummary, ThreadState) use doc comments (///). Using doc comments would provide consistent documentation style and enable rustdoc to generate documentation for these variants.

✏️ Suggested change
-// ThreadListenerCommand is used to perform operations in the context of the thread listener, for serialization purposes.
+/// Commands executed in the thread listener context for serialization purposes.
 pub(crate) enum ThreadListenerCommand {
-    // SendThreadResumeResponse is used to resume an already running thread by sending the thread's history to the client and atomically subscribing for new updates.
+    /// Resume an already running thread by sending history to the client and atomically subscribing for updates.
     SendThreadResumeResponse(Box<PendingThreadResumeRequest>),
-    // ResolveServerRequest is used to notify the client that the request has been resolved.
-    // It is executed in the thread listener's context to ensure that the resolved notification is ordered with regard to the request itself.
+    /// Notify the client that a server request has been resolved.
+    ///
+    /// Executed in the listener's context to ensure ordering with respect to the request.
     ResolveServerRequest {
         request_id: RequestId,
         completion_tx: oneshot::Sender<()>,
     },
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/src/thread_state.rs` around lines 45 - 55, The enum
ThreadListenerCommand and its variants (SendThreadResumeResponse,
ResolveServerRequest) are using line comments; change them to doc comments so
rustdoc picks them up and documentation style matches other types like
TurnSummary and ThreadState—replace the leading // with /// on the enum and each
variant, and include brief descriptions for Box<PendingThreadResumeRequest>,
RequestId and the completion_tx oneshot::Sender<()> field to explain their
roles.
codex-rs/README.md (1)

56-58: Minor wording polish: use “in-process” for consistency.

Line 56 currently reads as “in process”; hyphenating improves readability and matches the term used elsewhere in the PR context.

Suggested doc tweak
-Both `codex exec` and the interactive TUI run through `codex-app-server` in
-process (via `codex-app-server-client`) for a shared conversational runtime.
+Both `codex exec` and the interactive TUI run through `codex-app-server` in-process
+(via `codex-app-server-client`) for a shared conversational runtime.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/README.md` around lines 56 - 58, Update the README sentence that
mentions how codex exec and the interactive TUI run through codex-app-server:
change the phrase "in process" to the hyphenated form "in-process" to match
project terminology and improve consistency; the sentence references "codex
exec", "interactive TUI", "codex-app-server", and "codex-app-server-client".
codex-rs/app-server-protocol/schema/json/v2/HistoryEntryReadResponse.json (1)

26-40: Consider making entry a required property.

The entry property is defined but not listed in a required array. This means a valid response could be an empty object {} rather than { "entry": null }. For API clarity and explicit null handling, consider adding:

   "properties": {
     "entry": {
       ...
     }
   },
+  "required": [
+    "entry"
+  ],
   "title": "HistoryEntryReadResponse",

This ensures consumers always receive the entry field (either with a value or explicitly null), making client-side handling more predictable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server-protocol/schema/json/v2/HistoryEntryReadResponse.json`
around lines 26 - 40, HistoryEntryReadResponse's "entry" property is optional
right now; make it required so callers always receive an "entry" field (which
can be a HistoryEntry or null). Update the JSON schema for
HistoryEntryReadResponse by adding a "required": ["entry"] array alongside the
existing "properties" so the schema enforces presence of the "entry" property;
target the HistoryEntryReadResponse definition (the "entry" property and
surrounding object) when applying the change.
codex-rs/app-server-protocol/schema/json/v2/McpServerStartupUpdatedNotification.json (1)

36-53: Minor inconsistency: failed variant is missing title property.

The other McpServerStartupStatus variants (starting, ready, cancelled) include a title property, but the failed variant does not. For consistency and better tooling support (e.g., code generators that use titles for type names):

         "required": [
           "error",
           "state"
         ],
+        "title": "FailedMcpServerStartupStatus",
         "type": "object"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@codex-rs/app-server-protocol/schema/json/v2/McpServerStartupUpdatedNotification.json`
around lines 36 - 53, The "failed" variant in the
McpServerStartupUpdatedNotification JSON schema is missing a title property;
update the object schema for the failed state (the object under properties with
required ["error","state"] that corresponds to the "failed" enum) to include a
"title" property consistent with the other McpServerStartupStatus variants
(e.g., "title": "failed") so code generators/tooling treat it like the other
variants.
codex-rs/app-server/src/app_server_tracing.rs (1)

74-80: Use request-specific RPC method labels for in-process spans.

Setting both otel.name and rpc.method to "in_process" collapses all typed requests into one trace bucket. Consider recording the concrete request method (variant-level mapping) to preserve actionable observability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/src/app_server_tracing.rs` around lines 74 - 80, The span
currently hardcodes otel.name and rpc.method to "in_process", which collapses
all requests; change the info_span! call that creates span (the span variable
via info_span!) to use the concrete request method string instead: extract or
compute a method name from the incoming request (e.g., add/call a small helper
like request_method(req) or match on the request enum/variant to return a &str)
and pass that value into otel.name and rpc.method fields of info_span! so each
span is labeled by the real RPC method.
codex-rs/tui/src/chatwidget.rs (1)

7698-7700: Consolidate op sending through submit_op or wrapper to ensure consistent logging and state setup.

The op_sender() method at line 7698 allows direct channel access, and ops sent through this path can bypass the logging (line 7705) and review task state setup (lines 7706-7708) in submit_op. While the codebase handles non-active thread ops in submit_op_to_thread by calling log_outbound_op explicitly, Op::Shutdown is sent directly without logging in request_shutdown_for_inactive_threads (app.rs:894), and the Review task state setup is skipped for all non-active thread ops. Consider either making op_sender private and routing all ops through controlled submission points, or wrapping the returned sender to enforce consistent side effects.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/tui/src/chatwidget.rs` around lines 7698 - 7700, op_sender() exposes
a raw UnboundedSender<Op> allowing ops (e.g., Op::Shutdown) to bypass
submit_op's logging and review task state setup; make op_sender private or
replace it with a controlled wrapper that routes all sends through submit_op (or
a new submit_op_wrapper) so that log_outbound_op and the review-task state setup
in submit_op/submit_op_to_thread always run; update callers that currently call
op_sender().send(...) (including request_shutdown_for_inactive_threads) to use
submit_op/submit_op_wrapper and ensure submit_op_to_thread still explicitly
calls log_outbound_op for non-active threads.
codex-rs/app-server/src/bespoke_event_handling.rs (1)

243-262: Add branch tests for newly introduced event handlers.

I don’t see direct tests in this file for the new warning/background/elicitation/undo/MCP-startup/shutdown-complete branches. A small set of focused tests here would reduce regression risk.

Also applies to: 641-678, 1612-1707

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/src/bespoke_event_handling.rs` around lines 243 - 262,
Add focused tests that cover the new event branches (EventMsg::Warning,
EventMsg::BackgroundEvent and the other newly added branches like
elicitation/undo/MCP-startup/shutdown-complete) by exercising the code paths in
bespoke_event_handling.rs: construct the appropriate EventMsg variants and
assert that outgoing.send_server_notification is awaited and receives the
expected ServerNotification variants (e.g., ServerNotification::Warning with
WarningNotification, ServerNotification::BackgroundEvent with
BackgroundEventNotification). Use the existing test harness/mocks for outgoing
(or implement a simple mock/spy for send_server_notification) to capture sent
notifications, and add similar tests for the other branches mentioned (lines
~641-678 and ~1612-1707) to ensure each branch serializes the correct
notification type and message. Ensure each test is small, deterministic, and
asserts the notification payload matches the event.message.
codex-rs/app-server-protocol/src/protocol/common.rs (1)

1596-1691: Consider adding serialization tests for the remaining new notifications.

Great coverage overall; adding tests for BackgroundEvent, ThreadUndoStarted, ThreadUndoCompleted, McpServerStartupCompleted, and SkillsUpdated would close the remaining gaps.

Also applies to: 911-947

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server-protocol/src/protocol/common.rs` around lines 1596 -
1691, Add unit tests mirroring the existing serialize_* tests for the missing
notification variants: BackgroundEvent, ThreadUndoStarted, ThreadUndoCompleted,
McpServerStartupCompleted, and SkillsUpdated by creating
ServerNotification::BackgroundEvent(...),
ServerNotification::ThreadUndoStarted(...),
ServerNotification::ThreadUndoCompleted(...),
ServerNotification::McpServerStartupCompleted(...), and
ServerNotification::SkillsUpdated(...) instances (using the v2 types like
v2::BackgroundEventNotification, v2::ThreadUndoStartedNotification, etc.), then
assert their serde_json::to_value outputs equal the expected json!() structures
just like the existing serialize_warning_notification and
serialize_stream_error_notification tests; place them alongside the other
serialize_* tests and follow the same pattern of constructing the notification,
building the expected JSON object with correct field names (e.g., "method" and
"params"), and using assert_eq!(json!(...),
serde_json::to_value(&notification)?).
codex-rs/app-server/src/codex_message_processor.rs (1)

2739-3367: Consider extracting a shared helper for the new thread-op handlers.

thread_undo, thread_shutdown, memory_drop, memory_update, thread_shell_command_run, and history_add repeat the same load/submit/respond/error pattern. A common helper would reduce duplication and future drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/src/codex_message_processor.rs` around lines 2739 - 3367,
Many handlers (thread_undo, thread_shutdown, memory_drop, memory_update,
thread_shell_command_run, history_add) duplicate the pattern of calling
load_thread, sending load errors, submitting an Op on the thread, and sending a
response or internal error—extract a shared async helper (e.g.,
with_thread_submit) that takes (&self, request_id: ConnectionRequestId,
thread_id: String, F) where F is an async closure that receives the loaded
thread and returns Result<ResponsePayload, E> (or directly invokes
thread.submit(Op::...)). Implement the helper to call
self.load_thread(&thread_id).await and on Err call self.outgoing.send_error, on
Ok run the closure, map Ok to self.outgoing.send_response(request_id,
payload).await and map Err to self.send_internal_error(request_id,
format!(...)). Update the listed functions (thread_undo, thread_shutdown,
memory_drop, memory_update, thread_shell_command_run, history_add) to call
with_thread_submit and pass a closure that performs the thread.submit(Op::...)
and returns the appropriate response payload (or turn_id for shell command) to
eliminate duplication while preserving existing
send_invalid_request_error/send_internal_error behavior.
codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json (1)

13811-13851: ThreadContextSnapshot should mirror the override surface for client reconciliation.

Line 15535 allows setting personality, serviceTier, and summary, but Line 13811 snapshot payload does not expose them back. That makes state reconciliation lossy.

♻️ Proposed schema alignment
 "ThreadContextSnapshot": {
   "properties": {
     "approvalPolicy": { "$ref": "#/definitions/v2/AskForApproval" },
     "cwd": { "type": "string" },
     "model": { "type": "string" },
     "modelProvider": { "type": "string" },
+    "personality": {
+      "anyOf": [
+        { "$ref": "#/definitions/v2/Personality" },
+        { "type": "null" }
+      ]
+    },
     "reasoningEffort": {
       "anyOf": [
         { "$ref": "#/definitions/v2/ReasoningEffort" },
         { "type": "null" }
       ]
     },
     "sandbox": { "$ref": "#/definitions/v2/SandboxPolicy" },
+    "serviceTier": {
+      "anyOf": [
+        { "$ref": "#/definitions/v2/ServiceTier" },
+        { "type": "null" }
+      ]
+    },
+    "summary": {
+      "anyOf": [
+        { "$ref": "#/definitions/v2/ReasoningSummary" },
+        { "type": "null" }
+      ]
+    },
     "threadId": { "type": "string" }
   }
 }

Also applies to: 15535-15654

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json`
around lines 13811 - 13851, ThreadContextSnapshot currently omits the client
override fields used elsewhere (personality, serviceTier, summary), causing
lossy reconciliation; add "personality", "serviceTier", and "summary" to the
ThreadContextSnapshot "properties" object and model them with the same schema
references/types used by the override surface (use the same v2 definitions
referenced at the override location), and keep them optional (not in "required")
so the snapshot mirrors the override surface for client reconciliation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@codex-rs/app-server-client/src/lib.rs`:
- Around line 469-491: The shutdown method can early-return on command_result
(the `??` after mapping) before performing worker cleanup; change the flow so
you send `ClientCommand::Shutdown { response_tx }`, await the `response_rx` with
`timeout(SHUTDOWN_TIMEOUT, response_rx)`, capture the command_result (or its
mapped IoError) into a local variable instead of using `??` to return
immediately, then always run the worker cleanup logic that awaits or aborts
`worker_handle` (the `timeout(SHUTDOWN_TIMEOUT, &mut worker_handle).await`
branch and abort/await sequence) and only after that return the captured error
(if any). Ensure references to `command_tx`, `response_rx`, `timeout`,
`SHUTDOWN_TIMEOUT`, and `worker_handle` are used so cleanup always runs even
when the shutdown command errors.

In `@codex-rs/app-server-protocol/schema/typescript/v2/HistoryEntryReadParams.ts`:
- Line 5: HistoryEntryReadParams and the HistoryEntry u64 fields are generating
TypeScript bigint types which break JSON serialization; update the Rust
definitions for the affected fields (e.g., the log_id field in the
HistoryEntryReadParams struct and the corresponding u64 in HistoryEntry) to
include the #[ts(type = "number")] annotation so they generate number types in
TypeScript consistent with other fields like
SessionConfiguredEvent.history_log_id, or alternatively change the generated
TypeScript types to string and add conversion at the serialization
boundary—prefer adding #[ts(type = "number")] on the Rust u64 fields to match
project conventions.

In `@codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts`:
- Line 11: The generated TypeScript ThreadForkResponse forces the 64-bit Rust
field history_log_id into a JS number (historyLogId) losing precision; edit the
Rust declaration in protocol v2.rs where history_log_id is annotated with
#[ts(type = "number")] and remove that override so ts-rs will emit bigint for
u64, matching how SessionConfiguredNotification is handled, then regenerate the
TypeScript types.

In `@codex-rs/app-server-protocol/src/protocol/v2.rs`:
- Around line 2497-2504: The HistoryEntry struct's ts field is u64 but missing
the ts-rs override, causing TypeScript to emit bigint; add the ts-rs attribute
to force a number: annotate the ts field in struct HistoryEntry with #[ts(type =
"number")] (matching other u64 fields in this file) so the generated
HistoryEntry.ts uses number for ts and aligns with JSON transport.

In `@codex-rs/app-server/src/codex_message_processor.rs`:
- Around line 3380-3408: The code calls load_thread(&thread_id) but then
performs a global lookup via codex_core::lookup_global_history_entry(log_id,
offset, ...) allowing cross-thread reads; change to use a thread-scoped lookup
or validate the looked-up entry belongs to the requested thread: either call a
thread-aware API (e.g., lookup_thread_history_entry or similar) passing
thread_id along with log_id/offset, or after
codex_core::lookup_global_history_entry verify the returned entry's thread
identifier matches thread_id and return an error (use self.send_internal_error
or self.outgoing.send_error) if it does not; update references around
load_thread, lookup_global_history_entry, and the HistoryEntryReadResponse
construction to ensure only entries from the requested thread are returned.
- Around line 7634-7637: The struct literal for PendingThreadResumeRequest is
currently hard-coding synthetic metadata (history_log_id, history_entry_count,
forked_from_thread_id, network_proxy) which can overwrite true client state;
instead, populate these fields from the real thread/state when constructing
PendingThreadResumeRequest (reuse the same population logic used by the
non-running resume path or pull values from the existing Thread/ThreadMetadata),
and remove the zero/None defaults so running-thread resume responses carry
actual metadata rather than placeholders.

In `@codex-rs/app-server/src/in_process.rs`:
- Around line 205-210: The send on self.client_tx inside shutdown() (the
InProcessClientMessage::Shutdown { done_tx } send) is awaited without a timeout,
which can hang indefinitely if the channel is full; wrap the send in a bounded
wait (e.g. tokio::time::timeout with a short Duration) or use try_send with
fallback, and handle the timeout/error by logging or returning so shutdown() can
proceed to the existing abort/timeout logic. Concretely: in shutdown() replace
the direct .await of self.client_tx.send(...) with a timeout-wrapped send (or
try_send with retry/spawn), detect elapsed or send failure, and ensure you still
signal failure (log/propagate) and continue so the later abort logic runs.

In `@codex-rs/app-server/src/message_processor.rs`:
- Around line 319-329: The handlers process_notification (JSONRPCNotification)
and process_client_notification (ClientNotification) currently log entire
payloads; change them to avoid emitting full protocol objects by logging only
safe, redacted summary fields (e.g., method/type, id, and a masked or
length-only params summary) or a short constant message; update the tracing
calls in process_notification and process_client_notification to extract and log
non-sensitive metadata instead of the full Debug of notification objects, and
ensure any other similar handlers (the ones mentioned around the other
occurrences of full-object logging) follow the same redaction pattern.

In `@codex-rs/exec/src/lib.rs`:
- Around line 750-755: The current spawned task awaits tokio::signal::ctrl_c()
only once and sends to interrupt_tx a single time, so subsequent Ctrl+C presses
are ignored; change the tokio::spawn block that references
interrupt_tx/interrupt_rx and tokio::signal::ctrl_c() to repeatedly await ctrl_c
in a loop and send a notification for every signal (and exit the loop if send
fails because the receiver was dropped), ensuring multiple interrupts are
delivered to interrupt_rx for later turns.
- Around line 147-173: The SessionConfiguredEvent construction in
session_configured_from_start_response currently hardcodes history_log_id and
history_entry_count to 0; replace those zeros by forwarding the values from the
incoming ThreadStartResponse (use response.history_log_id and
response.history_entry_count). Do the same for the sibling converter
(session_configured_from_resume_response) which maps ThreadResumeResponse ->
SessionConfiguredEvent, ensuring both history_log_id and history_entry_count are
copied from the respective response struct fields rather than hardcoded.

In `@codex-rs/tui/src/app_event_sender.rs`:
- Line 21: The matches! call is moving `event` (AppEvent is not Copy) which
breaks later uses; change the pattern to match a reference instead (use `&event`
in the matches! invocation) so it borrows rather than moves, keeping subsequent
uses of `event` valid and preserving the existing patterns `AppEvent::CodexOp(_)
| AppEvent::AgentCommand(_)`.

In `@codex-rs/tui/src/app.rs`:
- Around line 1423-1425: reset_thread_event_state() currently clears
thread_attach_retry_state but does not cancel previously spawned retry tasks, so
those tasks can still enqueue stale AppEvent::ThreadCreated after a reset;
update reset_thread_event_state to first cancel/abort any outstanding retry
tasks (e.g., by storing and signalling a CancellationToken/AbortHandle or
keeping JoinHandles and aborting them) referenced from thread_attach_retry_state
before calling clear(), and harden the retry task callback that enqueues
AppEvent::ThreadCreated to verify the current attach context/state (e.g., check
active_thread_id or a unique attach attempt id) before emitting the event; apply
the same cancellation/validation change to the other reset site around the code
referenced at 3399-3403.
- Around line 3373-3375: The duplicate-scheduling guard currently compares
now.saturating_duration_since(state.last_scheduled_at) against a fixed
THREAD_ATTACH_RETRY_DELAY, which allows later backoff retries (500ms/750ms/...)
to overlap; update both checks (the one using state.last_scheduled_at around the
shown block and the similar one at the other occurrence) to compare against a
dynamic delay derived from the thread's current retry/backoff state instead of
the fixed constant—e.g., compute a delay = THREAD_ATTACH_RETRY_DELAY *
(state.attach_attempts.saturating_add(1)) or use an existing per-thread
next_retry_delay field (state.next_retry_delay) and replace the fixed
THREAD_ATTACH_RETRY_DELAY in the condition with that computed/field value so
retries are de-duplicated according to the actual backoff schedule.

In `@codex-rs/tui/src/chatwidget/agent.rs`:
- Around line 603-605: When handling EventMsg::TurnComplete(_) and
EventMsg::TurnAborted(_) (the match arm that currently sets current_turn_id =
None), also clear the pending_server_requests map so stale approval entries
don't leak into subsequent turns; update the same matching logic that touches
current_turn_id (and the analogous spot near the other TurnComplete/TurnAborted
handler) to call pending_server_requests.clear() or remove only entries
associated with the finished turn id if you prefer more targeted cleanup,
ensuring both current_turn_id and pending_server_requests are synchronized when
a turn ends.
- Around line 992-1017: The code currently removes the pending ID from
pending_server_requests.exec_approvals before awaiting
client.resolve_server_request, which loses the mapping if the async call fails;
instead, look up or clone the request_id without removing it (e.g., use
get(&id).cloned()), call client.resolve_server_request(request_id,
result).await, and only call pending_server_requests.exec_approvals.remove(&id)
after the resolve succeeds; keep the same send_warning_event/send_error_event
flows and ensure errors do not drop the pending entry so users can retry.

---

Nitpick comments:
In
`@codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json`:
- Around line 13811-13851: ThreadContextSnapshot currently omits the client
override fields used elsewhere (personality, serviceTier, summary), causing
lossy reconciliation; add "personality", "serviceTier", and "summary" to the
ThreadContextSnapshot "properties" object and model them with the same schema
references/types used by the override surface (use the same v2 definitions
referenced at the override location), and keep them optional (not in "required")
so the snapshot mirrors the override surface for client reconciliation.

In `@codex-rs/app-server-protocol/schema/json/v2/HistoryEntryReadResponse.json`:
- Around line 26-40: HistoryEntryReadResponse's "entry" property is optional
right now; make it required so callers always receive an "entry" field (which
can be a HistoryEntry or null). Update the JSON schema for
HistoryEntryReadResponse by adding a "required": ["entry"] array alongside the
existing "properties" so the schema enforces presence of the "entry" property;
target the HistoryEntryReadResponse definition (the "entry" property and
surrounding object) when applying the change.

In
`@codex-rs/app-server-protocol/schema/json/v2/McpServerStartupUpdatedNotification.json`:
- Around line 36-53: The "failed" variant in the
McpServerStartupUpdatedNotification JSON schema is missing a title property;
update the object schema for the failed state (the object under properties with
required ["error","state"] that corresponds to the "failed" enum) to include a
"title" property consistent with the other McpServerStartupStatus variants
(e.g., "title": "failed") so code generators/tooling treat it like the other
variants.

In `@codex-rs/app-server-protocol/src/protocol/common.rs`:
- Around line 1596-1691: Add unit tests mirroring the existing serialize_* tests
for the missing notification variants: BackgroundEvent, ThreadUndoStarted,
ThreadUndoCompleted, McpServerStartupCompleted, and SkillsUpdated by creating
ServerNotification::BackgroundEvent(...),
ServerNotification::ThreadUndoStarted(...),
ServerNotification::ThreadUndoCompleted(...),
ServerNotification::McpServerStartupCompleted(...), and
ServerNotification::SkillsUpdated(...) instances (using the v2 types like
v2::BackgroundEventNotification, v2::ThreadUndoStartedNotification, etc.), then
assert their serde_json::to_value outputs equal the expected json!() structures
just like the existing serialize_warning_notification and
serialize_stream_error_notification tests; place them alongside the other
serialize_* tests and follow the same pattern of constructing the notification,
building the expected JSON object with correct field names (e.g., "method" and
"params"), and using assert_eq!(json!(...),
serde_json::to_value(&notification)?).

In `@codex-rs/app-server/src/app_server_tracing.rs`:
- Around line 74-80: The span currently hardcodes otel.name and rpc.method to
"in_process", which collapses all requests; change the info_span! call that
creates span (the span variable via info_span!) to use the concrete request
method string instead: extract or compute a method name from the incoming
request (e.g., add/call a small helper like request_method(req) or match on the
request enum/variant to return a &str) and pass that value into otel.name and
rpc.method fields of info_span! so each span is labeled by the real RPC method.

In `@codex-rs/app-server/src/bespoke_event_handling.rs`:
- Around line 243-262: Add focused tests that cover the new event branches
(EventMsg::Warning, EventMsg::BackgroundEvent and the other newly added branches
like elicitation/undo/MCP-startup/shutdown-complete) by exercising the code
paths in bespoke_event_handling.rs: construct the appropriate EventMsg variants
and assert that outgoing.send_server_notification is awaited and receives the
expected ServerNotification variants (e.g., ServerNotification::Warning with
WarningNotification, ServerNotification::BackgroundEvent with
BackgroundEventNotification). Use the existing test harness/mocks for outgoing
(or implement a simple mock/spy for send_server_notification) to capture sent
notifications, and add similar tests for the other branches mentioned (lines
~641-678 and ~1612-1707) to ensure each branch serializes the correct
notification type and message. Ensure each test is small, deterministic, and
asserts the notification payload matches the event.message.

In `@codex-rs/app-server/src/codex_message_processor.rs`:
- Around line 2739-3367: Many handlers (thread_undo, thread_shutdown,
memory_drop, memory_update, thread_shell_command_run, history_add) duplicate the
pattern of calling load_thread, sending load errors, submitting an Op on the
thread, and sending a response or internal error—extract a shared async helper
(e.g., with_thread_submit) that takes (&self, request_id: ConnectionRequestId,
thread_id: String, F) where F is an async closure that receives the loaded
thread and returns Result<ResponsePayload, E> (or directly invokes
thread.submit(Op::...)). Implement the helper to call
self.load_thread(&thread_id).await and on Err call self.outgoing.send_error, on
Ok run the closure, map Ok to self.outgoing.send_response(request_id,
payload).await and map Err to self.send_internal_error(request_id,
format!(...)). Update the listed functions (thread_undo, thread_shutdown,
memory_drop, memory_update, thread_shell_command_run, history_add) to call
with_thread_submit and pass a closure that performs the thread.submit(Op::...)
and returns the appropriate response payload (or turn_id for shell command) to
eliminate duplication while preserving existing
send_invalid_request_error/send_internal_error behavior.

In `@codex-rs/app-server/src/thread_state.rs`:
- Around line 45-55: The enum ThreadListenerCommand and its variants
(SendThreadResumeResponse, ResolveServerRequest) are using line comments; change
them to doc comments so rustdoc picks them up and documentation style matches
other types like TurnSummary and ThreadState—replace the leading // with /// on
the enum and each variant, and include brief descriptions for
Box<PendingThreadResumeRequest>, RequestId and the completion_tx
oneshot::Sender<()> field to explain their roles.

In `@codex-rs/README.md`:
- Around line 56-58: Update the README sentence that mentions how codex exec and
the interactive TUI run through codex-app-server: change the phrase "in process"
to the hyphenated form "in-process" to match project terminology and improve
consistency; the sentence references "codex exec", "interactive TUI",
"codex-app-server", and "codex-app-server-client".

In `@codex-rs/tui/src/chatwidget.rs`:
- Around line 7698-7700: op_sender() exposes a raw UnboundedSender<Op> allowing
ops (e.g., Op::Shutdown) to bypass submit_op's logging and review task state
setup; make op_sender private or replace it with a controlled wrapper that
routes all sends through submit_op (or a new submit_op_wrapper) so that
log_outbound_op and the review-task state setup in submit_op/submit_op_to_thread
always run; update callers that currently call op_sender().send(...) (including
request_shutdown_for_inactive_threads) to use submit_op/submit_op_wrapper and
ensure submit_op_to_thread still explicitly calls log_outbound_op for non-active
threads.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b8f305ca-3e0d-4768-9417-ccca81d22d8a

📥 Commits

Reviewing files that changed from the base of the PR and between 52521a5 and d6b8906.

⛔ Files ignored due to path filters (1)
  • codex-rs/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (118)
  • codex-rs/Cargo.toml
  • codex-rs/README.md
  • codex-rs/app-server-client/Cargo.toml
  • codex-rs/app-server-client/README.md
  • codex-rs/app-server-client/src/lib.rs
  • codex-rs/app-server-protocol/schema/json/ClientRequest.json
  • codex-rs/app-server-protocol/schema/json/McpElicitationRequestParams.json
  • codex-rs/app-server-protocol/schema/json/McpElicitationRequestResponse.json
  • codex-rs/app-server-protocol/schema/json/ServerNotification.json
  • codex-rs/app-server-protocol/schema/json/ServerRequest.json
  • codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
  • codex-rs/app-server-protocol/schema/json/v2/BackgroundEventNotification.json
  • codex-rs/app-server-protocol/schema/json/v2/HistoryAddParams.json
  • codex-rs/app-server-protocol/schema/json/v2/HistoryAddResponse.json
  • codex-rs/app-server-protocol/schema/json/v2/HistoryEntryReadParams.json
  • codex-rs/app-server-protocol/schema/json/v2/HistoryEntryReadResponse.json
  • codex-rs/app-server-protocol/schema/json/v2/McpServerStartupCompletedNotification.json
  • codex-rs/app-server-protocol/schema/json/v2/McpServerStartupUpdatedNotification.json
  • codex-rs/app-server-protocol/schema/json/v2/McpToolsListParams.json
  • codex-rs/app-server-protocol/schema/json/v2/McpToolsListResponse.json
  • codex-rs/app-server-protocol/schema/json/v2/MemoryDropParams.json
  • codex-rs/app-server-protocol/schema/json/v2/MemoryDropResponse.json
  • codex-rs/app-server-protocol/schema/json/v2/MemoryUpdateParams.json
  • codex-rs/app-server-protocol/schema/json/v2/MemoryUpdateResponse.json
  • codex-rs/app-server-protocol/schema/json/v2/PromptCustomListParams.json
  • codex-rs/app-server-protocol/schema/json/v2/PromptCustomListResponse.json
  • codex-rs/app-server-protocol/schema/json/v2/SkillsUpdatedNotification.json
  • codex-rs/app-server-protocol/schema/json/v2/StreamErrorNotification.json
  • codex-rs/app-server-protocol/schema/json/v2/ThreadConfigReloadParams.json
  • codex-rs/app-server-protocol/schema/json/v2/ThreadConfigReloadResponse.json
  • codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json
  • codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json
  • codex-rs/app-server-protocol/schema/json/v2/ThreadShellCommandRunParams.json
  • codex-rs/app-server-protocol/schema/json/v2/ThreadShellCommandRunResponse.json
  • codex-rs/app-server-protocol/schema/json/v2/ThreadShutdownCompletedNotification.json
  • codex-rs/app-server-protocol/schema/json/v2/ThreadShutdownParams.json
  • codex-rs/app-server-protocol/schema/json/v2/ThreadShutdownResponse.json
  • codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json
  • codex-rs/app-server-protocol/schema/json/v2/ThreadTurnContextSetParams.json
  • codex-rs/app-server-protocol/schema/json/v2/ThreadTurnContextSetResponse.json
  • codex-rs/app-server-protocol/schema/json/v2/ThreadUndoCompletedNotification.json
  • codex-rs/app-server-protocol/schema/json/v2/ThreadUndoParams.json
  • codex-rs/app-server-protocol/schema/json/v2/ThreadUndoResponse.json
  • codex-rs/app-server-protocol/schema/json/v2/ThreadUndoStartedNotification.json
  • codex-rs/app-server-protocol/schema/json/v2/WarningNotification.json
  • codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts
  • codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts
  • codex-rs/app-server-protocol/schema/typescript/ServerRequest.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/BackgroundEventNotification.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/HistoryAddParams.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/HistoryAddResponse.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/HistoryEntry.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/HistoryEntryReadParams.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/HistoryEntryReadResponse.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/McpElicitationDecision.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/McpElicitationRequestParams.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/McpElicitationRequestResponse.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/McpServerStartupCompletedNotification.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/McpServerStartupFailure.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/McpServerStartupStatus.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/McpServerStartupUpdatedNotification.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/McpToolsListParams.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/McpToolsListResponse.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/MemoryDropParams.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/MemoryDropResponse.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/MemoryUpdateParams.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/MemoryUpdateResponse.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/PromptCustomListParams.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/PromptCustomListResponse.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/SessionNetworkProxyRuntime.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/SkillsUpdatedNotification.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/StreamErrorNotification.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/ThreadConfigReloadParams.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/ThreadConfigReloadResponse.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/ThreadContextSnapshot.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/ThreadShellCommandRunParams.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/ThreadShellCommandRunResponse.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/ThreadShutdownCompletedNotification.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/ThreadShutdownParams.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/ThreadShutdownResponse.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/ThreadTurnContextSetParams.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/ThreadTurnContextSetResponse.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/ThreadUndoCompletedNotification.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/ThreadUndoParams.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/ThreadUndoResponse.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/ThreadUndoStartedNotification.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/WarningNotification.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/index.ts
  • codex-rs/app-server-protocol/src/protocol/common.rs
  • codex-rs/app-server-protocol/src/protocol/v2.rs
  • codex-rs/app-server/README.md
  • codex-rs/app-server/src/app_server_tracing.rs
  • codex-rs/app-server/src/bespoke_event_handling.rs
  • codex-rs/app-server/src/codex_message_processor.rs
  • codex-rs/app-server/src/in_process.rs
  • codex-rs/app-server/src/lib.rs
  • codex-rs/app-server/src/message_processor.rs
  • codex-rs/app-server/src/outgoing_message.rs
  • codex-rs/app-server/src/thread_state.rs
  • codex-rs/app-server/src/transport.rs
  • codex-rs/core/src/lib.rs
  • codex-rs/exec/Cargo.toml
  • codex-rs/exec/src/lib.rs
  • codex-rs/tui/Cargo.toml
  • codex-rs/tui/src/agent_command.rs
  • codex-rs/tui/src/app.rs
  • codex-rs/tui/src/app_event.rs
  • codex-rs/tui/src/app_event_sender.rs
  • codex-rs/tui/src/bottom_pane/approval_overlay.rs
  • codex-rs/tui/src/bottom_pane/mod.rs
  • codex-rs/tui/src/chatwidget.rs
  • codex-rs/tui/src/chatwidget/agent.rs
  • codex-rs/tui/src/chatwidget/tests.rs
  • codex-rs/tui/src/lib.rs
  • codex-rs/tui/src/status_indicator_widget.rs


// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export type HistoryEntryReadParams = { threadId: string, logId: bigint, offset: number, };
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Step 1: Confirm bigint in generated params type ==="
if [ -f "codex-rs/app-server-protocol/schema/typescript/v2/HistoryEntryReadParams.ts" ]; then
    cat codex-rs/app-server-protocol/schema/typescript/v2/HistoryEntryReadParams.ts
else
    echo "File not found at specified path, searching..."
    find . -name "HistoryEntryReadParams.ts" 2>/dev/null | head -5
fi

Repository: fcoury/codex

Length of output: 355


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Step 2: Locate JSON serialization call sites ==="
rg -nP 'JSON\.stringify' --type typescript --type javascript codex-rs/app-server-protocol -A 2 -B 2 | head -100

Repository: fcoury/codex

Length of output: 160


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Step 3: Search for BigInt handling mechanisms ==="
rg -nP 'BigInt|replacer|reviver' --type typescript --type javascript codex-rs/app-server-protocol -B 2 -A 2 | head -100

Repository: fcoury/codex

Length of output: 161


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Step 4: Find where HistoryEntryReadParams is used ==="
rg -nP 'HistoryEntryReadParams' --type typescript --type javascript codex-rs/app-server-protocol -B 2 -A 3 | head -150

Repository: fcoury/codex

Length of output: 165


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Step 2: Locate JSON serialization call sites ==="
rg -nP 'JSON\.stringify' codex-rs/app-server-protocol -A 2 -B 2 | head -120

Repository: fcoury/codex

Length of output: 108


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Step 3: Search for BigInt handling ==="
rg -i 'BigInt|replacer|reviver' codex-rs/app-server-protocol -B 2 -A 2 | head -120

Repository: fcoury/codex

Length of output: 7363


🏁 Script executed:

#!/bin/bash
#!/bin/bash
set -euo pipefail

echo "=== Step 4: Find usages of HistoryEntryReadParams ==="
rg 'HistoryEntryReadParams' codex-rs/app-server-protocol -B 2 -A 5 | head -150

Repository: fcoury/codex

Length of output: 15712


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Step 5: Check for WebSocket or stdio transport mechanisms ==="
rg -nP 'send|transport|serialize' codex-rs/app-server-protocol -A 1 -B 1 --max-count=50

Repository: fcoury/codex

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find TypeScript/JavaScript client code that uses HistoryEntryReadParams
echo "=== Finding TypeScript client code using HistoryEntryReadParams ==="
find . -name "*.ts" -o -name "*.js" | xargs grep -l "HistoryEntryReadParams" 2>/dev/null | grep -v schema | grep -v node_modules | head -20

Repository: fcoury/codex

Length of output: 123


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check how requests are actually constructed and sent
echo "=== Check for request construction/transmission code ==="
rg -i "history.*entry.*read|sendRequest|fetch|JSON.stringify" --max-count=30 --type-list | head -5
find . -name "*.ts" -o -name "*.js" | xargs grep -l "history/entry/read" 2>/dev/null | grep -v schema | head -10

Repository: fcoury/codex

Length of output: 519


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look at the Rust source definition more closely
echo "=== Examining Rust source definition of HistoryEntryReadParams ==="
cat codex-rs/app-server-protocol/src/protocol/v2.rs | grep -A 10 "pub struct HistoryEntryReadParams"

Repository: fcoury/codex

Length of output: 426


🏁 Script executed:

#!/bin/bash
set -euso pipefail

# Check ts-rs configuration and how it handles u64
echo "=== Check how ts-rs handles u64 and if there's custom handling ==="
rg "ts-rs|u64.*ts|bigint" codex-rs/app-server-protocol/src/protocol/v2.rs | head -30

Repository: fcoury/codex

Length of output: 353


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Looking at JSON schema for HistoryEntryReadParams ==="
cat codex-rs/app-server-protocol/schema/json/v2/HistoryEntryReadParams.json

Repository: fcoury/codex

Length of output: 549


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check if bigint is ever used in generated TS files ==="
grep -r "bigint" codex-rs/app-server-protocol/schema/typescript/v2/ --include="*.ts" | head -20

Repository: fcoury/codex

Length of output: 1633


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Look for any serialization adapter or custom handlers ==="
find codex-rs -name "*.rs" | xargs grep -l "serialize.*u64\|u64.*serialize" 2>/dev/null | head -5

Repository: fcoury/codex

Length of output: 156


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check README or documentation about wire format ==="
if [ -f "README.md" ]; then head -100 README.md; fi
find . -name "*PROTOCOL*" -o -name "*protocol*readme*" 2>/dev/null | head -5

Repository: fcoury/codex

Length of output: 2736


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Critical: Check if JSON schema is actually used and if uint64 is compatible ==="
cat codex-rs/app-server-protocol/schema/json/v2/HistoryEntryReadParams.json | jq '.properties.logId'

Repository: fcoury/codex

Length of output: 201


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for ANY TypeScript/JavaScript client code in repo ==="
find . -path ./node_modules -prune -o -type f \( -name "*.ts" -o -name "*.js" \) -print | grep -v schema | grep -v node_modules | head -20

Repository: fcoury/codex

Length of output: 890


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check if there are actual client implementations ==="
find . -name "*client*" -type f | grep -v node_modules | grep -v ".git" | head -20

Repository: fcoury/codex

Length of output: 885


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Look at how the protocol documentation describes wire format ==="
find codex-rs/app-server-protocol -name "*.md" -o -name "*.txt" | xargs cat 2>/dev/null | head -100

Repository: fcoury/codex

Length of output: 124


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check ts-rs documentation or comments about u64 handling ==="
grep -r "ts-rs\|u64" codex-rs/app-server-protocol/src --include="*.rs" -B 2 -A 2 | head -60

Repository: fcoury/codex

Length of output: 3627


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check ALL u64 fields in v2.rs and their ts-rs annotations ==="
grep -B 2 "pub.*:\s*u64" codex-rs/app-server-protocol/src/protocol/v2.rs | grep -E "(pub struct|pub.*:\s*u64|#\[ts)" | head -80

Repository: fcoury/codex

Length of output: 389


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check if SDK typescript imports from generated types ==="
grep -r "from.*protocol\|import.*HistoryEntryReadParams" sdk/typescript/src --include="*.ts" 2>/dev/null

Repository: fcoury/codex

Length of output: 235


🏁 Script executed:

#!/antml/bash
set -euo pipefail

echo "=== Get full context of HistoryEntryReadParams struct in Rust ==="
sed -n '545,560p' codex-rs/app-server-protocol/src/protocol/v2.rs

Repository: fcoury/codex

Length of output: 577


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Find exact line number and context of HistoryEntryReadParams ==="
grep -n "struct HistoryEntryReadParams" codex-rs/app-server-protocol/src/protocol/v2.rs

Repository: fcoury/codex

Length of output: 165


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Get full HistoryEntryReadParams struct with decorators ==="
awk '/pub struct HistoryEntryReadParams/,/^}/' codex-rs/app-server-protocol/src/protocol/v2.rs | head -20

Repository: fcoury/codex

Length of output: 227


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Compare HistoryEntryReadParams with SessionConfiguredEvent which HAS the annotation ==="
echo "--- SessionConfiguredEvent (HAS #[ts(type='number')] on history_log_id) ---"
awk '/pub struct SessionConfiguredEvent/,/^}/' codex-rs/app-server-protocol/src/protocol/v2.rs | head -30
echo ""
echo "--- HistoryEntryReadParams (NO annotation) ---"
awk '/pub struct HistoryEntryReadParams/,/^}/' codex-rs/app-server-protocol/src/protocol/v2.rs | head -20

Repository: fcoury/codex

Length of output: 380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check if there are other u64 fields without the annotation ==="
rg -A 1 "pub.*:\s*u64[,\s]" codex-rs/app-server-protocol/src/protocol/v2.rs | grep -B 1 "pub.*:\s*u64" | grep -v "ts\(type" | head -40

Repository: fcoury/codex

Length of output: 163


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Get lines 2485-2500 to see all decorators for HistoryEntryReadParams ==="
sed -n '2485,2500p' codex-rs/app-server-protocol/src/protocol/v2.rs

Repository: fcoury/codex

Length of output: 597


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Find ALL u64 fields lacking #[ts(...)] annotation in v2.rs ==="
grep -n "pub.*u64" codex-rs/app-server-protocol/src/protocol/v2.rs | grep -v "#"

Repository: fcoury/codex

Length of output: 272


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check lines around HistoryEntryReadParams and surrounding types ==="
sed -n '2480,2520p' codex-rs/app-server-protocol/src/protocol/v2.rs

Repository: fcoury/codex

Length of output: 1248


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Look for ts-rs crate version and BigInt documentation ==="
grep -r "ts-rs\|serde_json" codex-rs/app-server-protocol/Cargo.toml | head -10

Repository: fcoury/codex

Length of output: 180


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check lines 1890-1910 to see SessionConfiguredEvent with the annotation ==="
sed -n '1890,1910p' codex-rs/app-server-protocol/src/protocol/v2.rs

Repository: fcoury/codex

Length of output: 799


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Verify if HistoryEntryReadParams is actually part of wire protocol ==="
grep -n "HistoryEntryRead" codex-rs/app-server-protocol/src/protocol/common.rs | head -20

Repository: fcoury/codex

Length of output: 396


Missing #[ts(type = "number")] annotation on u64 fields breaks TypeScript serialization.

logId: bigint and related u64 fields in HistoryEntryReadParams and HistoryEntry are generated without the #[ts(type = "number")] annotation present on other similar u64 fields (e.g., SessionConfiguredEvent.history_log_id). This causes bigint type generation in TypeScript, which cannot serialize with vanilla JSON.stringify() (throws TypeError). Wire transport requires either:

  • Add #[ts(type = "number")] to Rust field (matches codebase pattern for u64 fields), or
  • Change TypeScript type to string and convert at boundaries
Affected fields in v2.rs
  • Line 2493: HistoryEntryReadParams.log_id: u64 (missing annotation)
  • Line 2502: HistoryEntry.ts: u64 (missing annotation)

Compare with line 1899: SessionConfiguredEvent.history_log_id: u64 which correctly has #[ts(type = "number")]

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server-protocol/schema/typescript/v2/HistoryEntryReadParams.ts`
at line 5, HistoryEntryReadParams and the HistoryEntry u64 fields are generating
TypeScript bigint types which break JSON serialization; update the Rust
definitions for the affected fields (e.g., the log_id field in the
HistoryEntryReadParams struct and the corresponding u64 in HistoryEntry) to
include the #[ts(type = "number")] annotation so they generate number types in
TypeScript consistent with other fields like
SessionConfiguredEvent.history_log_id, or alternatively change the generated
TypeScript types to string and add conversion at the serialization
boundary—prefer adding #[ts(type = "number")] on the Rust u64 fields to match
project conventions.

import type { Thread } from "./Thread";

export type ThreadForkResponse = { thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: string, approvalPolicy: AskForApproval, sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null, };
export type ThreadForkResponse = { thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: string, approvalPolicy: AskForApproval, sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null, historyLogId: number, historyEntryCount: number, forkedFromThreadId: string | null, networkProxy: SessionNetworkProxyRuntime | null, };
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check ThreadForkResponse and related field definitions across the codebase
echo "=== Searching for ThreadForkResponse in Rust ==="
rg -n "ThreadForkResponse|historyLogId|historyEntryCount|history_log_id|history_entry_count" codex-rs -g '*.rs' | head -50

echo -e "\n=== Searching for definitions in schema/JSON ==="
rg -n "ThreadForkResponse|historyLogId|historyEntryCount|history_log_id|history_entry_count" codex-rs -g '*.json' -g '*.yaml' -g '*.yml' | head -50

echo -e "\n=== Searching for TS type definitions ==="
rg -n "ThreadForkResponse|historyLogId|historyEntryCount" codex-rs -g '*.ts' | head -50

Repository: fcoury/codex

Length of output: 10348


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check ThreadForkResponse.json schema definition
echo "=== ThreadForkResponse.json historyLogId/historyEntryCount definition ==="
jq '.properties | {historyLogId, historyEntryCount}' codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json

echo -e "\n=== SessionConfiguredNotification.json historyLogId definition (for comparison) ==="
jq '.properties | {historyLogId, historyEntryCount}' codex-rs/app-server-protocol/schema/json/v2/SessionConfiguredNotification.json

# Check if there's a proto or rust source for ThreadForkResponse
echo -e "\n=== Searching for Rust ThreadForkResponse definition ==="
rg -A 10 "^pub struct ThreadForkResponse|^pub enum ThreadForkResponse" codex-rs -g '*.rs' | head -30

Repository: fcoury/codex

Length of output: 552


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find SessionConfiguredNotification files
echo "=== Finding SessionConfiguredNotification files ==="
fd -t f "SessionConfiguredNotification" codex-rs

echo -e "\n=== Check SessionConfiguredNotification.ts ==="
cat -n codex-rs/app-server-protocol/schema/typescript/v2/SessionConfiguredNotification.ts

# Check for any u64 → bigint codegen rules
echo -e "\n=== Search for codegen config or transformer rules ==="
fd -t f "Codegen\|codegen\|transformer\|tsconfig" codex-rs/app-server-protocol --max-depth 3 | head -20

Repository: fcoury/codex

Length of output: 367


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check SessionConfiguredNotification.ts (no v2 prefix)
echo "=== SessionConfiguredNotification.ts (root typescript dir) ==="
cat -n codex-rs/app-server-protocol/schema/typescript/SessionConfiguredNotification.ts

# Also check if there's a v2 version
echo -e "\n=== Checking for v2 version ==="
find codex-rs/app-server-protocol/schema/typescript -name "*SessionConfiguredNotification*"

# Look for codegen/transformer configuration
echo -e "\n=== Searching for codegen configuration ==="
fd -t f "\.ts$" codex-rs/app-server-protocol --max-depth 1 | head -20
ls -la codex-rs/app-server-protocol/ | grep -i build\|codegen\|gen

Repository: fcoury/codex

Length of output: 958


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if v2/ThreadForkResponse.ts is generated or hand-written
echo "=== ThreadForkResponse.ts header ==="
head -5 codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts

# Search for Rust ThreadForkResponse struct
echo -e "\n=== Rust ThreadForkResponse struct definition ==="
rg -B 2 -A 15 "pub struct ThreadForkResponse" codex-rs -g '*.rs'

# Check for u64 in proto or Rust serde definitions
echo -e "\n=== Searching for history_log_id and history_entry_count in Rust structs ==="
rg -B 3 -A 3 "history_log_id|history_entry_count" codex-rs -g '*.rs' | grep -A 3 "struct\|u64\|usize" | head -40

Repository: fcoury/codex

Length of output: 5167


Remove #[ts(type = "number")] overrides for 64-bit integer fields in Rust source.

history_log_id is u64 in Rust but forced to TypeScript number via an explicit #[ts(type = "number")] attribute override in codex-rs/app-server-protocol/src/protocol/v2.rs. This loses precision above 2^53 - 1. Remove the override to allow ts-rs to generate bigint, consistent with how SessionConfiguredNotification handles the same field elsewhere in the codebase.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts` at
line 11, The generated TypeScript ThreadForkResponse forces the 64-bit Rust
field history_log_id into a JS number (historyLogId) losing precision; edit the
Rust declaration in protocol v2.rs where history_log_id is annotated with
#[ts(type = "number")] and remove that override so ts-rs will emit bigint for
u64, matching how SessionConfiguredNotification is handled, then regenerate the
TypeScript types.

Comment on lines +2497 to +2504
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct HistoryEntry {
pub conversation_id: String,
pub ts: u64,
pub text: String,
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify Rust source currently lacks a TS type override for HistoryEntry.ts
rg -n 'pub struct HistoryEntry|pub ts: u64|#\[ts\(type = "number"\)\]' codex-rs/app-server-protocol/src/protocol/v2.rs

# Verify generated TS currently uses bigint for ts
rg -n 'export type HistoryEntry|ts: bigint|ts: number' codex-rs/app-server-protocol/schema/typescript/v2/HistoryEntry.ts

Repository: fcoury/codex

Length of output: 788


HistoryEntry.ts emits bigint for JSON transport—should emit number.

The ts field at line 2502 is u64 without a ts-rs override, generating ts: bigint in the TypeScript definition. JSON cannot safely transport bigint, creating a type mismatch for clients. Other u64 fields throughout the file correctly use #[ts(type = "number")], so this is an inconsistency.

Fix
pub struct HistoryEntry {
    pub conversation_id: String,
+   #[ts(type = "number")]
    pub ts: u64,
    pub text: String,
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct HistoryEntry {
pub conversation_id: String,
pub ts: u64,
pub text: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct HistoryEntry {
pub conversation_id: String,
#[ts(type = "number")]
pub ts: u64,
pub text: String,
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server-protocol/src/protocol/v2.rs` around lines 2497 - 2504,
The HistoryEntry struct's ts field is u64 but missing the ts-rs override,
causing TypeScript to emit bigint; add the ts-rs attribute to force a number:
annotate the ts field in struct HistoryEntry with #[ts(type = "number")]
(matching other u64 fields in this file) so the generated HistoryEntry.ts uses
number for ts and aligns with JSON transport.

Comment on lines +3380 to +3408
let (_, _thread) = match self.load_thread(&thread_id).await {
Ok(v) => v,
Err(error) => {
self.outgoing.send_error(request_id, error).await;
return;
}
};

let config = Arc::clone(&self.config);
let entry = match tokio::task::spawn_blocking(move || {
codex_core::lookup_global_history_entry(log_id, offset, config.as_ref())
})
.await
{
Ok(entry) => entry,
Err(err) => {
self.send_internal_error(
request_id,
format!("failed to lookup history entry: {err}"),
)
.await;
return;
}
};

let response = HistoryEntryReadResponse {
entry: entry.map(Into::into),
};
self.outgoing.send_response(request_id, response).await;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Scope history/entry/read to the requested thread before global lookup.

Line 3380 validates the thread exists, but Line 3389 reads by caller-provided log_id and offset globally without verifying that log_id belongs to that thread. This allows cross-thread history reads if a log_id is known.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/src/codex_message_processor.rs` around lines 3380 - 3408,
The code calls load_thread(&thread_id) but then performs a global lookup via
codex_core::lookup_global_history_entry(log_id, offset, ...) allowing
cross-thread reads; change to use a thread-scoped lookup or validate the
looked-up entry belongs to the requested thread: either call a thread-aware API
(e.g., lookup_thread_history_entry or similar) passing thread_id along with
log_id/offset, or after codex_core::lookup_global_history_entry verify the
returned entry's thread identifier matches thread_id and return an error (use
self.send_internal_error or self.outgoing.send_error) if it does not; update
references around load_thread, lookup_global_history_entry, and the
HistoryEntryReadResponse construction to ensure only entries from the requested
thread are returned.

// Record inbound events for high-fidelity session replay.
// Avoid double-logging Ops; those are logged at the point of submission.
if !matches!(event, AppEvent::CodexOp(_)) {
if !matches!(event, AppEvent::CodexOp(_) | AppEvent::AgentCommand(_)) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find app_event.rs
APP_EVENT_FILE="$(fd '^app_event\.rs$' 2>/dev/null | head -n 1)"

if [[ -n "${APP_EVENT_FILE}" ]]; then
  echo "=== AppEvent definition and Copy trait ==="
  cat -n "${APP_EVENT_FILE}"
else
  echo "Could not find app_event.rs, searching by pattern..."
  rg -l "enum\s+AppEvent" 2>/dev/null | head -n 1
fi

echo
echo "=== app_event_sender.rs context around line 21 ==="
wc -l codex-rs/tui/src/app_event_sender.rs
head -n 30 codex-rs/tui/src/app_event_sender.rs | cat -n

Repository: fcoury/codex

Length of output: 20633


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if AppEvent implements Copy
echo "=== Searching for Copy impl for AppEvent ==="
rg "impl\s+Copy\s+for\s+AppEvent|#\[derive\([^)]*Copy" codex-rs/tui/src/

# Check the full enum derive on AppEvent
echo
echo "=== Full AppEvent enum derive clause ==="
rg -B2 "pub\(crate\)\s+enum\s+AppEvent" codex-rs/tui/src/app_event.rs | head -n 5

# Check the full app_event_sender.rs file to see if it compiles
echo
echo "=== Full app_event_sender.rs for compilation context ==="
cat codex-rs/tui/src/app_event_sender.rs

Repository: fcoury/codex

Length of output: 7328


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if this file has any tests or if it's actively used
echo "=== Checking for tests or usage of AppEventSender::send ==="
rg -l "AppEventSender|app_event_sender" codex-rs/tui/src/ | head -10

# Look for any compiler warnings or errors in the build
echo
echo "=== Checking for any related compiler artifacts or error docs ==="
find . -name "*.rs" -path "*/codex-rs/tui/src/*" | wc -l

# Double-check the exact line being reviewed
echo
echo "=== Exact context around line 21 with line numbers ==="
sed -n '15,27p' codex-rs/tui/src/app_event_sender.rs | cat -n

Repository: fcoury/codex

Length of output: 1299


Use a reference in matches! to avoid moving event before subsequent uses.

AppEvent does not implement Copy, so the matches! macro on line 21 moves event. Since event is used again on lines 22 and 24, pass a reference to matches! instead:

Fix
-        if !matches!(event, AppEvent::CodexOp(_) | AppEvent::AgentCommand(_)) {
+        if !matches!(&event, AppEvent::CodexOp(_) | AppEvent::AgentCommand(_)) {
             session_log::log_inbound_app_event(&event);
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if !matches!(event, AppEvent::CodexOp(_) | AppEvent::AgentCommand(_)) {
if !matches!(&event, AppEvent::CodexOp(_) | AppEvent::AgentCommand(_)) {
session_log::log_inbound_app_event(&event);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/tui/src/app_event_sender.rs` at line 21, The matches! call is moving
`event` (AppEvent is not Copy) which breaks later uses; change the pattern to
match a reference instead (use `&event` in the matches! invocation) so it
borrows rather than moves, keeping subsequent uses of `event` valid and
preserving the existing patterns `AppEvent::CodexOp(_) |
AppEvent::AgentCommand(_)`.

Comment on lines 1423 to 1425
self.thread_attach_retry_state.clear();
self.suppress_shutdown_complete = false;
self.active_thread_id = None;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Scheduled attach-retry tasks are not canceled on thread-state reset

reset_thread_event_state() clears retry metadata, but previously spawned retry tasks still fire and can enqueue stale AppEvent::ThreadCreated later. That can resurrect old attach flows after session reset.

Suggested mitigation
 // App fields
- thread_attach_retry_state: HashMap<ThreadId, ThreadAttachRetryState>,
+ thread_attach_retry_state: HashMap<ThreadId, ThreadAttachRetryState>,
+ thread_attach_retry_tasks: HashMap<ThreadId, JoinHandle<()>>,

 fn clear_thread_attach_retry_state(&mut self, thread_id: ThreadId) {
     self.thread_attach_retry_state.remove(&thread_id);
+    if let Some(handle) = self.thread_attach_retry_tasks.remove(&thread_id) {
+        handle.abort();
+    }
 }

 fn reset_thread_event_state(&mut self) {
     self.abort_all_thread_event_listeners();
     self.thread_event_channels.clear();
     self.agent_picker_threads.clear();
     self.thread_attach_retry_state.clear();
+    for (_, handle) in self.thread_attach_retry_tasks.drain() {
+        handle.abort();
+    }
     self.suppress_shutdown_complete = false;
     ...
 }

- tokio::spawn(async move {
+ let handle = tokio::spawn(async move {
     tokio::time::sleep(delay).await;
     app_event_tx.send(AppEvent::ThreadCreated(thread_id));
 });
+ if let Some(previous) = self.thread_attach_retry_tasks.insert(thread_id, handle) {
+     previous.abort();
+ }

Also applies to: 3399-3403

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/tui/src/app.rs` around lines 1423 - 1425, reset_thread_event_state()
currently clears thread_attach_retry_state but does not cancel previously
spawned retry tasks, so those tasks can still enqueue stale
AppEvent::ThreadCreated after a reset; update reset_thread_event_state to first
cancel/abort any outstanding retry tasks (e.g., by storing and signalling a
CancellationToken/AbortHandle or keeping JoinHandles and aborting them)
referenced from thread_attach_retry_state before calling clear(), and harden the
retry task callback that enqueues AppEvent::ThreadCreated to verify the current
attach context/state (e.g., check active_thread_id or a unique attach attempt
id) before emitting the event; apply the same cancellation/validation change to
the other reset site around the code referenced at 3399-3403.

Comment on lines +3373 to +3375
if let Some(last_scheduled_at) = state.last_scheduled_at
&& now.saturating_duration_since(last_scheduled_at) < THREAD_ATTACH_RETRY_DELAY
{
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Retry de-duplication window is too short for later backoff attempts

The duplicate-scheduling guard always uses a fixed 250ms window, but retries are scheduled at 500ms/750ms/... on later attempts. That allows overlapping retries for the same thread and can burn through attempts faster than intended.

Suggested fix
 fn maybe_schedule_thread_attach_retry(&mut self, thread_id: ThreadId, err: &str) {
     let now = Instant::now();
     let state = self.thread_attach_retry_state.entry(thread_id).or_default();
-    if let Some(last_scheduled_at) = state.last_scheduled_at
-        && now.saturating_duration_since(last_scheduled_at) < THREAD_ATTACH_RETRY_DELAY
-    {
-        tracing::debug!(
-            "attach retry already pending for thread {thread_id}; suppressing duplicate scheduling"
-        );
-        return;
-    }
+    if let Some(last_scheduled_at) = state.last_scheduled_at {
+        let pending_delay =
+            THREAD_ATTACH_RETRY_DELAY.saturating_mul(u32::from(state.attempts.max(1)));
+        if now.saturating_duration_since(last_scheduled_at) < pending_delay {
+            tracing::debug!(
+                "attach retry already pending for thread {thread_id}; suppressing duplicate scheduling"
+            );
+            return;
+        }
+    }

Also applies to: 3390-3397

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/tui/src/app.rs` around lines 3373 - 3375, The duplicate-scheduling
guard currently compares now.saturating_duration_since(state.last_scheduled_at)
against a fixed THREAD_ATTACH_RETRY_DELAY, which allows later backoff retries
(500ms/750ms/...) to overlap; update both checks (the one using
state.last_scheduled_at around the shown block and the similar one at the other
occurrence) to compare against a dynamic delay derived from the thread's current
retry/backoff state instead of the fixed constant—e.g., compute a delay =
THREAD_ATTACH_RETRY_DELAY * (state.attach_attempts.saturating_add(1)) or use an
existing per-thread next_retry_delay field (state.next_retry_delay) and replace
the fixed THREAD_ATTACH_RETRY_DELAY in the condition with that computed/field
value so retries are de-duplicated according to the actual backoff schedule.

Comment on lines +603 to +605
EventMsg::TurnComplete(_) | EventMsg::TurnAborted(_) => {
current_turn_id = None;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Clear pending server requests when a turn completes or aborts.

At Line 603 and Line 1727, current_turn_id is reset but pending_server_requests is not. If approvals are left pending when a turn ends, stale entries remain and can leak state across subsequent operations.

🔧 Proposed fix
-EventMsg::TurnComplete(_) | EventMsg::TurnAborted(_) => {
-    current_turn_id = None;
-}
+EventMsg::TurnComplete(_) | EventMsg::TurnAborted(_) => {
+    current_turn_id = None;
+    pending_server_requests.clear();
+}

Also applies to: 1727-1729

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/tui/src/chatwidget/agent.rs` around lines 603 - 605, When handling
EventMsg::TurnComplete(_) and EventMsg::TurnAborted(_) (the match arm that
currently sets current_turn_id = None), also clear the pending_server_requests
map so stale approval entries don't leak into subsequent turns; update the same
matching logic that touches current_turn_id (and the analogous spot near the
other TurnComplete/TurnAborted handler) to call pending_server_requests.clear()
or remove only entries associated with the finished turn id if you prefer more
targeted cleanup, ensuring both current_turn_id and pending_server_requests are
synchronized when a turn ends.

Comment on lines +992 to +1017
let Some(request_id) = pending_server_requests.exec_approvals.remove(&id) else {
send_warning_event(
app_event_tx,
format!("exec approval ignored because request id `{id}` was not pending"),
);
return;
};
let response = CommandExecutionRequestApprovalResponse {
decision: CommandExecutionApprovalDecision::from(decision),
};
let result = match serde_json::to_value(response) {
Ok(value) => value,
Err(err) => {
send_error_event(
app_event_tx,
format!("failed to encode exec approval: {err}"),
);
return;
}
};
if let Err(err) = client.resolve_server_request(request_id, result).await {
send_error_event(
app_event_tx,
format!("failed to submit exec approval: {err}"),
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t drop pending request IDs before resolve_server_request succeeds.

At Lines 992/1020/1063/1098/1139, the pending ID is removed before the async resolve call. If resolve fails, the mapping is lost and the user cannot retry the response submission.

🔧 Proposed fix pattern (apply to each response branch)
- let Some(request_id) = pending_server_requests.exec_approvals.remove(&id) else {
+ let Some(request_id) = pending_server_requests.exec_approvals.get(&id).cloned() else {
     send_warning_event(
         app_event_tx,
         format!("exec approval ignored because request id `{id}` was not pending"),
     );
     return;
 };
 ...
 if let Err(err) = client.resolve_server_request(request_id, result).await {
     send_error_event(
         app_event_tx,
         format!("failed to submit exec approval: {err}"),
     );
+    return;
 }
+pending_server_requests.exec_approvals.remove(&id);

Also applies to: 1020-1052, 1063-1095, 1098-1135, 1139-1177

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/tui/src/chatwidget/agent.rs` around lines 992 - 1017, The code
currently removes the pending ID from pending_server_requests.exec_approvals
before awaiting client.resolve_server_request, which loses the mapping if the
async call fails; instead, look up or clone the request_id without removing it
(e.g., use get(&id).cloned()), call client.resolve_server_request(request_id,
result).await, and only call pending_server_requests.exec_approvals.remove(&id)
after the resolve succeeds; keep the same send_warning_event/send_error_event
flows and ensure errors do not drop the pending entry so users can retry.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR embeds codex-app-server in-process for codex-tui and codex-exec, so both surfaces share the same app-server runtime/protocol mapping layer while avoiding JSON serialization on the in-process hot path (typed channels for requests/events).

Changes:

  • Introduces codex-app-server-client as a Send-safe in-process embedding facade (worker task, backpressure/lag signaling, typed request helpers, graceful shutdown).
  • Refactors app-server outgoing messages to carry typed core events (OutgoingMessage::CoreEvent) and serializes those to legacy codex/event/* JSON-RPC notifications only at transport boundaries.
  • Updates TUI codepaths/tests to route intent via AgentCommand / AppEvent::AgentCommand / SubmitThreadAgentCommand instead of constructing raw protocol Op directly; expands v2 protocol/schema (new RPCs + notifications including MCP elicitation).

Reviewed changes

Copilot reviewed 118 out of 119 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
codex-rs/tui/src/status_indicator_widget.rs Switches interrupt dispatch to AgentCommand::Interrupt.
codex-rs/tui/src/lib.rs Registers new agent_command module.
codex-rs/tui/src/chatwidget/tests.rs Updates widget construction and app-event expectations for agent command flow.
codex-rs/tui/src/chatwidget.rs Removes ThreadManager dependency in ctor path; exposes additional agent helpers; adds op_sender().
codex-rs/tui/src/bottom_pane/mod.rs Updates tests to assert interrupt is sent via AgentCommand.
codex-rs/tui/src/bottom_pane/approval_overlay.rs Routes approval/elicitation responses via SubmitThreadAgentCommand + AgentCommand variants.
codex-rs/tui/src/app_event_sender.rs Avoids replay double-logging for AgentCommand events (similar to CodexOp).
codex-rs/tui/src/app_event.rs Adds AgentCommand + SubmitThreadAgentCommand + ThreadCreated app events.
codex-rs/tui/src/agent_command.rs New intent-level command enum with lossless conversion into protocol Op.
codex-rs/tui/Cargo.toml Adds dependency on codex-app-server-client.
codex-rs/exec/Cargo.toml Adds dependencies on codex-app-server-client and codex-app-server-protocol.
codex-rs/core/src/lib.rs Adds lookup_global_history_entry convenience wrapper for global history lookup.
codex-rs/app-server/src/transport.rs Custom-serializes OutgoingMessage::CoreEvent to legacy JSON-RPC notifications; updates opt-out filtering.
codex-rs/app-server/src/thread_state.rs Adds module docs and expands struct documentation for thread/subscription bookkeeping.
codex-rs/app-server/src/outgoing_message.rs Introduces OutgoingMessage::CoreEvent and legacy_event_method(); renames sender helper accordingly.
codex-rs/app-server/src/message_processor.rs Adds typed request/notification processing and makes session source configurable (but contains a compile-blocking move bug; see comments).
codex-rs/app-server/src/lib.rs Exposes in_process module; threads session source through message processor args.
codex-rs/app-server/src/bespoke_event_handling.rs Adds v2 notifications (warning/background/streamError/undo/shutdown/MCP startup/skills) + MCP elicitation request flow.
codex-rs/app-server/src/app_server_tracing.rs Adds span builder for typed in-process requests (typed_request_span).
codex-rs/app-server/README.md Documents in-process runtime model and adds RPC alias guidance + MCP elicitation docs.
codex-rs/app-server-protocol/src/protocol/v2.rs Adds v2 request/response/notification types (undo/context/config/shutdown/memories/history/prompts/MCP tools + new notifications).
codex-rs/app-server-protocol/schema/typescript/v2/index.ts Re-exports newly generated v2 TS types.
codex-rs/app-server-protocol/schema/typescript/v2/WarningNotification.ts Generated TS type for warning notification.
codex-rs/app-server-protocol/schema/typescript/v2/ThreadUndoStartedNotification.ts Generated TS type for thread/undo/started notification.
codex-rs/app-server-protocol/schema/typescript/v2/ThreadUndoResponse.ts Generated TS type for thread/undo response.
codex-rs/app-server-protocol/schema/typescript/v2/ThreadUndoParams.ts Generated TS type for thread/undo params.
codex-rs/app-server-protocol/schema/typescript/v2/ThreadUndoCompletedNotification.ts Generated TS type for thread/undo/completed notification.
codex-rs/app-server-protocol/schema/typescript/v2/ThreadTurnContextSetResponse.ts Generated TS type for context update response.
codex-rs/app-server-protocol/schema/typescript/v2/ThreadTurnContextSetParams.ts Generated TS type for context update params.
codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts Generated TS type updated with enriched bootstrap fields.
codex-rs/app-server-protocol/schema/typescript/v2/ThreadShutdownResponse.ts Generated TS type for thread/shutdown response.
codex-rs/app-server-protocol/schema/typescript/v2/ThreadShutdownParams.ts Generated TS type for thread/shutdown params.
codex-rs/app-server-protocol/schema/typescript/v2/ThreadShutdownCompletedNotification.ts Generated TS type for shutdown completion notification.
codex-rs/app-server-protocol/schema/typescript/v2/ThreadShellCommandRunResponse.ts Generated TS type for shell command response.
codex-rs/app-server-protocol/schema/typescript/v2/ThreadShellCommandRunParams.ts Generated TS type for shell command params.
codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts Generated TS type updated with enriched bootstrap fields.
codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts Generated TS type updated with enriched bootstrap fields.
codex-rs/app-server-protocol/schema/typescript/v2/ThreadContextSnapshot.ts Generated TS type for thread context snapshot.
codex-rs/app-server-protocol/schema/typescript/v2/ThreadConfigReloadResponse.ts Generated TS type for config reload response.
codex-rs/app-server-protocol/schema/typescript/v2/ThreadConfigReloadParams.ts Generated TS type for config reload params.
codex-rs/app-server-protocol/schema/typescript/v2/StreamErrorNotification.ts Generated TS type for stream error notification.
codex-rs/app-server-protocol/schema/typescript/v2/SkillsUpdatedNotification.ts Generated TS type for skills update notification.
codex-rs/app-server-protocol/schema/typescript/v2/SessionNetworkProxyRuntime.ts Generated TS type for session proxy runtime bind addresses.
codex-rs/app-server-protocol/schema/typescript/v2/PromptCustomListResponse.ts Generated TS type for custom prompt listing response.
codex-rs/app-server-protocol/schema/typescript/v2/PromptCustomListParams.ts Generated TS type for custom prompt listing params.
codex-rs/app-server-protocol/schema/typescript/v2/MemoryUpdateResponse.ts Generated TS type for memory update response.
codex-rs/app-server-protocol/schema/typescript/v2/MemoryUpdateParams.ts Generated TS type for memory update params.
codex-rs/app-server-protocol/schema/typescript/v2/MemoryDropResponse.ts Generated TS type for memory drop response.
codex-rs/app-server-protocol/schema/typescript/v2/MemoryDropParams.ts Generated TS type for memory drop params.
codex-rs/app-server-protocol/schema/typescript/v2/McpToolsListResponse.ts Generated TS type for MCP tools list response.
codex-rs/app-server-protocol/schema/typescript/v2/McpToolsListParams.ts Generated TS type for MCP tools list params.
codex-rs/app-server-protocol/schema/typescript/v2/McpServerStartupUpdatedNotification.ts Generated TS type for MCP startup updated notification.
codex-rs/app-server-protocol/schema/typescript/v2/McpServerStartupStatus.ts Generated TS type for MCP startup status enum.
codex-rs/app-server-protocol/schema/typescript/v2/McpServerStartupFailure.ts Generated TS type for MCP startup failure item.
codex-rs/app-server-protocol/schema/typescript/v2/McpServerStartupCompletedNotification.ts Generated TS type for MCP startup completed notification.
codex-rs/app-server-protocol/schema/typescript/v2/McpElicitationRequestResponse.ts Generated TS type for MCP elicitation response payload.
codex-rs/app-server-protocol/schema/typescript/v2/McpElicitationRequestParams.ts Generated TS type for MCP elicitation request params.
codex-rs/app-server-protocol/schema/typescript/v2/McpElicitationDecision.ts Generated TS union type for elicitation decision.
codex-rs/app-server-protocol/schema/typescript/v2/HistoryEntryReadResponse.ts Generated TS type for history entry read response.
codex-rs/app-server-protocol/schema/typescript/v2/HistoryEntryReadParams.ts Generated TS type for history entry read params.
codex-rs/app-server-protocol/schema/typescript/v2/HistoryEntry.ts Generated TS type for history entry.
codex-rs/app-server-protocol/schema/typescript/v2/HistoryAddResponse.ts Generated TS type for history append response.
codex-rs/app-server-protocol/schema/typescript/v2/HistoryAddParams.ts Generated TS type for history append params.
codex-rs/app-server-protocol/schema/typescript/v2/BackgroundEventNotification.ts Generated TS type for background event notification.
codex-rs/app-server-protocol/schema/typescript/ServerRequest.ts Updates union to include MCP elicitation server request.
codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts Updates union to include new v2 notifications.
codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts Updates union to include new v2 RPCs + backward-compatible aliases.
codex-rs/app-server-protocol/schema/json/v2/WarningNotification.json Generated JSON schema for warning notification.
codex-rs/app-server-protocol/schema/json/v2/ThreadUndoStartedNotification.json Generated JSON schema for undo started notification.
codex-rs/app-server-protocol/schema/json/v2/ThreadUndoResponse.json Generated JSON schema for undo response.
codex-rs/app-server-protocol/schema/json/v2/ThreadUndoParams.json Generated JSON schema for undo params.
codex-rs/app-server-protocol/schema/json/v2/ThreadUndoCompletedNotification.json Generated JSON schema for undo completed notification.
codex-rs/app-server-protocol/schema/json/v2/ThreadTurnContextSetResponse.json Generated JSON schema for context update response.
codex-rs/app-server-protocol/schema/json/v2/ThreadTurnContextSetParams.json Generated JSON schema for context update params.
codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json Generated JSON schema updated with enriched bootstrap fields.
codex-rs/app-server-protocol/schema/json/v2/ThreadShutdownResponse.json Generated JSON schema for shutdown response.
codex-rs/app-server-protocol/schema/json/v2/ThreadShutdownParams.json Generated JSON schema for shutdown params.
codex-rs/app-server-protocol/schema/json/v2/ThreadShutdownCompletedNotification.json Generated JSON schema for shutdown completed notification.
codex-rs/app-server-protocol/schema/json/v2/ThreadShellCommandRunResponse.json Generated JSON schema for shell command response.
codex-rs/app-server-protocol/schema/json/v2/ThreadShellCommandRunParams.json Generated JSON schema for shell command params.
codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json Generated JSON schema updated with enriched bootstrap fields.
codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json Generated JSON schema updated with enriched bootstrap fields.
codex-rs/app-server-protocol/schema/json/v2/ThreadConfigReloadResponse.json Generated JSON schema for config reload response.
codex-rs/app-server-protocol/schema/json/v2/ThreadConfigReloadParams.json Generated JSON schema for config reload params.
codex-rs/app-server-protocol/schema/json/v2/StreamErrorNotification.json Generated JSON schema for stream error notification.
codex-rs/app-server-protocol/schema/json/v2/SkillsUpdatedNotification.json Generated JSON schema for skills updated notification.
codex-rs/app-server-protocol/schema/json/v2/PromptCustomListResponse.json Generated JSON schema for custom prompt list response.
codex-rs/app-server-protocol/schema/json/v2/PromptCustomListParams.json Generated JSON schema for custom prompt list params.
codex-rs/app-server-protocol/schema/json/v2/MemoryUpdateResponse.json Generated JSON schema for memory update response.
codex-rs/app-server-protocol/schema/json/v2/MemoryUpdateParams.json Generated JSON schema for memory update params.
codex-rs/app-server-protocol/schema/json/v2/MemoryDropResponse.json Generated JSON schema for memory drop response.
codex-rs/app-server-protocol/schema/json/v2/MemoryDropParams.json Generated JSON schema for memory drop params.
codex-rs/app-server-protocol/schema/json/v2/McpToolsListResponse.json Generated JSON schema for MCP tools list response.
codex-rs/app-server-protocol/schema/json/v2/McpToolsListParams.json Generated JSON schema for MCP tools list params.
codex-rs/app-server-protocol/schema/json/v2/McpServerStartupUpdatedNotification.json Generated JSON schema for MCP startup updated notification.
codex-rs/app-server-protocol/schema/json/v2/McpServerStartupCompletedNotification.json Generated JSON schema for MCP startup completed notification.
codex-rs/app-server-protocol/schema/json/v2/HistoryEntryReadResponse.json Generated JSON schema for history entry read response.
codex-rs/app-server-protocol/schema/json/v2/HistoryEntryReadParams.json Generated JSON schema for history entry read params.
codex-rs/app-server-protocol/schema/json/v2/HistoryAddResponse.json Generated JSON schema for history append response.
codex-rs/app-server-protocol/schema/json/v2/HistoryAddParams.json Generated JSON schema for history append params.
codex-rs/app-server-protocol/schema/json/v2/BackgroundEventNotification.json Generated JSON schema for background event notification.
codex-rs/app-server-protocol/schema/json/ServerRequest.json Generated JSON schema updated for MCP elicitation server request.
codex-rs/app-server-protocol/schema/json/ServerNotification.json Generated JSON schema updated for new server notifications.
codex-rs/app-server-protocol/schema/json/McpElicitationRequestResponse.json Generated JSON schema for MCP elicitation response payload.
codex-rs/app-server-protocol/schema/json/McpElicitationRequestParams.json Generated JSON schema for MCP elicitation request params.
codex-rs/app-server-protocol/schema/json/ClientRequest.json Generated JSON schema updated for new v2 RPCs + aliases.
codex-rs/app-server-client/src/lib.rs New in-process client facade crate with backpressure/lag/shutdown handling.
codex-rs/app-server-client/README.md Documentation for new in-process client crate.
codex-rs/app-server-client/Cargo.toml New crate manifest for codex-app-server-client.
codex-rs/README.md Documents that codex exec and TUI run through app-server in-process.
codex-rs/Cargo.toml Adds app-server-client to workspace members and dependencies.
codex-rs/Cargo.lock Adds lockfile entries for new crate and updated deps.
Comments suppressed due to low confidence (1)

codex-rs/app-server/src/message_processor.rs:389

  • match codex_request { ... } moves codex_request, but the function then calls codex_request.experimental_reason() and later match codex_request { ... } again. This won’t compile (use-after-move). Consider matching on &codex_request for the initialization/guard checks (or using if matches!(&codex_request, ...)) so the request value remains available for later dispatch, or restructure into a single match that performs the guards and then delegates.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +294 to +301
request.id().clone(),
JSONRPCErrorError {
code: -32001,
message: "in-process app-server event queue is full".to_string(),
data: None,
},
);
}
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overload error code is hard-coded as -32001 here. Since this value is already defined as OVERLOADED_ERROR_CODE in codex-app-server (and used by both transport and in-process runtimes), consider defining a named constant in this crate (e.g., const OVERLOADED_ERROR_CODE: i64 = -32001;) and using it in both places to avoid magic numbers and keep semantics aligned if the code ever changes.

Copilot uses AI. Check for mistakes.
@fcoury fcoury force-pushed the feat/tui-app-server branch from d6b8906 to ab1c02f Compare March 5, 2026 23:14
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
codex-rs/core/src/mcp_connection_manager.rs (1)

319-342: ⚠️ Potential issue | 🟠 Major

Handle event-send failure before awaiting rx to avoid stuck elicitation futures.

If tx_event.send(...) fails, the pending responder remains stored and rx.await can hang indefinitely.

Suggested fix
                 let (tx, rx) = oneshot::channel();
                 {
                     let mut lock = elicitation_requests.lock().await;
                     lock.insert((server_name.clone(), id.clone()), tx);
                 }
-                let _ = tx_event
-                    .send(Event {
-                        id: "mcp_elicitation_request".to_string(),
-                        msg: EventMsg::ElicitationRequest(ElicitationRequestEvent {
-                            server_name,
-                            id: match id.clone() {
-                                rmcp::model::NumberOrString::String(value) => {
-                                    ProtocolRequestId::String(value.to_string())
-                                }
-                                rmcp::model::NumberOrString::Number(value) => {
-                                    ProtocolRequestId::Integer(value)
-                                }
-                            },
-                            request,
-                        }),
-                    })
-                    .await;
+                if tx_event
+                    .send(Event {
+                        id: "mcp_elicitation_request".to_string(),
+                        msg: EventMsg::ElicitationRequest(ElicitationRequestEvent {
+                            server_name: server_name.clone(),
+                            id: match id.clone() {
+                                rmcp::model::NumberOrString::String(value) => {
+                                    ProtocolRequestId::String(value.to_string())
+                                }
+                                rmcp::model::NumberOrString::Number(value) => {
+                                    ProtocolRequestId::Integer(value)
+                                }
+                            },
+                            request,
+                        }),
+                    })
+                    .await
+                    .is_err()
+                {
+                    let mut lock = elicitation_requests.lock().await;
+                    lock.remove(&(server_name.clone(), id.clone()));
+                    return Err(anyhow!("failed to emit MCP elicitation request"));
+                }
                 rx.await
                     .context("elicitation request channel closed unexpectedly")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/mcp_connection_manager.rs` around lines 319 - 342, The code
inserts a responder into elicitation_requests then sends the Event via tx_event
but doesn't handle the send() failure, which can leave the stored oneshot
responder and cause rx.await to hang; change the flow in the block around
oneshot::channel(), elicitation_requests, and tx_event.send(...) so that you
first insert the (server_name.clone(), id.clone()) => tx into
elicitation_requests, attempt tx_event.send(...).await and if send returns Err,
acquire the elicitation_requests lock again and remove the previously inserted
key (or otherwise take ownership of the tx) and return an Err immediately
(propagating context) instead of awaiting rx; only on Ok from send should you
await rx. Reference symbols: elicitation_requests, tx_event.send(Event { id:
"mcp_elicitation_request", msg:
EventMsg::ElicitationRequest(ElicitationRequestEvent { server_name, id:
ProtocolRequestId::..., request }) }), oneshot::channel()'s tx/rx, and rx.await.
codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json (1)

3247-3264: ⚠️ Potential issue | 🟠 Major

Backward-compatibility risk in elicitation_request event shape.

Line 3263 now makes request mandatory, which rejects legacy payloads that only provide message. That introduces a wire-shape break during migration.

Proposed compatibility-safe schema update
         {
           "properties": {
             "id": {
               "$ref": "#/definitions/v2/RequestId"
             },
+            "message": {
+              "type": "string"
+            },
             "request": {
               "$ref": "#/definitions/ElicitationRequest"
             },
             "server_name": {
               "type": "string"
             },
             "type": {
               "enum": [
                 "elicitation_request"
               ],
               "title": "ElicitationRequestEventMsgType",
               "type": "string"
             }
           },
+          "anyOf": [
+            { "required": ["message"] },
+            { "required": ["request"] }
+          ],
           "required": [
             "id",
-            "request",
             "server_name",
             "type"
           ],
           "title": "ElicitationRequestEventMsg",
           "type": "object"
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json`
around lines 3247 - 3264, The schema change made "request" required on the
elicitation_request event and breaks legacy payloads that only include
"message"; revert this compatibility break by removing "request" from the
required array for the elicitation_request event (the object whose "type" enum
contains "elicitation_request" and that references ElicitationRequest) or
alternatively make validation accept either property via oneOf; ensure the
properties include both "request" and "message" and that only "id" and
"server_name" (or the original required set) remain mandatory so payloads with
only "message" still validate.
codex-rs/app-server-protocol/schema/json/EventMsg.json (1)

2016-2033: ⚠️ Potential issue | 🟠 Major

Preserve backward compatibility for legacy message-based elicitation payloads.

Line 2032 and Line 7825 now require request and remove message, which is a wire-shape breaking change for older clients. Keep support for both fields during migration.

Suggested compatibility schema shape
           "properties": {
             "id": {
               "$ref": "#/definitions/RequestId"
             },
+            "message": {
+              "type": "string"
+            },
             "request": {
               "$ref": "#/definitions/ElicitationRequest"
             },
             "server_name": {
               "type": "string"
             },
             "type": {
               "enum": [
                 "elicitation_request"
               ],
               "title": "ElicitationRequestEventMsgType",
               "type": "string"
             }
           },
-          "required": [
-            "id",
-            "request",
-            "server_name",
-            "type"
-          ],
+          "required": [
+            "id",
+            "server_name",
+            "type"
+          ],
+          "anyOf": [
+            { "required": ["request"] },
+            { "required": ["message"] }
+          ],

Also applies to: 7809-7826

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server-protocol/schema/json/EventMsg.json` around lines 2016 -
2033, The schema change made the elicitation_request event wire-incompatible by
requiring "request" and removing legacy "message"; update the EventMsg.json
definition for the object with "type": "elicitation_request" (the
ElicitationRequestEventMsg shape) to accept both payload shapes during
migration: make both "request" and "message" optional in properties and replace
the current "required" list that forces "request" with an anyOf/oneOf constraint
that enforces at least one of ["request","message"] is present (or use a
discriminator-free oneOf that accepts either the new {required:["request"]} or
legacy {required:["message"]} variant), keeping "id" and "server_name" required
as before.
codex-rs/core/src/plugins/manager.rs (1)

517-525: ⚠️ Potential issue | 🟡 Minor

Normalize app connector IDs before storing them.

You validate with trim() but persist the untrimmed string, which can leave invisible whitespace bugs in connector IDs.

🔧 Proposed normalization fix
         .filter_map(|app| {
-            if app.id.trim().is_empty() {
+            let id = app.id.trim();
+            if id.is_empty() {
                 warn!(
                     plugin = %plugin_root.display(),
                     "plugin app config is missing an app id"
                 );
                 None
             } else {
-                Some(AppConnectorId(app.id))
+                Some(AppConnectorId(id.to_string()))
             }
         })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/plugins/manager.rs` around lines 517 - 525, The code
validates app.id with app.id.trim().is_empty() but stores the original untrimmed
string; change the storage to a normalized, trimmed value so invisible
whitespace can't persist: in the branch that returns
Some(AppConnectorId(app.id)), replace it with creating AppConnectorId from the
trimmed string (e.g., app.id.trim().to_string()) so the saved connector id is
normalized; update the construction in the same block (where AppConnectorId is
created) to use the trimmed value.
codex-rs/artifacts/src/client.rs (1)

65-81: ⚠️ Potential issue | 🟠 Major

Prevent caller env from overriding reserved runtime env keys.

At lines 80-82 and 105-107, request.env is applied after internal env setup. Since Rust's Command::env() uses last-write-wins semantics, duplicate keys allow callers to overwrite CODEX_ARTIFACT_BUILD_ENTRYPOINT, CODEX_ARTIFACT_RENDER_ENTRYPOINT, and ELECTRON_RUN_AS_NODE, potentially bypassing runtime configuration controls.

Apply request.env before setting reserved vars to ensure internal env vars take precedence:

🔐 Suggested fix
-        command
-            .arg(&script_path)
-            .current_dir(&request.cwd)
-            .env("CODEX_ARTIFACT_BUILD_ENTRYPOINT", runtime.build_js_path())
-            .env(
-                "CODEX_ARTIFACT_RENDER_ENTRYPOINT",
-                runtime.render_cli_path(),
-            )
-            .stdout(Stdio::piped())
-            .stderr(Stdio::piped());
-        if js_runtime.requires_electron_run_as_node() {
-            command.env("ELECTRON_RUN_AS_NODE", "1");
-        }
         for (key, value) in &request.env {
             command.env(key, value);
         }
+        command
+            .arg(&script_path)
+            .current_dir(&request.cwd)
+            .stdout(Stdio::piped())
+            .stderr(Stdio::piped())
+            .env("CODEX_ARTIFACT_BUILD_ENTRYPOINT", runtime.build_js_path())
+            .env(
+                "CODEX_ARTIFACT_RENDER_ENTRYPOINT",
+                runtime.render_cli_path(),
+            );
+        if js_runtime.requires_electron_run_as_node() {
+            command.env("ELECTRON_RUN_AS_NODE", "1");
+        }

Also applies to: execute_render (lines 105-107)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/artifacts/src/client.rs` around lines 65 - 81, The request.env map
is applied after internal env vars so callers can overwrite reserved keys;
reorder or guard this in both execute_build and execute_render: apply the loop
over request.env (referencing request.env and the for (key, value) in
&request.env loop) before setting CODEX_ARTIFACT_BUILD_ENTRYPOINT
(runtime.build_js_path()), CODEX_ARTIFACT_RENDER_ENTRYPOINT
(runtime.render_cli_path()), and ELECTRON_RUN_AS_NODE
(js_runtime.requires_electron_run_as_node()), or alternatively skip any keys
matching those reserved names when iterating request.env so the Command built
from Command::new(js_runtime.executable_path()) cannot be influenced by
caller-supplied overrides.
♻️ Duplicate comments (3)
codex-rs/app-server/src/in_process.rs (1)

232-239: ⚠️ Potential issue | 🟠 Major

Bound shutdown enqueue wait to avoid indefinite hangs.

self.client_tx.send(...).await can block forever when the queue is full, preventing the existing timeout/abort cleanup from running.

🔧 Suggested fix
-        if self
-            .client_tx
-            .send(InProcessClientMessage::Shutdown { done_tx })
-            .await
-            .is_ok()
-        {
+        let shutdown_enqueued = timeout(
+            SHUTDOWN_TIMEOUT,
+            self.client_tx.send(InProcessClientMessage::Shutdown { done_tx }),
+        )
+        .await
+        .is_ok_and(|send_result| send_result.is_ok());
+        if shutdown_enqueued {
             let _ = timeout(SHUTDOWN_TIMEOUT, done_rx).await;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/src/in_process.rs` around lines 232 - 239, The send to
self.client_tx can await forever when the channel is full, so wrap or replace
the blocking send used for InProcessClientMessage::Shutdown { done_tx } with a
bounded non-blocking or timed send and fall back to the existing shutdown
timeout: either use a try_send on client_tx and, on failure, log/handle the
backpressure and still run the timeout(SHUTDOWN_TIMEOUT, done_rx).await, or wrap
the async send in timeout(SHUTDOWN_TIMEOUT, self.client_tx.send(...)).await and
handle the Err (timeout) case by proceeding to wait on done_rx with the same
SHUTDOWN_TIMEOUT; ensure you reference client_tx,
InProcessClientMessage::Shutdown, done_tx/done_rx, and SHUTDOWN_TIMEOUT when
applying the change.
codex-rs/app-server-client/src/lib.rs (1)

521-544: ⚠️ Potential issue | 🟠 Major

Run worker cleanup unconditionally before returning shutdown errors.

At Line 531–Line 537, the ?? can return early and bypass the cleanup at Line 539–Line 542, leaving the worker task alive.

🔧 Suggested fix
 pub async fn shutdown(self) -> IoResult<()> {
     let mut worker_handle = self.worker_handle;
     let (response_tx, response_rx) = oneshot::channel();
+    let mut shutdown_err: Option<IoError> = None;
     if self
         .command_tx
         .send(ClientCommand::Shutdown { response_tx })
         .await
         .is_ok()
         && let Ok(command_result) = timeout(SHUTDOWN_TIMEOUT, response_rx).await
     {
-        command_result.map_err(|_| {
-            IoError::new(
-                ErrorKind::BrokenPipe,
-                "in-process app-server shutdown channel is closed",
-            )
-        })??;
+        if let Err(err) = command_result
+            .map_err(|_| {
+                IoError::new(
+                    ErrorKind::BrokenPipe,
+                    "in-process app-server shutdown channel is closed",
+                )
+            })
+            .and_then(|inner| inner)
+        {
+            shutdown_err = Some(err);
+        }
     }

     if let Err(_elapsed) = timeout(SHUTDOWN_TIMEOUT, &mut worker_handle).await {
         worker_handle.abort();
         let _ = worker_handle.await;
     }
+    if let Some(err) = shutdown_err {
+        return Err(err);
+    }
     Ok(())
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server-client/src/lib.rs` around lines 521 - 544, The shutdown
method currently may return early from the command_result??; path and skip
stopping the worker; change the logic in shutdown (function shutdown, variables
worker_handle, command_tx, response_rx, timeout and SHUTDOWN_TIMEOUT) so that
you never return before running the worker cleanup: first attempt the send and
await the response into a local Result (e.g. capture the result of timeout(...)
and of command_result.map_err(...) into a variable), do not use the `??`
early-return operator inline, then after that handling always run the worker
cleanup block (timeout(..., &mut worker_handle), worker_handle.abort(),
worker_handle.await) and finally return any stored error (if the command_result
was Err) or Ok(()) — in short, convert the inline `??` early return into
capturing the error and returning it only after the worker cleanup has executed.
codex-rs/app-server/src/message_processor.rs (1)

327-338: ⚠️ Potential issue | 🟠 Major

Avoid full-payload logging in protocol handlers.

These handlers still emit full objects (notification, typed notification, response, error). They can carry sensitive data; log only safe metadata.

🔒 Redaction-safe logging change
 pub(crate) async fn process_notification(&self, notification: JSONRPCNotification) {
-    tracing::info!("<- notification: {:?}", notification);
+    tracing::debug!(method = %notification.method, "<- notification received");
 }

 pub(crate) async fn process_client_notification(&self, notification: ClientNotification) {
-    tracing::info!("<- typed notification: {:?}", notification);
+    let method = serde_json::to_value(&notification)
+        .ok()
+        .and_then(|v| v.get("method").and_then(serde_json::Value::as_str).map(ToOwned::to_owned))
+        .unwrap_or_else(|| "<unknown>".to_string());
+    tracing::debug!(method = %method, "<- typed notification received");
 }

 pub(crate) async fn process_response(&mut self, response: JSONRPCResponse) {
-    tracing::info!("<- response: {:?}", response);
     let JSONRPCResponse { id, result, .. } = response;
+    tracing::debug!(response_id = ?id, "<- response received");
     self.outgoing.notify_client_response(id, result).await
 }

 pub(crate) async fn process_error(&mut self, err: JSONRPCError) {
-    tracing::error!("<- error: {:?}", err);
-    self.outgoing.notify_client_error(err.id, err.error).await;
+    let JSONRPCError { id, error, .. } = err;
+    tracing::warn!(response_id = ?id, code = error.code, "<- error received");
+    self.outgoing.notify_client_error(id, error).await;
 }

Also applies to: 373-383

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/src/message_processor.rs` around lines 327 - 338, The
handlers process_notification and process_client_notification currently log
entire payloads (notification, typed notification); change them to log only
non-sensitive metadata (e.g., method name, notification.kind/type, optional id
or source) instead of printing the whole object — replace tracing::info!("<-
notification: {:?}", notification) and tracing::info!("<- typed notification:
{:?}", notification) with structured logs that extract and log safe fields
(method, kind/type, id/source, timestamp) and avoid Debug-printing the full
payload; apply the same redaction-safe change to the similar logging at the
other locations mentioned (the block around lines 373-383) so responses/errors
are also logged only with safe metadata.
🧹 Nitpick comments (8)
codex-rs/app-server/src/codex_message_processor.rs (1)

7270-7305: Add one more regression case for service_tier: Some(None).

The current test covers Some(Fast) vs Some(Flex). A second case for explicit clear (Some(None)) would lock behavior for the “clear tier while running” path too.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/src/codex_message_processor.rs` around lines 7270 - 7305,
Add a second regression assertion to the test around
collect_resume_override_mismatches to cover the explicit "clear tier" case:
construct a ThreadResumeParams with service_tier: Some(None) and a
ThreadConfigSnapshot with service_tier: Some(Some(...)) (e.g., Flex) and assert
that collect_resume_override_mismatches returns the expected mismatch string
(e.g., "service_tier requested=None active=Some(Flex)" or matching the existing
message format). Use the same test scaffolding (ThreadResumeParams,
ThreadConfigSnapshot, collect_resume_override_mismatches) so the new case
validates behavior when a running thread requests clearing the tier.
codex-rs/core/src/plugins/render.rs (1)

38-41: Add a non-empty rendering regression test.

Please add one test that validates list formatting for at least one plugin entry (including escaping behavior).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/plugins/render.rs` around lines 38 - 41, Add a new unit
test that calls render_plugins_section with a non-empty slice containing at
least one Plugin (use the same struct/constructor used elsewhere) and asserts
the returned Option is Some(String) matching the expected markdown/list
formatting and escaping; specifically create a plugin entry with characters that
require escaping (e.g., parentheses, brackets, or backticks) and check that
render_plugins_section produces the correct escaped list item string, e.g.,
assert_eq!(render_plugins_section(&[my_plugin]), Some(expected_string)); name
the test something like
render_plugins_section_formats_and_escapes_plugin_entries to make its purpose
clear and reference render_plugins_section and the Plugin value used to build
the test input.
codex-rs/core/src/project_doc.rs (1)

116-121: Add a dedicated plugin-section regression test.

Please add at least one assertion covering plugin section rendering (including placement relative to JS REPL/skills blocks and blank-line separators).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/project_doc.rs` around lines 116 - 121, Add a regression
test that verifies plugin section rendering: create a test that constructs a
ProjectDoc (or the same input used elsewhere) with plugins present so
render_plugins_section produces output, then assert the produced document string
(the result of calling the function that builds output using the
plugins.and_then(render_plugins_section) branch) contains the plugin_section
content and is placed correctly relative to JS REPL/skills blocks with the
correct blank-line separators; specifically, exercise the code path that sets
plugin_section via plugins.and_then(render_plugins_section), check that when
output already contains JS REPL/skills blocks it inserts "\n\n" before
plugin_section, and add at least one assertion for the case where output is
empty (no preceding blocks) to ensure no extra blank lines are added.
codex-rs/core/src/sandboxing/mod.rs (1)

420-426: Deduplicate the two execution wrappers.

execute_env and execute_exec_request_with_after_spawn duplicate the same policy extraction/delegation logic. Consider funneling execute_env through the newer function to keep one code path.

♻️ Proposed refactor
 pub async fn execute_env(
     exec_request: ExecRequest,
     stdout_stream: Option<StdoutStream>,
 ) -> crate::error::Result<ExecToolCallOutput> {
-    let effective_policy = exec_request.sandbox_policy.clone();
-    execute_exec_request(exec_request, &effective_policy, stdout_stream, None).await
+    execute_exec_request_with_after_spawn(exec_request, stdout_stream, None).await
 }

Also applies to: 428-435

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/sandboxing/mod.rs` around lines 420 - 426, The two wrappers
execute_env and execute_exec_request_with_after_spawn duplicate policy
extraction/delegation; refactor execute_env to call the newer
execute_exec_request_with_after_spawn to funnel through one code path: in
execute_env obtain the effective_policy as currently done, then call
execute_exec_request_with_after_spawn(exec_request, &effective_policy,
stdout_stream, None).await (i.e., pass None for the after_spawn callback) and
remove the duplicated delegation logic so execute_exec_request_with_after_spawn
+ execute_exec_request remain the single implementation.
codex-rs/core/src/exec.rs (1)

234-252: Avoid dual source-of-truth for sandbox policy.

execute_exec_request takes sandbox_policy as an argument while also receiving ExecRequest.sandbox_policy (ignored at Line 249). This is easy to drift over time; either use one source or assert equality defensively.

🔧 Defensive consistency check
 pub(crate) async fn execute_exec_request(
     exec_request: ExecRequest,
     sandbox_policy: &SandboxPolicy,
     stdout_stream: Option<StdoutStream>,
     after_spawn: Option<Box<dyn FnOnce() + Send>>,
 ) -> Result<ExecToolCallOutput> {
+    debug_assert_eq!(
+        &exec_request.sandbox_policy,
+        sandbox_policy,
+        "mismatched sandbox policy in execute_exec_request"
+    );
+
     let ExecRequest {
         command,
         cwd,
         env,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/exec.rs` around lines 234 - 252, The function
execute_exec_request currently takes sandbox_policy as a parameter but also
destructures ExecRequest.sandbox_policy into _sandbox_policy_from_env and
ignores it; replace this dual source-of-truth by either (1) using the
ExecRequest.sandbox_policy consistently or (2) verifying they match and
returning an error if they don't—i.e., in execute_exec_request compare the
incoming sandbox_policy parameter with ExecRequest.sandbox_policy (the symbol
_sandbox_policy_from_env) and bail with a clear error (or assert) on mismatch,
or remove the ExecRequest.sandbox_policy field usage entirely and always use the
sandbox_policy parameter to avoid drift.
codex-rs/artifacts/src/runtime/installed.rs (1)

78-83: Validate extracted manifest schema_version explicitly.
After parsing on Line 78, the loader should fail fast on unsupported schema versions instead of relying on downstream behavior.

♻️ Suggested guard (plus small error-surface extension)
--- a/codex-rs/artifacts/src/runtime/error.rs
+++ b/codex-rs/artifacts/src/runtime/error.rs
@@
     #[error("invalid manifest at {path}")]
     InvalidManifest {
         path: PathBuf,
         #[source]
         source: serde_json::Error,
     },
+    #[error("unsupported manifest schema version {found} at {path} (expected {expected})")]
+    UnsupportedManifestSchema {
+        path: PathBuf,
+        found: u32,
+        expected: u32,
+    },
--- a/codex-rs/artifacts/src/runtime/installed.rs
+++ b/codex-rs/artifacts/src/runtime/installed.rs
@@
+const EXTRACTED_RUNTIME_SCHEMA_VERSION: u32 = 1;
@@
         let manifest = serde_json::from_slice::<ExtractedRuntimeManifest>(&manifest_bytes)
             .map_err(|source| ArtifactRuntimeError::InvalidManifest {
                 path: manifest_path,
                 source,
             })?;
+        if manifest.schema_version != EXTRACTED_RUNTIME_SCHEMA_VERSION {
+            return Err(ArtifactRuntimeError::UnsupportedManifestSchema {
+                path: root_dir.join("manifest.json"),
+                found: manifest.schema_version,
+                expected: EXTRACTED_RUNTIME_SCHEMA_VERSION,
+            });
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/artifacts/src/runtime/installed.rs` around lines 78 - 83, After
deserializing the ExtractedRuntimeManifest in the block that sets manifest
(serde_json::from_slice), add an explicit check of manifest.schema_version and
fail fast for any unsupported versions before calling
resolve_relative_runtime_path; return a clear ArtifactRuntimeError variant
(e.g., ArtifactRuntimeError::UnsupportedSchemaVersion or extend
ArtifactRuntimeError::InvalidManifest) that includes the manifest_path and the
found schema_version so callers get immediate feedback about unsupported schema
versions rather than relying on downstream failures.
codex-rs/core/src/tools/handlers/artifacts.rs (1)

340-343: Prefer the exported base-URL constant in this assertion.
This test currently duplicates the release URL string literal; using the crate constant avoids drift if the default ever changes.

♻️ Suggested tweak
-        assert_eq!(
-            manager.config().release().base_url().as_str(),
-            "https://github.com/openai/codex/releases/download/"
-        );
+        assert_eq!(
+            manager.config().release().base_url().as_str(),
+            codex_artifacts::DEFAULT_RELEASE_BASE_URL
+        );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/tools/handlers/artifacts.rs` around lines 340 - 343,
Replace the hard-coded URL string in the assertion that calls
manager.config().release().base_url().as_str() with the crate's exported release
base-URL constant (use the constant exported by the release/config module, e.g.,
RELEASE_BASE_URL or the crate's equivalent name) so the test compares against
that constant instead of duplicating the literal.
codex-rs/artifacts/src/runtime/manager.rs (1)

68-77: Consider documenting base URL format requirement.

Url::join with a path like "tag/file" behaves differently depending on whether base_url ends with a slash. The default URL includes a trailing slash, but if users provide a custom base URL without one, the join may produce unexpected results.

Consider either:

  1. Documenting that base_url must end with a slash, or
  2. Ensuring the trailing slash programmatically in new().
♻️ Optional: Ensure trailing slash in base URL
     pub fn new(base_url: Url, runtime_version: impl Into<String>) -> Self {
+        let base_url = if base_url.as_str().ends_with('/') {
+            base_url
+        } else {
+            Url::parse(&format!("{}/", base_url)).unwrap_or(base_url)
+        };
         Self {
             base_url,
             runtime_version: runtime_version.into(),
             release_tag_prefix: DEFAULT_RELEASE_TAG_PREFIX.to_string(),
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/artifacts/src/runtime/manager.rs` around lines 68 - 77, manifest_url
uses Url::join with a path like "{tag}/{file}" which breaks if base_url lacks a
trailing slash; update the constructor (new()) that creates/accepts
self.base_url to normalize the URL by ensuring it ends with a '/' (or otherwise
canonicalize it) before storing so manifest_url.join(...) always behaves
predictably; reference the manifest_url method and the
release_tag()/manifest_file_name() helpers when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@codex-rs/app-server-protocol/schema/json/McpServerElicitationRequestParams.json`:
- Around line 66-69: The JSON schema for McpServerElicitationRequestParams is
missing "turnId" in its "required" array while the generated TypeScript type
McpServerElicitationRequestParams treats turnId as mandatory (string | null);
update the JSON schema by adding "turnId" to the "required" list and ensure the
turnId property schema allows null (e.g., type: ["string","null"] or equivalent)
so the schema and the generated TS type are aligned.

In
`@codex-rs/app-server-protocol/schema/json/McpServerElicitationRequestResponse.json`:
- Around line 17-23: The schema's "content" property is currently missing a type
and not listed in "required", causing a mismatch with the generated TypeScript
which expects content: JsonValue | null; update the
McpServerElicitationRequestResponse JSON Schema by adding "content" to the
"required" array and define "content" to accept any JSON value (i.e., the JSON
Schema equivalent of JsonValue and nullable) so it aligns with the TS contract
(keep descriptive text in "description" and preserve nullability).

In `@codex-rs/app-server/src/app_server_tracing.rs`:
- Around line 86-93: The span currently hardcodes "in_process" into otel.name
and rpc.method (in the info_span! call), which collapses all in-process RPCs
into one bucket; implement a helper fn typed_request_method(request:
&ClientRequest) -> &'static str (mirror client_request_id's style) that
pattern-matches on ClientRequest variants and returns real, static RPC method
names, then replace the hardcoded "in_process" values in the info_span!
invocation (and any similar usages) with typed_request_method(request) so
otel.name and rpc.method carry the actual method name.

In `@codex-rs/app-server/src/lib.rs`:
- Line 589: The processor is being initialized with a hard-coded
SessionSource::VSCode; change run_main_with_transport to accept a SessionSource
parameter (thread session_source through its signature and callers) and use that
parameter when constructing the processor instead of SessionSource::VSCode;
follow the pattern in the in_process module for accepting and forwarding
session_source, and ensure callers (e.g., the CLI invocation) pass
SessionSource::Cli where appropriate so telemetry/routing use the correct
source.

In `@codex-rs/core/src/exec.rs`:
- Around line 743-745: The after_spawn() callback should be invoked inside
std::panic::catch_unwind to prevent its panic from skipping subsequent cleanup
(like consume_truncated_output) — wrap the call to after_spawn (the
Some(after_spawn) branch) with catch_unwind, record whether it returned Ok or
Err, then proceed to call consume_truncated_output and other termination/timeout
handling as normal, and finally if catch_unwind returned Err re-raise the panic
using std::panic::resume_unwind; refer to the after_spawn variable, the
consume_truncated_output call, and ensure panic handling does not alter existing
kill_on_drop behavior.

In `@codex-rs/core/src/plugins/render.rs`:
- Around line 14-18: The current rendering inserts plugin.display_name raw into
inline backticked markdown (in the lines.extend mapping in render.rs), which
allows backticks/newlines to break formatting; fix it by sanitizing/escaping the
display name before interpolation—add or use a helper like
escape_markdown_inline or sanitize_display_name that replaces backticks with
'\`' and collapses or removes newlines (or encodes them) and then use that
sanitized value in the format!("- `{}`", ...). Update the mapping over plugins
and any related helpers to ensure all plugin.display_name usages in markdown go
through this sanitizer.

---

Outside diff comments:
In
`@codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json`:
- Around line 3247-3264: The schema change made "request" required on the
elicitation_request event and breaks legacy payloads that only include
"message"; revert this compatibility break by removing "request" from the
required array for the elicitation_request event (the object whose "type" enum
contains "elicitation_request" and that references ElicitationRequest) or
alternatively make validation accept either property via oneOf; ensure the
properties include both "request" and "message" and that only "id" and
"server_name" (or the original required set) remain mandatory so payloads with
only "message" still validate.

In `@codex-rs/app-server-protocol/schema/json/EventMsg.json`:
- Around line 2016-2033: The schema change made the elicitation_request event
wire-incompatible by requiring "request" and removing legacy "message"; update
the EventMsg.json definition for the object with "type": "elicitation_request"
(the ElicitationRequestEventMsg shape) to accept both payload shapes during
migration: make both "request" and "message" optional in properties and replace
the current "required" list that forces "request" with an anyOf/oneOf constraint
that enforces at least one of ["request","message"] is present (or use a
discriminator-free oneOf that accepts either the new {required:["request"]} or
legacy {required:["message"]} variant), keeping "id" and "server_name" required
as before.

In `@codex-rs/artifacts/src/client.rs`:
- Around line 65-81: The request.env map is applied after internal env vars so
callers can overwrite reserved keys; reorder or guard this in both execute_build
and execute_render: apply the loop over request.env (referencing request.env and
the for (key, value) in &request.env loop) before setting
CODEX_ARTIFACT_BUILD_ENTRYPOINT (runtime.build_js_path()),
CODEX_ARTIFACT_RENDER_ENTRYPOINT (runtime.render_cli_path()), and
ELECTRON_RUN_AS_NODE (js_runtime.requires_electron_run_as_node()), or
alternatively skip any keys matching those reserved names when iterating
request.env so the Command built from Command::new(js_runtime.executable_path())
cannot be influenced by caller-supplied overrides.

In `@codex-rs/core/src/mcp_connection_manager.rs`:
- Around line 319-342: The code inserts a responder into elicitation_requests
then sends the Event via tx_event but doesn't handle the send() failure, which
can leave the stored oneshot responder and cause rx.await to hang; change the
flow in the block around oneshot::channel(), elicitation_requests, and
tx_event.send(...) so that you first insert the (server_name.clone(),
id.clone()) => tx into elicitation_requests, attempt tx_event.send(...).await
and if send returns Err, acquire the elicitation_requests lock again and remove
the previously inserted key (or otherwise take ownership of the tx) and return
an Err immediately (propagating context) instead of awaiting rx; only on Ok from
send should you await rx. Reference symbols: elicitation_requests,
tx_event.send(Event { id: "mcp_elicitation_request", msg:
EventMsg::ElicitationRequest(ElicitationRequestEvent { server_name, id:
ProtocolRequestId::..., request }) }), oneshot::channel()'s tx/rx, and rx.await.

In `@codex-rs/core/src/plugins/manager.rs`:
- Around line 517-525: The code validates app.id with app.id.trim().is_empty()
but stores the original untrimmed string; change the storage to a normalized,
trimmed value so invisible whitespace can't persist: in the branch that returns
Some(AppConnectorId(app.id)), replace it with creating AppConnectorId from the
trimmed string (e.g., app.id.trim().to_string()) so the saved connector id is
normalized; update the construction in the same block (where AppConnectorId is
created) to use the trimmed value.

---

Duplicate comments:
In `@codex-rs/app-server-client/src/lib.rs`:
- Around line 521-544: The shutdown method currently may return early from the
command_result??; path and skip stopping the worker; change the logic in
shutdown (function shutdown, variables worker_handle, command_tx, response_rx,
timeout and SHUTDOWN_TIMEOUT) so that you never return before running the worker
cleanup: first attempt the send and await the response into a local Result (e.g.
capture the result of timeout(...) and of command_result.map_err(...) into a
variable), do not use the `??` early-return operator inline, then after that
handling always run the worker cleanup block (timeout(..., &mut worker_handle),
worker_handle.abort(), worker_handle.await) and finally return any stored error
(if the command_result was Err) or Ok(()) — in short, convert the inline `??`
early return into capturing the error and returning it only after the worker
cleanup has executed.

In `@codex-rs/app-server/src/in_process.rs`:
- Around line 232-239: The send to self.client_tx can await forever when the
channel is full, so wrap or replace the blocking send used for
InProcessClientMessage::Shutdown { done_tx } with a bounded non-blocking or
timed send and fall back to the existing shutdown timeout: either use a try_send
on client_tx and, on failure, log/handle the backpressure and still run the
timeout(SHUTDOWN_TIMEOUT, done_rx).await, or wrap the async send in
timeout(SHUTDOWN_TIMEOUT, self.client_tx.send(...)).await and handle the Err
(timeout) case by proceeding to wait on done_rx with the same SHUTDOWN_TIMEOUT;
ensure you reference client_tx, InProcessClientMessage::Shutdown,
done_tx/done_rx, and SHUTDOWN_TIMEOUT when applying the change.

In `@codex-rs/app-server/src/message_processor.rs`:
- Around line 327-338: The handlers process_notification and
process_client_notification currently log entire payloads (notification, typed
notification); change them to log only non-sensitive metadata (e.g., method
name, notification.kind/type, optional id or source) instead of printing the
whole object — replace tracing::info!("<- notification: {:?}", notification) and
tracing::info!("<- typed notification: {:?}", notification) with structured logs
that extract and log safe fields (method, kind/type, id/source, timestamp) and
avoid Debug-printing the full payload; apply the same redaction-safe change to
the similar logging at the other locations mentioned (the block around lines
373-383) so responses/errors are also logged only with safe metadata.

---

Nitpick comments:
In `@codex-rs/app-server/src/codex_message_processor.rs`:
- Around line 7270-7305: Add a second regression assertion to the test around
collect_resume_override_mismatches to cover the explicit "clear tier" case:
construct a ThreadResumeParams with service_tier: Some(None) and a
ThreadConfigSnapshot with service_tier: Some(Some(...)) (e.g., Flex) and assert
that collect_resume_override_mismatches returns the expected mismatch string
(e.g., "service_tier requested=None active=Some(Flex)" or matching the existing
message format). Use the same test scaffolding (ThreadResumeParams,
ThreadConfigSnapshot, collect_resume_override_mismatches) so the new case
validates behavior when a running thread requests clearing the tier.

In `@codex-rs/artifacts/src/runtime/installed.rs`:
- Around line 78-83: After deserializing the ExtractedRuntimeManifest in the
block that sets manifest (serde_json::from_slice), add an explicit check of
manifest.schema_version and fail fast for any unsupported versions before
calling resolve_relative_runtime_path; return a clear ArtifactRuntimeError
variant (e.g., ArtifactRuntimeError::UnsupportedSchemaVersion or extend
ArtifactRuntimeError::InvalidManifest) that includes the manifest_path and the
found schema_version so callers get immediate feedback about unsupported schema
versions rather than relying on downstream failures.

In `@codex-rs/artifacts/src/runtime/manager.rs`:
- Around line 68-77: manifest_url uses Url::join with a path like "{tag}/{file}"
which breaks if base_url lacks a trailing slash; update the constructor (new())
that creates/accepts self.base_url to normalize the URL by ensuring it ends with
a '/' (or otherwise canonicalize it) before storing so manifest_url.join(...)
always behaves predictably; reference the manifest_url method and the
release_tag()/manifest_file_name() helpers when making the change.

In `@codex-rs/core/src/exec.rs`:
- Around line 234-252: The function execute_exec_request currently takes
sandbox_policy as a parameter but also destructures ExecRequest.sandbox_policy
into _sandbox_policy_from_env and ignores it; replace this dual source-of-truth
by either (1) using the ExecRequest.sandbox_policy consistently or (2) verifying
they match and returning an error if they don't—i.e., in execute_exec_request
compare the incoming sandbox_policy parameter with ExecRequest.sandbox_policy
(the symbol _sandbox_policy_from_env) and bail with a clear error (or assert) on
mismatch, or remove the ExecRequest.sandbox_policy field usage entirely and
always use the sandbox_policy parameter to avoid drift.

In `@codex-rs/core/src/plugins/render.rs`:
- Around line 38-41: Add a new unit test that calls render_plugins_section with
a non-empty slice containing at least one Plugin (use the same
struct/constructor used elsewhere) and asserts the returned Option is
Some(String) matching the expected markdown/list formatting and escaping;
specifically create a plugin entry with characters that require escaping (e.g.,
parentheses, brackets, or backticks) and check that render_plugins_section
produces the correct escaped list item string, e.g.,
assert_eq!(render_plugins_section(&[my_plugin]), Some(expected_string)); name
the test something like
render_plugins_section_formats_and_escapes_plugin_entries to make its purpose
clear and reference render_plugins_section and the Plugin value used to build
the test input.

In `@codex-rs/core/src/project_doc.rs`:
- Around line 116-121: Add a regression test that verifies plugin section
rendering: create a test that constructs a ProjectDoc (or the same input used
elsewhere) with plugins present so render_plugins_section produces output, then
assert the produced document string (the result of calling the function that
builds output using the plugins.and_then(render_plugins_section) branch)
contains the plugin_section content and is placed correctly relative to JS
REPL/skills blocks with the correct blank-line separators; specifically,
exercise the code path that sets plugin_section via
plugins.and_then(render_plugins_section), check that when output already
contains JS REPL/skills blocks it inserts "\n\n" before plugin_section, and add
at least one assertion for the case where output is empty (no preceding blocks)
to ensure no extra blank lines are added.

In `@codex-rs/core/src/sandboxing/mod.rs`:
- Around line 420-426: The two wrappers execute_env and
execute_exec_request_with_after_spawn duplicate policy extraction/delegation;
refactor execute_env to call the newer execute_exec_request_with_after_spawn to
funnel through one code path: in execute_env obtain the effective_policy as
currently done, then call execute_exec_request_with_after_spawn(exec_request,
&effective_policy, stdout_stream, None).await (i.e., pass None for the
after_spawn callback) and remove the duplicated delegation logic so
execute_exec_request_with_after_spawn + execute_exec_request remain the single
implementation.

In `@codex-rs/core/src/tools/handlers/artifacts.rs`:
- Around line 340-343: Replace the hard-coded URL string in the assertion that
calls manager.config().release().base_url().as_str() with the crate's exported
release base-URL constant (use the constant exported by the release/config
module, e.g., RELEASE_BASE_URL or the crate's equivalent name) so the test
compares against that constant instead of duplicating the literal.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9e48084a-4199-4931-9c8e-64d304eb7936

📥 Commits

Reviewing files that changed from the base of the PR and between d6b8906 and ab1c02f.

⛔ Files ignored due to path filters (2)
  • MODULE.bazel.lock is excluded by !**/*.lock
  • codex-rs/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (57)
  • codex-rs/Cargo.toml
  • codex-rs/app-server-client/Cargo.toml
  • codex-rs/app-server-client/README.md
  • codex-rs/app-server-client/src/lib.rs
  • codex-rs/app-server-protocol/Cargo.toml
  • codex-rs/app-server-protocol/schema/json/EventMsg.json
  • codex-rs/app-server-protocol/schema/json/McpServerElicitationRequestParams.json
  • codex-rs/app-server-protocol/schema/json/McpServerElicitationRequestResponse.json
  • codex-rs/app-server-protocol/schema/json/ServerRequest.json
  • codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
  • codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json
  • codex-rs/app-server-protocol/schema/typescript/ElicitationRequest.ts
  • codex-rs/app-server-protocol/schema/typescript/ElicitationRequestEvent.ts
  • codex-rs/app-server-protocol/schema/typescript/ServerRequest.ts
  • codex-rs/app-server-protocol/schema/typescript/index.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/McpServerElicitationAction.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/McpServerElicitationRequestParams.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/McpServerElicitationRequestResponse.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/index.ts
  • codex-rs/app-server-protocol/src/protocol/common.rs
  • codex-rs/app-server-protocol/src/protocol/v2.rs
  • codex-rs/app-server/Cargo.toml
  • codex-rs/app-server/README.md
  • codex-rs/app-server/src/app_server_tracing.rs
  • codex-rs/app-server/src/bespoke_event_handling.rs
  • codex-rs/app-server/src/codex_message_processor.rs
  • codex-rs/app-server/src/in_process.rs
  • codex-rs/app-server/src/lib.rs
  • codex-rs/app-server/src/message_processor.rs
  • codex-rs/app-server/tests/common/models_cache.rs
  • codex-rs/app-server/tests/suite/v2/mcp_server_elicitation.rs
  • codex-rs/app-server/tests/suite/v2/mod.rs
  • codex-rs/artifacts/Cargo.toml
  • codex-rs/artifacts/README.md
  • codex-rs/artifacts/src/client.rs
  • codex-rs/artifacts/src/lib.rs
  • codex-rs/artifacts/src/runtime/error.rs
  • codex-rs/artifacts/src/runtime/installed.rs
  • codex-rs/artifacts/src/runtime/js_runtime.rs
  • codex-rs/artifacts/src/runtime/manager.rs
  • codex-rs/artifacts/src/runtime/manifest.rs
  • codex-rs/artifacts/src/runtime/mod.rs
  • codex-rs/artifacts/src/tests.rs
  • codex-rs/codex-api/tests/models_integration.rs
  • codex-rs/core/src/client_common.rs
  • codex-rs/core/src/codex.rs
  • codex-rs/core/src/exec.rs
  • codex-rs/core/src/features.rs
  • codex-rs/core/src/mcp_connection_manager.rs
  • codex-rs/core/src/models_manager/model_info.rs
  • codex-rs/core/src/plugins/manager.rs
  • codex-rs/core/src/plugins/mod.rs
  • codex-rs/core/src/plugins/render.rs
  • codex-rs/core/src/project_doc.rs
  • codex-rs/core/src/sandboxing/mod.rs
  • codex-rs/core/src/tasks/user_shell.rs
  • codex-rs/core/src/tools/handlers/artifacts.rs
✅ Files skipped from review due to trivial changes (1)
  • codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • codex-rs/app-server-client/README.md
  • codex-rs/app-server/src/bespoke_event_handling.rs
  • codex-rs/app-server-protocol/src/protocol/common.rs
  • codex-rs/app-server-client/Cargo.toml
  • codex-rs/Cargo.toml

Comment on lines +66 to +69
"required": [
"serverName",
"threadId"
],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Schema/type contract drift: turnId is optional here but required in generated TS type.

At Line [66]-Line [69], turnId is not listed in required, but codex-rs/app-server-protocol/schema/typescript/v2/McpServerElicitationRequestParams.ts models it as mandatory (string | null). Please align both surfaces to avoid validator/codegen mismatch.

Proposed schema fix (if `turnId` should always be present, nullable when unknown)
   "required": [
     "serverName",
-    "threadId"
+    "threadId",
+    "turnId"
   ],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"required": [
"serverName",
"threadId"
],
"required": [
"serverName",
"threadId",
"turnId"
],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@codex-rs/app-server-protocol/schema/json/McpServerElicitationRequestParams.json`
around lines 66 - 69, The JSON schema for McpServerElicitationRequestParams is
missing "turnId" in its "required" array while the generated TypeScript type
McpServerElicitationRequestParams treats turnId as mandatory (string | null);
update the JSON schema by adding "turnId" to the "required" list and ensure the
turnId property schema allows null (e.g., type: ["string","null"] or equivalent)
so the schema and the generated TS type are aligned.

Comment on lines +17 to +23
"content": {
"description": "Structured user input for accepted elicitations, mirroring RMCP `CreateElicitationResult`.\n\nThis is nullable because decline/cancel responses have no content."
}
},
"required": [
"action"
],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== JSON Schema File ==="
if [ -f "codex-rs/app-server-protocol/schema/json/McpServerElicitationRequestResponse.json" ]; then
    cat -n "codex-rs/app-server-protocol/schema/json/McpServerElicitationRequestResponse.json"
else
    echo "File not found"
    find . -name "*McpServerElicitationRequestResponse*" -type f
fi

echo ""
echo "=== TypeScript Generated File ==="
if [ -f "codex-rs/app-server-protocol/schema/typescript/v2/McpServerElicitationRequestResponse.ts" ]; then
    cat -n "codex-rs/app-server-protocol/schema/typescript/v2/McpServerElicitationRequestResponse.ts"
else
    echo "File not found"
    find . -name "*McpServerElicitationRequestResponse*" -type f
fi

Repository: fcoury/oldcodex

Length of output: 1616


Schema/TS contract mismatch on content field.

The JSON schema defines content as optional (not in required array) and lacks a type definition, but the generated TypeScript expects content: JsonValue | null as a required field. This allows payloads valid against the schema but invalid against the TypeScript contract.

Add "content" to the required array and define its type:

Proposed schema patch
   "properties": {
     "action": {
       "$ref": "#/definitions/McpServerElicitationAction"
     },
     "content": {
-      "description": "Structured user input for accepted elicitations, mirroring RMCP `CreateElicitationResult`.\n\nThis is nullable because decline/cancel responses have no content."
+      "type": ["array", "boolean", "number", "object", "string", "null"],
+      "description": "Structured user input for accepted elicitations, mirroring RMCP `CreateElicitationResult`.\n\nThis is nullable because decline/cancel responses have no content."
     }
   },
   "required": [
-    "action"
+    "action",
+    "content"
   ],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"content": {
"description": "Structured user input for accepted elicitations, mirroring RMCP `CreateElicitationResult`.\n\nThis is nullable because decline/cancel responses have no content."
}
},
"required": [
"action"
],
"content": {
"type": ["array", "boolean", "number", "object", "string", "null"],
"description": "Structured user input for accepted elicitations, mirroring RMCP `CreateElicitationResult`.\n\nThis is nullable because decline/cancel responses have no content."
}
},
"required": [
"action",
"content"
],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@codex-rs/app-server-protocol/schema/json/McpServerElicitationRequestResponse.json`
around lines 17 - 23, The schema's "content" property is currently missing a
type and not listed in "required", causing a mismatch with the generated
TypeScript which expects content: JsonValue | null; update the
McpServerElicitationRequestResponse JSON Schema by adding "content" to the
"required" array and define "content" to accept any JSON value (i.e., the JSON
Schema equivalent of JsonValue and nullable) so it aligns with the TS contract
(keep descriptive text in "description" and preserve nullability).

Comment on lines +743 to +745
if let Some(after_spawn) = after_spawn {
after_spawn();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine the exact file and lines mentioned
find . -type f -name "exec.rs" | head -20

Repository: fcoury/oldcodex

Length of output: 122


🏁 Script executed:

# Count lines in the file to understand its size
wc -l codex-rs/core/src/exec.rs

Repository: fcoury/oldcodex

Length of output: 89


🏁 Script executed:

# Read the section around lines 743-745 to verify the code matches
sed -n '730,760p' codex-rs/core/src/exec.rs

Repository: fcoury/oldcodex

Length of output: 1252


🏁 Script executed:

# Search for the spawn_child_async call and context
rg -nP -A 20 'let\s+\w+\s+=\s+spawn_child_async' codex-rs/core/src/exec.rs

Repository: fcoury/oldcodex

Length of output: 889


🏁 Script executed:

# Search for after_spawn callback type definitions
rg -nP 'after_spawn.*Option<Box<dyn FnOnce' codex-rs/core/src/exec.rs

Repository: fcoury/oldcodex

Length of output: 168


🏁 Script executed:

# Look for all after_spawn usages
rg -nP '\bafter_spawn\b' codex-rs/core/src/exec.rs -C 4

Repository: fcoury/oldcodex

Length of output: 1320


🏁 Script executed:

# Check if there are any panic-prone operations in callback registrations
rg -nP 'after_spawn\s*=' codex-rs/core/src/exec.rs -B 5 -A 10

Repository: fcoury/oldcodex

Length of output: 41


🏁 Script executed:

# Read the consume_truncated_output function to understand what it does
sed -n '749,850p' codex-rs/core/src/exec.rs

Repository: fcoury/oldcodex

Length of output: 3774


🏁 Script executed:

# Look for any cleanup/drop logic for Child processes
rg -nP 'impl.*Drop.*Child|fn drop|kill_child|terminate' codex-rs/core/src/exec.rs -C 3

Repository: fcoury/oldcodex

Length of output: 1897


🏁 Script executed:

# Check the Child type definition and imports
rg -nP '^(use|type).*Child' codex-rs/core/src/exec.rs | head -20

Repository: fcoury/oldcodex

Length of output: 128


🏁 Script executed:

# Find actual usages of after_spawn callback in tests and real code
rg -nP 'after_spawn\s*=' --type=rust -C 6

Repository: fcoury/oldcodex

Length of output: 41


🏁 Script executed:

# Check what happens when we don't consume the child output
rg -nP 'spawn_child_async|Child\s*\{' codex-rs/core/src/exec.rs -C 2 | head -40

Repository: fcoury/oldcodex

Length of output: 456


🏁 Script executed:

# Search for where after_spawn is passed when calling execute_exec_request
rg -nP 'execute_exec_request\(' --type=rust -C 5

Repository: fcoury/oldcodex

Length of output: 3119


🏁 Script executed:

# Search for closures being passed to after_spawn
rg -nP 'after_spawn\s*:\s*' --type=rust -B 2 -A 2

Repository: fcoury/oldcodex

Length of output: 3873


🏁 Script executed:

# Look for Box::new or move || patterns that create the callback
rg -nP 'Box::new\(.*\)|move\s*\|\|' codex-rs --type=rust -C 3 | grep -A 3 -B 3 'after\|spawn'

Repository: fcoury/oldcodex

Length of output: 41014


🏁 Script executed:

# Check test code to see how after_spawn is used
rg -nP 'after_spawn' codex-rs/core/tests --type=rust -B 3 -A 3

Repository: fcoury/oldcodex

Length of output: 41


🏁 Script executed:

# Search for where after_spawn callbacks are created/implemented
rg -nP 'Box::new\(|move\s*\|\|' codex-rs/shell-escalation/src/unix/escalate_server.rs -A 10 | grep -A 10 -B 10 'after_spawn'

Repository: fcoury/oldcodex

Length of output: 41


🏁 Script executed:

# Look at the unix_escalation implementation
rg -nP 'after_spawn' codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs -B 5 -A 10

Repository: fcoury/oldcodex

Length of output: 1947


🏁 Script executed:

# Check for callback implementations in test files
rg -nP 'after_spawn\s*=' codex-rs/core/tests --type=rust -B 3 -A 8

Repository: fcoury/oldcodex

Length of output: 41


🏁 Script executed:

# Check if tokio::process::Child has Drop impl that kills process
python3 << 'SCRIPT'
import subprocess
result = subprocess.run(['rg', '-nP', 'impl.*Drop|kill|terminate', 'codex-rs/core/src/spawn.rs'], 
                       capture_output=True, text=True)
print("In spawn.rs:")
print(result.stdout)
print("\nSearching for Child type behavior in Tokio documentation context...")
SCRIPT

Repository: fcoury/oldcodex

Length of output: 296


🏁 Script executed:

# Check if there are any unwrap/expect calls in callback-related code
rg -nP '\b(unwrap|expect|panic!)\b' codex-rs/core/src/exec.rs -B 3 -A 1 | head -50

Repository: fcoury/oldcodex

Length of output: 479


🏁 Script executed:

# Get more context about kill_on_drop from spawn.rs
sed -n '80,130p' codex-rs/core/src/spawn.rs

Repository: fcoury/oldcodex

Length of output: 1830


🏁 Script executed:

# Search for actual callback implementations to see if panic is realistic
rg -nP 'after_spawn' codex-rs/shell-escalation -B 10 -A 5 | head -100

Repository: fcoury/oldcodex

Length of output: 8056


🏁 Script executed:

# Check what happens with child process when it's dropped
rg -nP 'close_client_socket|FD_CLOEXEC|socket' codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs -B 2 -A 2

Repository: fcoury/oldcodex

Length of output: 414


🏁 Script executed:

# Look for actual after_spawn callback implementations
fd -t f -e rs . codex-rs | xargs rg -l 'after_spawn.*Box::new|after_spawn.*\|' | head -10

Repository: fcoury/oldcodex

Length of output: 41


🏁 Script executed:

# Check unix_escalation to see if there are socket operations that could panic
sed -n '740,800p' codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs

Repository: fcoury/oldcodex

Length of output: 2314


🏁 Script executed:

# Search for actual after_spawn callback implementations in the codebase
rg -nP 'after_spawn\s*:\s*Some|Box::new.*||move\s*\|' codex-rs/shell-escalation -B 5 -A 10

Repository: fcoury/oldcodex

Length of output: 50374


🏁 Script executed:

# Look at where after_spawn is created/passed in shell-escalation
sed -n '540,570p' codex-rs/shell-escalation/src/unix/escalate_server.rs

Repository: fcoury/oldcodex

Length of output: 1310


🏁 Script executed:

# Check if there are FD operations or socket operations that could panic
rg -nP 'close\(|dup2|fcntl.*socket' codex-rs/shell-escalation/src/unix -B 2 -A 2

Repository: fcoury/oldcodex

Length of output: 2756


🏁 Script executed:

# Look at the EscalationSession to see how after_spawn callback is created
rg -nP 'after_spawn.*=|close_client_socket' codex-rs/shell-escalation/src/unix/escalate_server.rs -B 5 -A 10 | head -80

Repository: fcoury/oldcodex

Length of output: 2384


Guard after_spawn panic to prevent losing output and bypassing cleanup logic.

If the callback panics at line 744, the panic unwinds before consume_truncated_output, which skips output collection and important cleanup handling (timeout signals, proper termination sequence). Although kill_on_drop(true) prevents process zombification, the loss of managed output handling is still critical.

🛠️ Proposed fix
-    let child = spawn_child_async(SpawnChildRequest {
+    let mut child = spawn_child_async(SpawnChildRequest {
         program: PathBuf::from(program),
         args: args.into(),
         arg0: arg0_ref,
         cwd,
         sandbox_policy,
@@
     })
     .await?;
     if let Some(after_spawn) = after_spawn {
-        after_spawn();
+        let callback_result =
+            std::panic::catch_unwind(std::panic::AssertUnwindSafe(after_spawn));
+        if callback_result.is_err() {
+            let _ = kill_child_process_group(&mut child);
+            let _ = child.start_kill();
+            return Err(CodexErr::Io(io::Error::other(
+                "after_spawn callback panicked",
+            )));
+        }
     }
     consume_truncated_output(child, expiration, stdout_stream).await
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/exec.rs` around lines 743 - 745, The after_spawn() callback
should be invoked inside std::panic::catch_unwind to prevent its panic from
skipping subsequent cleanup (like consume_truncated_output) — wrap the call to
after_spawn (the Some(after_spawn) branch) with catch_unwind, record whether it
returned Ok or Err, then proceed to call consume_truncated_output and other
termination/timeout handling as normal, and finally if catch_unwind returned Err
re-raise the panic using std::panic::resume_unwind; refer to the after_spawn
variable, the consume_truncated_output call, and ensure panic handling does not
alter existing kill_on_drop behavior.

Comment on lines +14 to +18
lines.extend(
plugins
.iter()
.map(|plugin| format!("- `{}`", plugin.display_name)),
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Escape plugin display names before interpolating into markdown.

plugin.display_name is inserted raw into backticked markdown. A crafted name containing backticks/newlines can break formatting and alter instruction content.

🔧 Proposed hardening patch
-    lines.extend(
-        plugins
-            .iter()
-            .map(|plugin| format!("- `{}`", plugin.display_name)),
-    );
+    lines.extend(plugins.iter().map(|plugin| {
+        let safe_name = plugin
+            .display_name
+            .replace('`', "\\`")
+            .replace('\n', " ")
+            .replace('\r', " ");
+        format!("- `{safe_name}`")
+    }));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
lines.extend(
plugins
.iter()
.map(|plugin| format!("- `{}`", plugin.display_name)),
);
lines.extend(plugins.iter().map(|plugin| {
let safe_name = plugin
.display_name
.replace('`', "\\`")
.replace('\n', " ")
.replace('\r', " ");
format!("- `{safe_name}`")
}));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/plugins/render.rs` around lines 14 - 18, The current
rendering inserts plugin.display_name raw into inline backticked markdown (in
the lines.extend mapping in render.rs), which allows backticks/newlines to break
formatting; fix it by sanitizing/escaping the display name before
interpolation—add or use a helper like escape_markdown_inline or
sanitize_display_name that replaces backticks with '\`' and collapses or removes
newlines (or encodes them) and then use that sanitized value in the format!("-
`{}`", ...). Update the mapping over plugins and any related helpers to ensure
all plugin.display_name usages in markdown go through this sanitizer.

@fcoury fcoury force-pushed the feat/tui-app-server branch from ab1c02f to b6fba47 Compare March 6, 2026 15:12
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
codex-rs/core/src/config/mod.rs (1)

1829-1835: ⚠️ Potential issue | 🟠 Major

Preserve explicit-vs-default distinction before pre-normalizing approval policy.

Line 1829 now applies fallback unconditionally, which pre-normalizes explicitly configured disallowed values and prevents apply_requirement_constrained_value(...) from surfacing a startup_warnings message. It also logs "default approval policy..." for explicit values.

💡 Proposed fix
-        let mut approval_policy = approval_policy_override
+        let approval_policy_was_explicit = approval_policy_override.is_some()
+            || config_profile.approval_policy.is_some()
+            || cfg.approval_policy.is_some();
+
+        let mut approval_policy = approval_policy_override
             .or(config_profile.approval_policy)
             .or(cfg.approval_policy)
             .unwrap_or_else(|| {
                 if active_project.is_trusted() {
                     AskForApproval::OnRequest
@@
-        if let Err(err) = constrained_approval_policy.can_set(&approval_policy) {
+        if !approval_policy_was_explicit
+            && let Err(err) = constrained_approval_policy.can_set(&approval_policy)
+        {
             tracing::warn!(
                 error = %err,
                 "default approval policy is disallowed by requirements; falling back to required default"
             );
             approval_policy = constrained_approval_policy.value();
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/config/mod.rs` around lines 1829 - 1835, The current code
unconditionally replaces approval_policy with
constrained_approval_policy.value() when can_set fails, which pre-normalizes
explicit user values and prevents apply_requirement_constrained_value(...) from
surfacing startup_warnings; change the logic to only perform the fallback when
the approval_policy was not explicitly provided (i.e., it is the default) — call
constrained_approval_policy.can_set(&approval_policy) and if it errors, only
assign approval_policy = constrained_approval_policy.value() when the original
approval_policy is the default/unset sentinel; if the value was explicit, leave
approval_policy untouched so apply_requirement_constrained_value(...) can run
and emit warnings.
codex-rs/core/src/skills/loader.rs (1)

1536-1579: ⚠️ Potential issue | 🟡 Minor

Test name is misleading.

The test loads_skill_macos_permissions_from_yaml_non_macos_does_not_create_profile asserts that the permission_profile is created with macos: Some(MacOsSeatbeltProfileExtensions { ... }), which contradicts the does_not_create_profile in the name.

If the intended behavior is that macOS permissions are now preserved on non-macOS platforms, consider renaming to something like loads_skill_macos_permissions_from_yaml_on_non_macos.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/skills/loader.rs` around lines 1536 - 1579, Rename the
misleading test function
loads_skill_macos_permissions_from_yaml_non_macos_does_not_create_profile to a
name that matches its assertions (e.g.,
loads_skill_macos_permissions_from_yaml_on_non_macos) and update the function
identifier wherever referenced; ensure the #[cfg(not(target_os = "macos"))]
attribute and any test helpers (load_skills_for_test, write_skill_metadata_at)
remain the same so behavior is unchanged.
codex-rs/core/src/seatbelt.rs (1)

267-287: ⚠️ Potential issue | 🟠 Major

Unix-socket-only configs can be silently dropped from network policy.

At Line 267, restricted mode does not consider unix-socket allowances. If ports is empty, has_proxy_config is false, and managed network is off, configured unix socket access won’t be emitted at all.

🔧 Proposed fix
 fn dynamic_network_policy(
     sandbox_policy: &SandboxPolicy,
     enforce_managed_network: bool,
     proxy: &ProxyPolicyInputs,
 ) -> String {
-    let should_use_restricted_network_policy =
-        !proxy.ports.is_empty() || proxy.has_proxy_config || enforce_managed_network;
+    let unix_socket_rules = unix_socket_policy(proxy);
+    let should_use_restricted_network_policy = !proxy.ports.is_empty()
+        || proxy.has_proxy_config
+        || enforce_managed_network
+        || !unix_socket_rules.is_empty();
     if should_use_restricted_network_policy {
         let mut policy = String::new();
         if proxy.allow_local_binding {
             policy.push_str("; allow loopback local binding and loopback traffic\n");
             policy.push_str("(allow network-bind (local ip \"localhost:*\"))\n");
             policy.push_str("(allow network-inbound (local ip \"localhost:*\"))\n");
             policy.push_str("(allow network-outbound (remote ip \"localhost:*\"))\n");
         }
         for port in &proxy.ports {
             policy.push_str(&format!(
                 "(allow network-outbound (remote ip \"localhost:{port}\"))\n"
             ));
         }
-        let unix_socket_policy = unix_socket_policy(proxy);
-        if !unix_socket_policy.is_empty() {
+        if !unix_socket_rules.is_empty() {
             policy.push_str("; allow unix domain sockets for local IPC\n");
-            policy.push_str(&unix_socket_policy);
+            policy.push_str(&unix_socket_rules);
         }
         return format!("{policy}{MACOS_SEATBELT_NETWORK_POLICY}");
     }

Please also add a regression test for “unix sockets configured with no proxy ports/env”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/seatbelt.rs` around lines 267 - 287, The current
restricted-policy gate (should_use_restricted_network_policy) ignores unix
socket allowances when proxy.ports is empty and proxy.has_proxy_config and
enforce_managed_network are false, so unix_socket_policy(proxy) can be dropped;
change the condition to also enter the policy block when
unix_socket_policy(proxy) is non-empty (call unix_socket_policy before the if or
compute a bool unix_allowed) and ensure its contents are appended and returned
alongside MACOS_SEATBELT_NETWORK_POLICY; update the code paths using
should_use_restricted_network_policy and the function that builds the policy
(the block that returns format!("{policy}{MACOS_SEATBELT_NETWORK_POLICY}")) to
include unix socket policy even when ports/config flags are absent, and add a
regression test that creates a proxy with unix socket config but no ports/env to
assert the generated policy contains the unix socket rules.
codex-rs/core/src/client_common.rs (1)

170-180: ⚠️ Potential issue | 🟠 Major

Fix API compatibility and type safety for tool fields.

output_format should use a typed enum with lowercase values (png, jpeg, webp per API docs). More critically, search_content_types is not documented in the OpenAI Responses API—it may be an unsupported field causing the web_search errors noted in the TODO comment on line 171. Remove this field or verify it's supported before merging.

♻️ Proposed refactor
+    #[derive(Debug, Clone, Serialize, PartialEq)]
+    #[serde(rename_all = "lowercase")]
+    pub(crate) enum ImageOutputFormat {
+        Png,
+        Jpeg,
+        Webp,
+    }
+
     #[derive(Debug, Clone, Serialize, PartialEq)]
     #[serde(tag = "type")]
     pub(crate) enum ToolSpec {
@@
         #[serde(rename = "image_generation")]
-        ImageGeneration { output_format: String },
+        ImageGeneration { output_format: ImageOutputFormat },
@@
         #[serde(rename = "web_search")]
         WebSearch {
             #[serde(skip_serializing_if = "Option::is_none")]
             external_web_access: Option<bool>,
-            #[serde(skip_serializing_if = "Option::is_none")]
-            search_content_types: Option<Vec<String>>,
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/client_common.rs` around lines 170 - 180, The
ImageGeneration variant currently uses a String for output_format and the
WebSearch variant includes an undocumented search_content_types field; change
ImageGeneration::output_format to a typed enum (e.g., OutputFormat with
lowercase variants "png", "jpeg", "webp") to enforce valid values and remove (or
gate behind verification) the WebSearch::search_content_types field—keep only
external_web_access in the WebSearch struct to avoid sending unsupported fields
to the Responses API (update serde rename/attributes accordingly and adapt any
code that constructs or matches on these variants).
codex-rs/core/src/mcp_connection_manager.rs (1)

331-356: ⚠️ Potential issue | 🔴 Critical

Handle elicitation event send failures to avoid stuck awaits.

At Line 336, send failure is ignored, but Line 354 still awaits the oneshot response while its sender remains stored in the pending map. That can block indefinitely.

🐛 Proposed fix
                 let (tx, rx) = oneshot::channel();
+                let key = (server_name.clone(), id.clone());
                 {
                     let mut lock = elicitation_requests.lock().await;
-                    lock.insert((server_name.clone(), id.clone()), tx);
+                    lock.insert(key.clone(), tx);
                 }
-                let _ = tx_event
+                if let Err(err) = tx_event
                     .send(Event {
                         id: "mcp_elicitation_request".to_string(),
                         msg: EventMsg::ElicitationRequest(ElicitationRequestEvent {
                             turn_id: None,
                             server_name,
@@
                             request,
                         }),
                     })
-                    .await;
+                    .await
+                {
+                    let mut lock = elicitation_requests.lock().await;
+                    lock.remove(&key);
+                    return Err(anyhow!("failed to dispatch elicitation request: {err}"));
+                }
                 rx.await
                     .context("elicitation request channel closed unexpectedly")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/mcp_connection_manager.rs` around lines 331 - 356, The code
stores a oneshot sender in elicitation_requests and then ignores failures from
tx_event.send, which can leave the sender in the map and make rx.await block
forever; update the block around elicitation_requests, tx_event.send and
rx.await so that you check the result of tx_event.send(Event { ... }) and if it
Errs remove the previously inserted sender from elicitation_requests (using the
same key (server_name.clone(), id.clone())) and return/propagate an error
instead of awaiting rx; ensure the map removal happens under the same async lock
to avoid races and that you do not leave the sender alive in the map when send
fails (refer to elicitation_requests, tx_event.send, tx/rx oneshot pair, and
ProtocolRequestId conversion to locate the code).
codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json (1)

3265-3289: ⚠️ Potential issue | 🟠 Major

elicitation_request event shape changed incompatibly (messagerequest).

Making request required without a compatibility alias can break existing clients that still decode the previous field. Keep both during migration (accept either, emit canonical one).

Transitional compatibility pattern
           "properties": {
             "id": {
               "$ref": "#/definitions/v2/RequestId"
             },
+            "message": {
+              "$ref": "#/definitions/ElicitationRequest"
+            },
             "request": {
               "$ref": "#/definitions/ElicitationRequest"
             },
@@
-          "required": [
-            "id",
-            "request",
-            "server_name",
-            "type"
-          ],
+          "anyOf": [
+            { "required": ["id", "request", "server_name", "type"] },
+            { "required": ["id", "message", "server_name", "type"] }
+          ],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json`
around lines 3265 - 3289, The schema now breaks compatibility by renaming
property "message" -> "request" and making "request" required; restore
backward-compatibility by keeping both properties and requiring at least one. In
the object that defines the elicitation event (the schema with enum
"ElicitationRequestEventMsgType" and properties "request" and "server_name"),
add a "message" property with the same schema as "request" (e.g., $ref to
"#/definitions/ElicitationRequest"), remove the unconditional "request" from
"required", and add a oneOf (or anyOf) constraint such as
[{"required":["request"]},{"required":["message"]}] so either field is accepted.
Also note producers should emit the canonical "request" field going forward.
codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json (1)

4851-4875: ⚠️ Potential issue | 🟠 Major

ElicitationRequestEventMsg introduces a wire-compatibility break.

Line 4874 now requires request and no legacy message fallback exists. Existing clients that only understand the old shape will fail to decode this event.

Proposed compatibility-safe schema adjustment
         {
           "properties": {
             "id": {
               "$ref": "#/definitions/RequestId"
             },
+            "message": {
+              "type": "string"
+            },
             "request": {
               "$ref": "#/definitions/ElicitationRequest"
             },
             "server_name": {
               "type": "string"
             },
             "turn_id": {
               "description": "Turn ID that this elicitation belongs to, when known.",
               "type": [
                 "string",
                 "null"
               ]
             },
             "type": {
               "enum": [
                 "elicitation_request"
               ],
               "title": "ElicitationRequestEventMsgType",
               "type": "string"
             }
           },
-          "required": [
-            "id",
-            "request",
-            "server_name",
-            "type"
-          ],
+          "required": ["id", "server_name", "type"],
+          "allOf": [
+            {
+              "anyOf": [
+                { "required": ["request"] },
+                { "required": ["message"] }
+              ]
+            }
+          ],
           "title": "ElicitationRequestEventMsg",
           "type": "object"
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json`
around lines 4851 - 4875, The schema change to ElicitationRequestEventMsg
currently makes "request" mandatory and drops the legacy "message" field,
breaking wire compatibility; update the schema for the
ElicitationRequestEventMsg definition so decoders accept either the new
"request" or the old "message" shape (e.g., remove "request" from the strict
"required" list and add an anyOf/oneOf clause that enforces presence of at least
one of ["request","message"] or otherwise allow "message" as an alternative),
ensuring the "request" property still references
`#/definitions/ElicitationRequest` and the legacy "message" property (with its
original schema) is preserved to support old clients.
♻️ Duplicate comments (6)
codex-rs/app-server/src/app_server_tracing.rs (1)

89-95: ⚠️ Potential issue | 🟠 Major

Use real typed RPC method names in in-process span fields.

Line 92 and Line 94 still hardcode "in_process", which collapses all in-process calls into one telemetry bucket. Please derive method names from ClientRequest so traces/metrics preserve per-RPC granularity.

Suggested change
 pub(crate) fn typed_request_span(
     request: &ClientRequest,
     connection_id: ConnectionId,
     session: &ConnectionSessionState,
 ) -> Span {
+    let method = typed_request_method(request);
     let span = info_span!(
         "app_server.request",
         otel.kind = "server",
-        otel.name = "in_process",
+        otel.name = method,
         rpc.system = "jsonrpc",
-        rpc.method = "in_process",
+        rpc.method = method,
         rpc.transport = "in-process",
         rpc.request_id = ?client_request_id(request),
         app_server.connection_id = ?connection_id,
         app_server.api_version = "v2",
         app_server.client_name = field::Empty,
         app_server.client_version = field::Empty,
     );
fn typed_request_method(request: &ClientRequest) -> &'static str {
    match request {
        ClientRequest::Initialize { .. } => "initialize",
        // ...exhaustive mapping for the remaining variants...
        ClientRequest::FuzzyFileSearchSessionStop { .. } => "fuzzyFileSearchSession/stop",
    }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/src/app_server_tracing.rs` around lines 89 - 95, The span
currently hardcodes "in_process" for otel.name and rpc.method; create a helper
fn like typed_request_method(request: &ClientRequest) -> &'static str that
returns the proper RPC method string for each ClientRequest variant (e.g.,
ClientRequest::Initialize => "initialize", etc.), then call that helper when
building the span (use the returned &str for both otel.name and rpc.method in
the info_span! invocation) so in-process spans preserve per-RPC granularity.
codex-rs/app-server-protocol/schema/json/McpServerElicitationRequestParams.json (1)

603-606: ⚠️ Potential issue | 🟠 Major

Schema/type drift remains for turnId requiredness.

Line 603–606 omits turnId from required, while generated TS models turnId as mandatory (string | null). This lets schema validation accept payloads that typed clients reject.

Proposed schema fix
   "required": [
     "serverName",
-    "threadId"
+    "threadId",
+    "turnId"
   ],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@codex-rs/app-server-protocol/schema/json/McpServerElicitationRequestParams.json`
around lines 603 - 606, The JSON schema McpServerElicitationRequestParams.json
currently lists only "serverName" and "threadId" as required but the generated
TypeScript models treat "turnId" as mandatory (string | null); update the schema
by adding "turnId" to the "required" array so runtime validation matches the TS
type, and re-generate/verify any codegen to ensure the models and schema no
longer drift (refer to the "required" array and the "turnId" property in the
same schema).
codex-rs/app-server/src/lib.rs (1)

340-346: ⚠️ Potential issue | 🟠 Major

session_source is still hard-coded to VSCode.

Line 607 forces SessionSource::VSCode, which can misclassify non-VSCode callers of run_main_with_transport and skew routing/telemetry semantics. Thread this through the runtime entrypoint instead of fixing it here.

Proposed fix
 pub async fn run_main(
     arg0_paths: Arg0DispatchPaths,
     cli_config_overrides: CliConfigOverrides,
     loader_overrides: LoaderOverrides,
     default_analytics_enabled: bool,
 ) -> IoResult<()> {
     run_main_with_transport(
         arg0_paths,
         cli_config_overrides,
         loader_overrides,
         default_analytics_enabled,
         AppServerTransport::Stdio,
+        SessionSource::VSCode,
     )
     .await
 }

 pub async fn run_main_with_transport(
     arg0_paths: Arg0DispatchPaths,
     cli_config_overrides: CliConfigOverrides,
     loader_overrides: LoaderOverrides,
     default_analytics_enabled: bool,
     transport: AppServerTransport,
+    session_source: SessionSource,
 ) -> IoResult<()> {
@@
         let mut processor = MessageProcessor::new(MessageProcessorArgs {
@@
-            session_source: SessionSource::VSCode,
+            session_source,
         });

Also applies to: 597-608

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/src/lib.rs` around lines 340 - 346, The code currently
forces SessionSource::VSCode inside run_main_with_transport, misclassifying
callers; update the function signature for run_main_with_transport to accept a
SessionSource parameter (or an Option<SessionSource>) and replace the hard-coded
SessionSource::VSCode use with that parameter, then propagate the new parameter
from the runtime entrypoint that calls run_main_with_transport so callers supply
the correct SessionSource; locate uses of run_main_with_transport and the place
where SessionSource::VSCode is created and remove the hard-coded value so
routing/telemetry see the threaded-in session_source instead.
codex-rs/app-server/src/message_processor.rs (1)

327-338: ⚠️ Potential issue | 🟠 Major

Redact inbound payload logs to avoid leaking auth and user data.

Line 330, Line 337, Line 375, and Line 382 still emit full protocol objects. These paths can carry sensitive fields and should log only safe metadata.

💡 Suggested fix
 pub(crate) async fn process_notification(&self, notification: JSONRPCNotification) {
-    // Currently, we do not expect to receive any notifications from the
-    // client, so we just log them.
-    tracing::info!("<- notification: {:?}", notification);
+    tracing::debug!(method = %notification.method, "<- notification received");
 }

 /// Handles typed notifications from in-process clients.
 pub(crate) async fn process_client_notification(&self, notification: ClientNotification) {
-    // Currently, we do not expect to receive any typed notifications from
-    // in-process clients, so we just log them.
-    tracing::info!("<- typed notification: {:?}", notification);
+    let _ = notification;
+    tracing::debug!("<- typed notification received (payload omitted)");
 }

 /// Handle a standalone JSON-RPC response originating from the peer.
 pub(crate) async fn process_response(&mut self, response: JSONRPCResponse) {
-    tracing::info!("<- response: {:?}", response);
     let JSONRPCResponse { id, result, .. } = response;
+    tracing::debug!(response_id = ?id, "<- response received");
     self.outgoing.notify_client_response(id, result).await
 }

 /// Handle an error object received from the peer.
 pub(crate) async fn process_error(&mut self, err: JSONRPCError) {
-    tracing::error!("<- error: {:?}", err);
-    self.outgoing.notify_client_error(err.id, err.error).await;
+    let JSONRPCError { id, error, .. } = err;
+    tracing::warn!(response_id = ?id, code = error.code, "<- error received");
+    self.outgoing.notify_client_error(id, error).await;
 }

Also applies to: 374-383

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/src/message_processor.rs` around lines 327 - 338, The two
logging sites (process_notification and process_client_notification) currently
log full inbound payloads and must be changed to redact sensitive fields; update
tracing::info! in process_notification and process_client_notification so they
do not print entire JSONRPCNotification or ClientNotification objects but
instead log only safe metadata such as the notification type/method, an optional
id or timestamp, and a truncated/omitted params payload (e.g., replace params
with "(redacted)" or log its size/type), ensuring any auth/user fields are never
included; locate the tracing::info! calls in those two functions and replace
their arguments with a small struct or formatted string containing only the
allowed metadata.
codex-rs/app-server/src/in_process.rs (1)

256-263: ⚠️ Potential issue | 🟠 Major

Bound the shutdown enqueue wait to avoid a potential hang.

At Line 256, client_tx.send(...).await can block indefinitely on a full bounded channel, so the timeout/abort path at Line 265 may never run.

🔧 Proposed fix
-        if self
-            .client_tx
-            .send(InProcessClientMessage::Shutdown { done_tx })
-            .await
-            .is_ok()
-        {
+        let shutdown_enqueued = timeout(
+            SHUTDOWN_TIMEOUT,
+            self.client_tx.send(InProcessClientMessage::Shutdown { done_tx }),
+        )
+        .await
+        .is_ok_and(|result| result.is_ok());
+        if shutdown_enqueued {
             let _ = timeout(SHUTDOWN_TIMEOUT, done_rx).await;
         }
#!/bin/bash
set -euo pipefail

# Verify shutdown currently does an unbounded await on enqueue.
sed -n '248,270p' codex-rs/app-server/src/in_process.rs

# Verify the client queue is bounded.
sed -n '314,321p' codex-rs/app-server/src/in_process.rs

# Confirm no timeout wrapper exists around this specific send.
rg -n 'client_tx\.send\(InProcessClientMessage::Shutdown|timeout\(SHUTDOWN_TIMEOUT,\s*self\.client_tx\.send' codex-rs/app-server/src/in_process.rs
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/src/in_process.rs` around lines 256 - 263, The shutdown
path currently awaits self.client_tx.send(InProcessClientMessage::Shutdown {
done_tx }).await which can block indefinitely on a full bounded channel; wrap
the enqueue in a timeout or use a non-blocking send to ensure the overall
shutdown timeout (SHUTDOWN_TIMEOUT) can trigger: replace the direct
.send(...).await with a timeout(SHUTDOWN_TIMEOUT,
self.client_tx.send(...)).await (or attempt try_send and treat
Err(TrySendError::Full) as a timeout/failure), and only await done_rx if the
enqueue succeeded; update handling around InProcessClientMessage::Shutdown,
client_tx, SHUTDOWN_TIMEOUT, and done_tx/done_rx accordingly so the shutdown
path cannot hang forever.
codex-rs/app-server-protocol/src/protocol/v2.rs (1)

4457-4460: ⚠️ Potential issue | 🟠 Major

Force TS number for JSON-facing minItems/maxItems bounds.

Lines 4457/4460/4482/4485 use Option<u64>. With ts-rs, this typically emits bigint, which is not JSON-compatible for these schema fields.

🔧 Proposed fix
 pub struct McpElicitationUntitledMultiSelectEnumSchema {
@@
-    #[ts(optional)]
+    #[ts(optional, type = "number")]
     pub min_items: Option<u64>,
@@
-    #[ts(optional)]
+    #[ts(optional, type = "number")]
     pub max_items: Option<u64>,
@@
 pub struct McpElicitationTitledMultiSelectEnumSchema {
@@
-    #[ts(optional)]
+    #[ts(optional, type = "number")]
     pub min_items: Option<u64>,
@@
-    #[ts(optional)]
+    #[ts(optional, type = "number")]
     pub max_items: Option<u64>,
#!/bin/bash
set -euo pipefail

# Show the Rust fields currently typed as Option<u64>.
rg -n -C2 'pub (min_items|max_items): Option<u64>' codex-rs/app-server-protocol/src/protocol/v2.rs

# Inspect generated TypeScript for emitted field types.
fd 'McpElicitation(UntitledMultiSelectEnumSchema|TitledMultiSelectEnumSchema)\.ts' codex-rs/app-server-protocol/schema/typescript/v2 \
  | xargs -I{} sh -c 'echo "---- {}"; rg -n "minItems|maxItems|bigint|number" "{}"'

Also applies to: 4482-4485

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server-protocol/src/protocol/v2.rs` around lines 4457 - 4460,
The TypeScript emit treats Rust u64 as bigint; to force JSON-compatible number
types, add the ts-rs attribute #[ts(type = "number")] to the min_items and
max_items fields (instead of changing their Rust type) in the relevant structs
(e.g., UntitledMultiSelectEnumSchema and TitledMultiSelectEnumSchema and the
other occurrences), i.e., place #[ts(optional)] and #[ts(type = "number")] (and
keep serde(skip_serializing_if = "Option::is_none")) above pub min_items:
Option<u64> and pub max_items: Option<u64> so generated .ts uses number for
these schema bounds.
🧹 Nitpick comments (17)
codex-rs/app-server-client/README.md (1)

64-65: Clarify what exactly is aborted on shutdown timeout.

“aborts” can be read as process-level termination. Suggest explicitly naming the aborted unit (e.g., worker task/runtime loop) to avoid operational misinterpretation.

✏️ Suggested wording
-- `shutdown()` performs a bounded graceful shutdown and then aborts if timeout
-  is exceeded.
+- `shutdown()` performs a bounded graceful shutdown and then aborts the
+  in-process worker task if the timeout is exceeded.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server-client/README.md` around lines 64 - 65, The README's
description of shutdown() is ambiguous about what "aborts" — update the sentence
to explicitly name the aborted unit (for example: "aborts the worker task",
"aborts the runtime loop", or "terminates the process") and choose the correct
one that matches implementation; edit the `shutdown()` description to read
something like "shutdown() performs a bounded graceful shutdown and aborts the
worker runtime loop if the timeout is exceeded" (or "aborts the process" if that
is the actual behavior) so readers know exactly what is being terminated.
codex-rs/core/src/sandboxing/macos_permissions.rs (1)

78-79: Optional: run these pure merge tests on all targets.

Line 78 restricts tests to macOS, but the helpers here are pure merge logic and can be validated cross-platform for wider CI coverage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/sandboxing/macos_permissions.rs` around lines 78 - 79, The
tests module is unnecessarily restricted to macOS by the attribute
#[cfg(all(test, target_os = "macos"))]; change it to run on all test targets by
replacing that attribute with #[cfg(test)] (or removing the target_os condition)
so the module tests (mod tests) and its helper functions for pure merge logic
execute on CI across platforms.
codex-rs/core/src/tools/handlers/mod.rs (1)

98-99: Either use or remove _cwd to avoid interface drift.

Line 98 adds _cwd, but it is currently unused in validation/normalization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/tools/handlers/mod.rs` around lines 98 - 99, The added
parameter `_cwd: &Path` is unused and causing interface drift; either remove
`_cwd` from the function signature that returns
Result<Option<PermissionProfile>, String> and update all callers to stop passing
a cwd, or actually use `_cwd` in the function's validation/normalization logic
(e.g., resolve relative paths, check file existence, or derive context for
PermissionProfile) and ensure the parameter name is not prefixed with `_` if
used; update any caller sites accordingly to match the chosen approach.
codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json (1)

9-11: Consider a stricter path type for response fields.

Line 9 and Line 30 currently accept arbitrary strings. Reusing an absolute-path schema type here would keep request/response path guarantees consistent.

Also applies to: 30-32

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json` around
lines 9 - 11, The response schema uses plain string types for file paths (the
"path" properties in PluginListResponse.json); replace those loose "type:
string" declarations with a reference to the existing absolute-path schema (i.e.
use the shared absolute-path schema/$ref instead of a raw string) so
request/response path validation is consistent — update both the top-level
"path" field and the other occurrence around lines 30-32 to point to the
absolute-path schema.
codex-rs/app-server/src/codex_message_processor.rs (1)

3925-3930: Optional: centralize repeated config-reload error handling.

Line 3925 (and the same pattern at Lines 4040, 4099, 4205, 4628, 5005, 5184) repeats load_latest_config(...)+send_error+return. A small helper would reduce drift and keep request handlers tighter.

♻️ Proposed refactor
+    async fn load_latest_config_or_send_error(
+        &self,
+        request_id: ConnectionRequestId,
+        fallback_cwd: Option<PathBuf>,
+    ) -> Option<Config> {
+        match self.load_latest_config(fallback_cwd).await {
+            Ok(config) => Some(config),
+            Err(error) => {
+                self.outgoing.send_error(request_id, error).await;
+                None
+            }
+        }
+    }
-        let config = match self.load_latest_config(None).await {
-            Ok(config) => config,
-            Err(error) => {
-                self.outgoing.send_error(request_id, error).await;
-                return;
-            }
-        };
+        let Some(config) = self
+            .load_latest_config_or_send_error(request_id, None)
+            .await
+        else {
+            return;
+        };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/src/codex_message_processor.rs` around lines 3925 - 3930,
Several handlers repeat the pattern of calling self.load_latest_config(...),
sending an error via self.outgoing.send_error(request_id, error).await and
returning; extract this into a small helper on the struct (e.g., a method like
try_load_latest_config(&self, request_id) -> Result<Config, ()> or
load_config_or_send_error(&self, request_id) -> Option<Config>) and replace
direct calls at sites using load_latest_config, so callers call the helper
(referencing load_latest_config, outgoing.send_error, and request_id) and either
get the Config or early-return when the helper already sent the error.
codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json (1)

11784-11786: Use AbsolutePathBuf consistently for plugin marketplace/local paths.

plugin/install requires a normalized absolute path, but list/source path fields are plain strings. Aligning these types prevents handoff mismatches from plugin/list to plugin/install.

Type-consistency patch
       "PluginMarketplaceEntry": {
         "properties": {
@@
           "path": {
-            "type": "string"
+            "$ref": "#/definitions/v2/AbsolutePathBuf"
           },
@@
       "PluginSource": {
         "oneOf": [
           {
             "properties": {
               "path": {
-                "type": "string"
+                "$ref": "#/definitions/v2/AbsolutePathBuf"
               },

Also applies to: 11852-11854, 11873-11875

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json`
around lines 11784 - 11786, Several plugin-related path fields are plain strings
but must be normalized absolute paths to match plugin/install expectations;
update the JSON schema so fields like "marketplacePath" (and the other
occurrences noted around the 11852–11854 and 11873–11875 regions) reference the
v2/AbsolutePathBuf definition instead of a plain string. Locate the plugin/list
and plugin/source (or list/source path) schema entries and replace their string
type or $ref with "$ref": "#/definitions/v2/AbsolutePathBuf" so plugin/list
outputs and plugin/install inputs use the same AbsolutePathBuf type.
codex-rs/core/src/sandboxing/mod.rs (1)

458-473: Consider deduplicating execution wrapper logic.

Line 462 and Line 471 repeat the same clone-and-dispatch flow. A small private helper would reduce drift risk.

Refactor sketch
+async fn execute_with_effective_policy(
+    exec_request: ExecRequest,
+    stdout_stream: Option<StdoutStream>,
+    after_spawn: Option<Box<dyn FnOnce() + Send>>,
+) -> crate::error::Result<ExecToolCallOutput> {
+    let effective_policy = exec_request.sandbox_policy.clone();
+    execute_exec_request(exec_request, &effective_policy, stdout_stream, after_spawn).await
+}
+
 pub async fn execute_env(
     exec_request: ExecRequest,
     stdout_stream: Option<StdoutStream>,
 ) -> crate::error::Result<ExecToolCallOutput> {
-    let effective_policy = exec_request.sandbox_policy.clone();
-    execute_exec_request(exec_request, &effective_policy, stdout_stream, None).await
+    execute_with_effective_policy(exec_request, stdout_stream, None).await
 }
 
 pub async fn execute_exec_request_with_after_spawn(
     exec_request: ExecRequest,
     stdout_stream: Option<StdoutStream>,
     after_spawn: Option<Box<dyn FnOnce() + Send>>,
 ) -> crate::error::Result<ExecToolCallOutput> {
-    let effective_policy = exec_request.sandbox_policy.clone();
-    execute_exec_request(exec_request, &effective_policy, stdout_stream, after_spawn).await
+    execute_with_effective_policy(exec_request, stdout_stream, after_spawn).await
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/sandboxing/mod.rs` around lines 458 - 473, Both execute_env
and execute_exec_request_with_after_spawn duplicate the clone-and-dispatch logic
for sandbox_policy; extract that into a small private helper (e.g., fn
dispatch_with_policy(exec_request: ExecRequest, stdout_stream:
Option<StdoutStream>, after_spawn: Option<Box<dyn FnOnce() + Send>>) -> impl
Future<Output = crate::error::Result<ExecToolCallOutput>> or an async fn) that
clones exec_request.sandbox_policy into effective_policy and calls
execute_exec_request(exec_request, &effective_policy, stdout_stream,
after_spawn). Then have execute_env and execute_exec_request_with_after_spawn
call this helper, passing None or the provided after_spawn respectively to
remove the duplicated clone-and-dispatch flow.
codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json (3)

14071-14076: Align saved_path across image-generation item schemas.

saved_path is now present on TurnItem.ImageGeneration, but the ThreadItem.imageGeneration shape still cannot represent it. Keeping both aligned avoids field loss between live and persisted/readback views.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json`
around lines 14071 - 14076, ThreadItem.imageGeneration schema is missing the
saved_path property that exists on TurnItem.ImageGeneration, causing loss when
persisting/reading back image-generation items; update the
ThreadItem.imageGeneration JSON schema to include saved_path with the same type
union ["string","null"] (mirror the property definition from
TurnItem.ImageGeneration) so both shapes align, and ensure any accompanying
required/optional lists match TurnItem.ImageGeneration's treatment of
saved_path.

8509-8511: Use AbsolutePathBuf for plugin path fields for contract consistency.

PluginMarketplaceEntry.path and PluginSource.path are plain string, while PluginInstallParams.marketplacePath is AbsolutePathBuf. Aligning these prevents round-trip type drift.

Suggested path typing alignment
     "PluginMarketplaceEntry": {
       "properties": {
         "name": {
           "type": "string"
         },
         "path": {
-          "type": "string"
+          "$ref": "#/definitions/AbsolutePathBuf"
         },
         "plugins": {
           "items": {
             "$ref": "#/definitions/PluginSummary"
           },
           "type": "array"
         }
       },
@@
     "PluginSource": {
       "oneOf": [
         {
           "properties": {
             "path": {
-              "type": "string"
+              "$ref": "#/definitions/AbsolutePathBuf"
             },
             "type": {
               "enum": [
                 "local"
               ],

Also applies to: 8530-8532

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json`
around lines 8509 - 8511, Change the JSON schema for the plugin path fields from
a plain string to the AbsolutePathBuf type to keep contracts consistent and
avoid round-trip drift: update the "path" property in PluginMarketplaceEntry
(schema name PluginMarketplaceEntry.path) and PluginSource (PluginSource.path)
to use the AbsolutePathBuf schema/type (the same type used by
PluginInstallParams.marketplacePath), ensuring any $ref or type declaration
matches the existing AbsolutePathBuf schema definition used elsewhere in the
spec.

8441-8450: Consider a temporary compatibility alias for marketplacePath.

Line 8449 makes marketplacePath mandatory immediately. If any existing v2 clients still send the previous field, this will hard-fail requests. A short transition window alias can reduce rollout risk.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json`
around lines 8441 - 8450, Add a short-lived compatibility alias for the previous
marketplace field so v2 doesn't hard-fail: introduce a new optional property
(e.g., legacyMarketplacePath) with the same "$ref":
"#/definitions/AbsolutePathBuf" alongside "marketplacePath" and then replace the
strict "required": ["marketplacePath","pluginName"] with a oneOf/anyOf that
enforces "pluginName" plus either "marketplacePath" or the legacy property
(e.g., oneOf:
[{required:["marketplacePath","pluginName"]},{required:["legacyMarketplacePath","pluginName"]}]).
This preserves the existing AbsolutePathBuf typing and allows a transition
window before removing the alias.
codex-rs/app-server-client/Cargo.toml (1)

22-22: Consider removing redundant serde_json in dev-dependencies.

serde_json is already included in [dependencies] (line 22), so the entry in [dev-dependencies] (line 29) is redundant. While harmless, removing it would reduce manifest noise.

♻️ Suggested fix
 [dev-dependencies]
 pretty_assertions = { workspace = true }
-serde_json = { workspace = true }
 tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }

Also applies to: 29-29

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server-client/Cargo.toml` at line 22, Remove the redundant
serde_json entry from the [dev-dependencies] section of Cargo.toml since
serde_json is already declared in [dependencies]; locate the serde_json key
under the dev-dependencies table and delete that entry to avoid duplicate
manifest noise while keeping the primary serde_json dependency in place.
codex-rs/app-server/tests/suite/v2/app_list.rs (1)

87-1135: Consider centralizing AppInfo test fixture construction.

There is substantial repeated literal construction; a small helper/builder would reduce churn when AppInfo evolves again.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/tests/suite/v2/app_list.rs` around lines 87 - 1135, Tests
repeatedly construct AppInfo literals; extract a centralized builder/helper to
reduce duplication: create a helper function or struct (e.g., app_info_fixture
or AppInfoBuilder) used by tests like list_apps_reports_is_enabled_from_config,
list_apps_emits_updates_and_returns_after_both_lists_load,
list_apps_waits_for_accessible_data_before_emitting_directory_updates, etc., to
produce common variants (accessible/inaccessible, with install_url, with
branding/metadata) and update usages of raw AppInfo { ... } instances and
expected_* vectors to call the builder with succinct methods (e.g.,
.id("beta").name("Beta").accessible(true).install_url(...)) so changes to
AppInfo fields only require updates in one place.
codex-rs/app-server/README.md (1)

157-157: Tighten phrasing for readability.

Line 157 can be shortened to “inaccessible in the current ChatGPT auth context” to reduce wordiness without changing meaning.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/README.md` at line 157, Update the README entry for
`plugin/install` to tighten the phrasing: replace the clause "that still are not
accessible in the current ChatGPT auth context" with "inaccessible in the
current ChatGPT auth context" so the description for the returned
`appsNeedingAuth` is more concise while preserving meaning.
codex-rs/core/src/connectors.rs (2)

432-441: Avoid overwriting existing plugin sources during enrichment.

Line [437]-Line [439] replaces connector.plugin_display_names outright. If provenance is partial, this can drop names already collected from ToolInfo. Prefer union + dedupe.

Suggested fix (merge instead of replace)
 pub fn with_app_plugin_sources(
     mut connectors: Vec<AppInfo>,
     tool_plugin_provenance: &ToolPluginProvenance,
 ) -> Vec<AppInfo> {
     for connector in &mut connectors {
-        connector.plugin_display_names = tool_plugin_provenance
-            .plugin_display_names_for_connector_id(connector.id.as_str())
-            .to_vec();
+        let mut merged: BTreeSet<String> = connector.plugin_display_names.drain(..).collect();
+        merged.extend(
+            tool_plugin_provenance
+                .plugin_display_names_for_connector_id(connector.id.as_str())
+                .iter()
+                .cloned(),
+        );
+        connector.plugin_display_names = merged.into_iter().collect();
     }
     connectors
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/connectors.rs` around lines 432 - 441,
with_app_plugin_sources currently replaces connector.plugin_display_names with
the provenance list, dropping any existing names from ToolInfo; instead, fetch
the provenance names via
tool_plugin_provenance.plugin_display_names_for_connector_id(connector.id.as_str()),
merge them with the existing connector.plugin_display_names (append or extend),
and deduplicate the combined list (e.g., use a HashSet or preserve order and
filter duplicates) before assigning back to connector.plugin_display_names so
you perform a union rather than an overwrite.

305-307: Comment is out of sync with behavior.

The note says no separate enrichment pass is required, but this file still performs enrichment passes (Line [136] and Line [222]-Line [223]). Updating the comment will avoid future confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/connectors.rs` around lines 305 - 307, The existing comment
above the "let tools = mcp_tools.values().filter_map(|tool|" line incorrectly
states that a separate enrichment pass is not required; update that comment to
reflect current behavior: acknowledge that while ToolInfo contains plugin
provenance, this module still performs enrichment passes (see the earlier and
later enrichment logic operating on mcp_tools/ToolInfo), so the comment should
not assert enrichment is unnecessary and should instead document that additional
enrichment is applied within this file for completeness and downstream needs.
codex-rs/core/src/codex.rs (2)

4648-4670: Avoid taking services.rollout lock twice during rollback preflight.

Line 4648 and Line 4666 acquire separate snapshots of the recorder. Reusing one snapshot avoids lock churn and prevents split-brain behavior if recorder state changes between reads.

♻️ Suggested refactor
-        let rollout_path = {
-            let recorder = {
-                let guard = sess.services.rollout.lock().await;
-                guard.clone()
-            };
-            let Some(recorder) = recorder else {
+        let recorder = {
+            let guard = sess.services.rollout.lock().await;
+            guard.clone()
+        };
+        let Some(recorder) = recorder else {
                 sess.send_event_raw(Event {
                     id: turn_context.sub_id.clone(),
                     msg: EventMsg::Error(ErrorEvent {
                         message: "thread rollback requires a persisted rollout path".to_string(),
                         codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed),
                     }),
                 })
                 .await;
                 return;
-            };
-            recorder.rollout_path().to_path_buf()
         };
-        if let Some(recorder) = {
-            let guard = sess.services.rollout.lock().await;
-            guard.clone()
-        } && let Err(err) = recorder.flush().await
-        {
+        let rollout_path = recorder.rollout_path().to_path_buf();
+        if let Err(err) = recorder.flush().await {
             sess.send_event_raw(Event {
                 id: turn_context.sub_id.clone(),
                 msg: EventMsg::Error(ErrorEvent {
                     message: format!(
                         "failed to flush rollout `{}` for rollback replay: {err}",
                         rollout_path.display()
                     ),
                     codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed),
                 }),
             })
             .await;
             return;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/codex.rs` around lines 4648 - 4670, The code currently
locks sess.services.rollout twice and clones two separate recorder snapshots;
capture the recorder once, reuse it for both computing rollout_path and later
flush to avoid lock churn and inconsistent state. Specifically, when you obtain
recorder (the Option from sess.services.rollout.lock().await) use that single
cloned recorder variable for calling recorder.rollout_path().to_path_buf() and
later for recorder.flush().await, and remove the second lock/clone block; keep
the early None branch that sends the ErrorEvent unchanged but ensure all
subsequent uses reference the same recorder variable.

5130-5147: Consider reusing one MCP tools snapshot per turn.

run_turn fetches mcp_tools for plugin injection, and built_tools fetches all tools again. Reusing a per-turn snapshot can reduce repeated manager reads on hot paths.

Also applies to: 5877-5891

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/codex.rs` around lines 5130 - 5147, run_turn currently
calls the MCP manager twice: once to populate mcp_tools and again inside
built_tools; instead capture a single per-turn snapshot by calling
sess.services.mcp_connection_manager.read().await.list_all_tools().or_cancel(&cancellation_token).await
once (as you already do for mcp_tools) and stash that HashMap in a local
variable (e.g., mcp_tools_snapshot) or attach it to turn_context, then pass that
snapshot into any downstream logic that previously called list_all_tools()
(notably the built_tools construction/function) so those callers reuse the
snapshot instead of re-reading the manager; update references to
mcp_tools/built_tools to accept the snapshot parameter and keep the same error
handling semantics (return None or empty HashMap) used in the original mcp_tools
branch.

Comment on lines +1790 to +1802
"properties": {
"_meta": true,
"message": {
"type": "string"
},
"mode": {
"enum": [
"form"
],
"type": "string"
},
"requested_schema": true
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Type requested_schema instead of allowing any JSON.

requested_schema: true removes structural validation on this path, while the rest of the elicitation surface is strongly typed. Reusing McpElicitationSchema here would keep behavior consistent.

Suggested schema tightening
-            "requested_schema": true
+            "requested_schema": {
+              "$ref": "#/definitions/McpElicitationSchema"
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"properties": {
"_meta": true,
"message": {
"type": "string"
},
"mode": {
"enum": [
"form"
],
"type": "string"
},
"requested_schema": true
},
"properties": {
"_meta": true,
"message": {
"type": "string"
},
"mode": {
"enum": [
"form"
],
"type": "string"
},
"requested_schema": {
"$ref": "#/definitions/McpElicitationSchema"
}
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json`
around lines 1790 - 1802, The schema currently sets "requested_schema": true
which disables validation; replace this with a reference to the existing
McpElicitationSchema so the "requested_schema" property is strongly typed.
Update the object that defines "requested_schema" in the same JSON schema block
to use "$ref": "#/definitions/McpElicitationSchema" (or the repository’s
canonical ref name) and ensure the property type and requiredness match
McpElicitationSchema; keep the surrounding properties ("_meta", "message",
"mode") unchanged.

Comment on lines +11800 to +11810
"properties": {
"appsNeedingAuth": {
"items": {
"$ref": "#/definitions/v2/AppSummary"
},
"type": "array"
}
},
"required": [
"appsNeedingAuth"
],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

PluginInstallResponse.appsNeedingAuth should be optional-with-default for rollout safety.

Making this new field required can break compatibility with older responders that omit it. A default empty array keeps behavior stable and still gives clients a consistent shape.

Backward-compatible response schema
       "PluginInstallResponse": {
         "$schema": "http://json-schema.org/draft-07/schema#",
         "properties": {
           "appsNeedingAuth": {
+            "default": [],
             "items": {
               "$ref": "#/definitions/v2/AppSummary"
             },
             "type": "array"
           }
         },
-        "required": [
-          "appsNeedingAuth"
-        ],
         "title": "PluginInstallResponse",
         "type": "object"
       },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"properties": {
"appsNeedingAuth": {
"items": {
"$ref": "#/definitions/v2/AppSummary"
},
"type": "array"
}
},
"required": [
"appsNeedingAuth"
],
"properties": {
"appsNeedingAuth": {
"default": [],
"items": {
"$ref": "#/definitions/v2/AppSummary"
},
"type": "array"
}
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json`
around lines 11800 - 11810, The new property
PluginInstallResponse.appsNeedingAuth is currently listed as required; change
the schema so it is optional and provide a default empty array to preserve
rollout safety: remove "appsNeedingAuth" from the "required" array (or make the
array empty) and add "default": [] to the appsNeedingAuth property definition
(the object under "properties" -> "appsNeedingAuth" referencing
"#/definitions/v2/AppSummary") so older responders that omit the field are
accepted and clients still receive a consistent array shape.

Comment on lines +615 to +674
EventMsg::ElicitationRequest(request) => {
if matches!(api_version, ApiVersion::V2) {
let permission_guard = thread_watch_manager
.note_permission_requested(&conversation_id.to_string())
.await;
let turn_id = match request.turn_id.clone() {
Some(turn_id) => Some(turn_id),
None => {
let state = thread_state.lock().await;
state.active_turn_snapshot().map(|turn| turn.id)
}
};
let server_name = request.server_name.clone();
let request_body = match request.request.try_into() {
Ok(request_body) => request_body,
Err(err) => {
error!(
error = %err,
server_name,
request_id = ?request.id,
"failed to parse typed MCP elicitation schema"
);
if let Err(err) = conversation
.submit(Op::ResolveElicitation {
server_name: request.server_name,
request_id: request.id,
decision: codex_protocol::approvals::ElicitationAction::Cancel,
content: None,
meta: None,
})
.await
{
error!("failed to submit ResolveElicitation: {err}");
}
return;
}
};
let params = McpServerElicitationRequestParams {
thread_id: conversation_id.to_string(),
turn_id,
server_name: request.server_name.clone(),
request: request_body,
};
let (pending_request_id, rx) = outgoing
.send_request(ServerRequestPayload::McpServerElicitationRequest(params))
.await;
tokio::spawn(async move {
on_mcp_server_elicitation_response(
request.server_name,
request.id,
pending_request_id,
rx,
conversation,
thread_state,
permission_guard,
)
.await;
});
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle non-v2 elicitation requests explicitly to avoid unresolved approvals.

Line 615 only processes elicitation for API v2. For other API versions, the request is dropped without submitting Op::ResolveElicitation, which can leave the turn waiting on a response.

💡 Suggested fix
         EventMsg::ElicitationRequest(request) => {
             if matches!(api_version, ApiVersion::V2) {
                 let permission_guard = thread_watch_manager
                     .note_permission_requested(&conversation_id.to_string())
                     .await;
                 ...
                 tokio::spawn(async move {
                     on_mcp_server_elicitation_response(
                         request.server_name,
                         request.id,
                         pending_request_id,
                         rx,
                         conversation,
                         thread_state,
                         permission_guard,
                     )
                     .await;
                 });
+            } else {
+                error!(
+                    server_name = %request.server_name,
+                    request_id = ?request.id,
+                    "mcp server elicitation is only supported on api v2"
+                );
+                if let Err(err) = conversation
+                    .submit(Op::ResolveElicitation {
+                        server_name: request.server_name,
+                        request_id: request.id,
+                        decision: codex_protocol::approvals::ElicitationAction::Cancel,
+                        content: None,
+                        meta: None,
+                    })
+                    .await
+                {
+                    error!("failed to submit ResolveElicitation: {err}");
+                }
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/src/bespoke_event_handling.rs` around lines 615 - 674,
The ElicitationRequest arm only handles ApiVersion::V2 and silently drops other
versions, leaving turns waiting; update the EventMsg::ElicitationRequest branch
to explicitly handle non-V2 cases by submitting a ResolveElicitation (e.g.,
Op::ResolveElicitation with decision=ElicitationAction::Cancel) via
conversation.submit (same pattern used in the parse-error branch) when
api_version != ApiVersion::V2, and log any submission errors; ensure you
reference request.server_name and request.id when constructing the
ResolveElicitation so the approval is resolved for the correct request.

Comment on lines 219 to +223
if codex_apps_ready || !accessible_connectors.is_empty() {
write_cached_accessible_connectors(cache_key, &accessible_connectors);
}
let accessible_connectors =
with_app_plugin_sources(accessible_connectors, &tool_plugin_provenance);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Cache currently stores pre-enriched connectors, causing cached-only reads to miss plugin sources.

Line [220] writes accessible_connectors before Line [222]-Line [223] applies with_app_plugin_sources. This means cached-only consumers can get stale/empty plugin_display_names compared to the non-cached path.

Suggested fix (cache enriched connectors)
-    let accessible_connectors =
-        filter_disallowed_connectors(accessible_connectors_from_mcp_tools(&tools));
-    if codex_apps_ready || !accessible_connectors.is_empty() {
-        write_cached_accessible_connectors(cache_key, &accessible_connectors);
-    }
-    let accessible_connectors =
-        with_app_plugin_sources(accessible_connectors, &tool_plugin_provenance);
+    let accessible_connectors = with_app_plugin_sources(
+        filter_disallowed_connectors(accessible_connectors_from_mcp_tools(&tools)),
+        &tool_plugin_provenance,
+    );
+    if codex_apps_ready || !accessible_connectors.is_empty() {
+        write_cached_accessible_connectors(cache_key, &accessible_connectors);
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/connectors.rs` around lines 219 - 223, The current flow
writes raw accessible_connectors to cache before enriching them with
with_app_plugin_sources, so cached reads miss plugin_display_names; change the
order to first produce an enriched collection (call
with_app_plugin_sources(accessible_connectors, &tool_plugin_provenance) and
store that result in a local variable) and then call
write_cached_accessible_connectors(cache_key, &enriched_accessible_connectors)
so the cache contains the enriched connectors with plugin_display_names.

Comment on lines +142 to 161
fn discover_marketplace_paths_from_roots(
additional_roots: &[AbsolutePathBuf],
home_dir: Option<&Path>,
) -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Some(repo_root) = get_git_repo_root(cwd) {
let path = repo_root.join(MARKETPLACE_RELATIVE_PATH);

if let Some(home) = home_dir {
let path = home.join(MARKETPLACE_RELATIVE_PATH);
if path.is_file() {
paths.push(path);
}
}

if let Some(home) = home_dir() {
let path = home.join(MARKETPLACE_RELATIVE_PATH);
if path.is_file() {
paths.push(path);
for root in additional_roots {
if let Some(repo_root) = get_git_repo_root(root.as_path()) {
let path = repo_root.join(MARKETPLACE_RELATIVE_PATH);
if path.is_file() && !paths.contains(&path) {
paths.push(path);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Additional roots outside Git repos are currently skipped.

discover_marketplace_paths_from_roots only searches when get_git_repo_root(...) returns Some(...). If an additional root is valid but not inside a Git repo, its .agents/plugins/marketplace.json is never considered.

Suggested fix
 fn discover_marketplace_paths_from_roots(
     additional_roots: &[AbsolutePathBuf],
     home_dir: Option<&Path>,
 ) -> Vec<PathBuf> {
     let mut paths = Vec::new();

@@
     for root in additional_roots {
-        if let Some(repo_root) = get_git_repo_root(root.as_path()) {
-            let path = repo_root.join(MARKETPLACE_RELATIVE_PATH);
-            if path.is_file() && !paths.contains(&path) {
-                paths.push(path);
-            }
-        }
+        let discovery_root =
+            get_git_repo_root(root.as_path()).unwrap_or_else(|| root.as_path().to_path_buf());
+        let path = discovery_root.join(MARKETPLACE_RELATIVE_PATH);
+        if path.is_file() && !paths.contains(&path) {
+            paths.push(path);
+        }
     }

     paths
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/plugins/marketplace.rs` around lines 142 - 161, The
function discover_marketplace_paths_from_roots currently only checks
additional_roots when get_git_repo_root(root.as_path()) returns Some(...), so
roots that are valid directories but not inside a git repo are skipped; update
the loop in discover_marketplace_paths_from_roots to also check the marketplace
file directly on the provided root when get_git_repo_root returns None (i.e.,
compute root.join(MARKETPLACE_RELATIVE_PATH), test is_file(), and push if not
already in paths) while preserving the existing deduplication check
(paths.contains) and existing handling for the repo_root path.

has_workspace_section,
"built realtime startup context"
);
info!("realtime startup context: {context}");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not log full startup context payload at info level.

This logs raw user asks and local path data into normal logs. Keep only metadata (size/section flags) and avoid payload content.

Suggested change
-    info!("realtime startup context: {context}");
+    info!(
+        approx_tokens = approx_token_count(&context),
+        bytes = context.len(),
+        has_recent_work_section,
+        has_workspace_section,
+        "realtime startup context built"
+    );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/realtime_context.rs` at line 96, The info log currently
emits the entire startup context via info!("realtime startup context:
{context}"); change it to only emit safe metadata (e.g., payload size, section
flags, and other non-sensitive fields) instead of full payload or user text:
locate the log site using the symbol/context variable in realtime_context.rs and
replace the message to compute and log only metadata attributes (size, section
flags, counts) while omitting or redacting any fields containing user asks, file
paths, or full payload content.

Comment on lines +125 to +135
fn build_recent_work_section(cwd: &Path, recent_threads: &[ThreadMetadata]) -> Option<String> {
let mut groups: HashMap<PathBuf, Vec<&ThreadMetadata>> = HashMap::new();
for entry in recent_threads {
let group =
resolve_root_git_project_for_trust(&entry.cwd).unwrap_or_else(|| entry.cwd.clone());
groups.entry(group).or_default().push(entry);
}

let current_group =
resolve_root_git_project_for_trust(cwd).unwrap_or_else(|| cwd.to_path_buf());
let mut groups = groups.into_iter().collect::<Vec<_>>();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

RT_FILE="$(fd 'realtime_context\.rs$' | head -n1)"
GI_FILE="$(fd 'git_info\.rs$' | head -n1)"

echo "== Calls to resolve_root_git_project_for_trust in realtime_context =="
rg -n 'resolve_root_git_project_for_trust\(' "$RT_FILE"

echo
echo "== Implementation of resolve_root_git_project_for_trust =="
rg -n -A25 -B3 'fn resolve_root_git_project_for_trust' "$GI_FILE"

Repository: fcoury/oldcodex

Length of output: 5206


Reduce repeated synchronous git-root resolution in startup path.

build_recent_work_section calls resolve_root_git_project_for_trust once per thread entry (line 129–130 in the loop) and again for the current directory (line 134). Each call spawns a synchronous git rev-parse subprocess (git_info.rs:618–622). With large recent-thread sets, this can noticeably block startup.

Memoize results by directory to eliminate duplicate subprocess calls:

Suggested change
 fn build_recent_work_section(cwd: &Path, recent_threads: &[ThreadMetadata]) -> Option<String> {
+    let mut root_cache: HashMap<PathBuf, PathBuf> = HashMap::new();
     let mut groups: HashMap<PathBuf, Vec<&ThreadMetadata>> = HashMap::new();
     for entry in recent_threads {
-        let group =
-            resolve_root_git_project_for_trust(&entry.cwd).unwrap_or_else(|| entry.cwd.clone());
+        let group = root_cache
+            .entry(entry.cwd.clone())
+            .or_insert_with(|| {
+                resolve_root_git_project_for_trust(&entry.cwd)
+                    .unwrap_or_else(|| entry.cwd.clone())
+            })
+            .clone();
         groups.entry(group).or_default().push(entry);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/realtime_context.rs` around lines 125 - 135,
build_recent_work_section currently calls resolve_root_git_project_for_trust
repeatedly (for each ThreadMetadata in recent_threads and again for cwd),
causing duplicate synchronous git subprocesses; add a small local memo/cache
(e.g., HashMap<PathBuf, PathBuf>) inside build_recent_work_section and use it to
store and reuse the resolved root for each directory key when processing
recent_threads and when computing current_group so
resolve_root_git_project_for_trust is invoked at most once per unique directory.

Comment on lines +348 to +376
for entry in entries {
let Some(first_user_message) = entry.first_user_message.as_deref() else {
continue;
};
let ask = first_user_message
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
let dedupe_key = format!("{}:{ask}", entry.cwd.display());
if ask.is_empty() || !seen.insert(dedupe_key) {
continue;
}
let ask = if ask.chars().count() > MAX_ASK_CHARS {
format!(
"{}...",
ask.chars()
.take(MAX_ASK_CHARS.saturating_sub(3))
.collect::<String>()
)
} else {
ask
};
lines.push(format!("- {}: {ask}", entry.cwd.display()));
if seen.len() == max_asks {
break;
}
}

(lines.len() > 5).then(|| lines.join("\n"))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Only emit thread groups when at least one ask is present.

Current lines.len() > 5 allows empty “User asks” sections when metadata lines are present.

Suggested change
-    for entry in entries {
+    let mut ask_count = 0usize;
+    for entry in entries {
         let Some(first_user_message) = entry.first_user_message.as_deref() else {
             continue;
         };
@@
         lines.push(format!("- {}: {ask}", entry.cwd.display()));
+        ask_count += 1;
         if seen.len() == max_asks {
             break;
         }
     }

-    (lines.len() > 5).then(|| lines.join("\n"))
+    (ask_count > 0).then(|| lines.join("\n"))
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/realtime_context.rs` around lines 348 - 376, The code
currently returns the thread-group text based on (lines.len() > 5) which can be
true even when no user "ask" lines were added (only metadata), so change the
logic to only emit the group when at least one ask was appended: add an
ask_count (or reuse a boolean like saw_ask) that you increment/set inside the
loop when you push the ask line (the lines.push(format!("- {}: {ask}",
entry.cwd.display())) call), and then replace the final (lines.len() >
5).then(...) with a check like (ask_count > 0).then(|| lines.join("\n")) so the
function only returns when there is at least one actual ask; reference symbols:
entries, first_user_message, ask, lines, seen, max_asks, MAX_ASK_CHARS.

Comment on lines +57 to +66
async fn save_image_generation_result_to_cwd(
cwd: &Path,
call_id: &str,
result: &str,
) -> Result<PathBuf> {
let bytes = BASE64_STANDARD
.decode(result.trim().as_bytes())
.map_err(|err| {
CodexErr::InvalidRequest(format!("invalid image generation payload: {err}"))
})?;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bound decoded image size before base64 decode to prevent memory spikes.

Line 62 decodes unbounded input into memory. A large payload can trigger excessive allocation before any guardrail.

💡 Suggested fix
+const MAX_IMAGE_OUTPUT_BYTES: usize = 10 * 1024 * 1024;
+
 async fn save_image_generation_result_to_cwd(
     cwd: &Path,
     call_id: &str,
     result: &str,
 ) -> Result<PathBuf> {
+    let payload = result.trim();
+    let max_base64_len = ((MAX_IMAGE_OUTPUT_BYTES + 2) / 3) * 4;
+    if payload.len() > max_base64_len {
+        return Err(CodexErr::InvalidRequest(format!(
+            "image generation payload exceeds {} bytes",
+            MAX_IMAGE_OUTPUT_BYTES
+        )));
+    }
+
     let bytes = BASE64_STANDARD
-        .decode(result.trim().as_bytes())
+        .decode(payload.as_bytes())
         .map_err(|err| {
             CodexErr::InvalidRequest(format!("invalid image generation payload: {err}"))
         })?;
+    if bytes.len() > MAX_IMAGE_OUTPUT_BYTES {
+        return Err(CodexErr::InvalidRequest(format!(
+            "image generation payload exceeds {} bytes",
+            MAX_IMAGE_OUTPUT_BYTES
+        )));
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/stream_events_utils.rs` around lines 57 - 66, The function
save_image_generation_result_to_cwd decodes unbounded base64 into memory
(BASE64_STANDARD.decode) which can spike memory; add a pre-check on the incoming
base64 string (result.trim()) and reject values that would decode to more than a
safe maximum: define a constant like MAX_IMAGE_BYTES (or MAX_BASE64_LEN) and
compute an estimated_decoded = trimmed_len * 3 / 4 (accounting for padding) and
return a CodexErr::InvalidRequest if estimated_decoded > MAX_IMAGE_BYTES before
calling BASE64_STANDARD.decode, then proceed with decoding and existing error
mapping.

@fcoury fcoury force-pushed the feat/tui-app-server branch from 4c3dacf to 90088ce Compare March 8, 2026 18:05
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json (1)

9473-9573: ⚠️ Potential issue | 🟠 Major

Encode the documented command/exec preconditions in the schema.

The descriptions say the server rejects several combinations, but the schema still accepts them: empty command, tty/streaming without processId, conflicting timeout/output-cap flags, and command/exec/write with neither bytes nor closeStdin. That makes the new public contract looser than the implementation.

Schema tightening
       "CommandExecParams": {
         "$schema": "http://json-schema.org/draft-07/schema#",
         "description": "Run a standalone command (argv vector) in the server sandbox without creating a thread or turn.\n\nThe final `command/exec` response is deferred until the process exits and is sent only after all `command/exec/outputDelta` notifications for that connection have been emitted.",
         "properties": {
           "command": {
             "description": "Command argv vector. Empty arrays are rejected.",
             "items": {
               "type": "string"
             },
+            "minItems": 1,
             "type": "array"
           },
           ...
         },
+        "allOf": [
+          {
+            "if": {
+              "properties": { "tty": { "const": true } },
+              "required": ["tty"]
+            },
+            "then": {
+              "required": ["processId"],
+              "properties": {
+                "streamStdin": { "const": true },
+                "streamStdoutStderr": { "const": true }
+              }
+            }
+          },
+          {
+            "if": {
+              "properties": { "streamStdin": { "const": true } },
+              "required": ["streamStdin"]
+            },
+            "then": { "required": ["processId"] }
+          },
+          {
+            "if": {
+              "properties": { "streamStdoutStderr": { "const": true } },
+              "required": ["streamStdoutStderr"]
+            },
+            "then": { "required": ["processId"] }
+          },
+          {
+            "not": {
+              "required": ["disableTimeout", "timeoutMs"],
+              "properties": { "disableTimeout": { "const": true } }
+            }
+          },
+          {
+            "not": {
+              "required": ["disableOutputCap", "outputBytesCap"],
+              "properties": { "disableOutputCap": { "const": true } }
+            }
+          }
+        ],
         "required": [
           "command"
         ],
         "title": "CommandExecParams",
         "type": "object"
       },
       "CommandExecWriteParams": {
         "$schema": "http://json-schema.org/draft-07/schema#",
         "description": "Write stdin bytes to a running `command/exec` session, close stdin, or both.",
         "properties": {
           ...
         },
+        "anyOf": [
+          { "required": ["deltaBase64"] },
+          {
+            "properties": { "closeStdin": { "const": true } },
+            "required": ["closeStdin"]
+          }
+        ],
         "required": [
           "processId"
         ],
         "title": "CommandExecWriteParams",
         "type": "object"
       },

Also applies to: 9674-9699

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json`
around lines 9473 - 9573, The schema currently allows combinations the docs
forbid; tighten the CommandExecParams and related definitions by: enforce
non-empty "command" (array minItems:1 and disallow empty strings if intended),
require "processId" when "tty" is true or when "streamStdin" or
"streamStdoutStderr" are true (use "if"/"then" conditional subschemas
referencing "tty","streamStdin","streamStdoutStderr" to require non-null
"processId"), add mutual-exclusion constraints so "disableTimeout" cannot
co-occur with "timeoutMs" and "disableOutputCap" cannot co-occur with
"outputBytesCap" (use "oneOf"/"not"/"required" patterns or "dependentSchemas"),
and for the command/exec/write schema ensure either "bytes" is present or
"closeStdin" is true (use an "anyOf"/"oneOf" conditional that requires at least
one). Reference these symbols/fields: CommandExecParams, command, processId,
tty, streamStdin, streamStdoutStderr, disableTimeout, timeoutMs,
disableOutputCap, outputBytesCap, and the command/exec/write request schema.
codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs (1)

291-297: ⚠️ Potential issue | 🟡 Minor

Filter control frames in assert_no_message to avoid test failures on keepalive traffic.

The function currently rejects any frame that arrives within the timeout, including WebSocket control frames (Ping/Pong). This causes tests to fail when the server sends keepalive pings. Mirror the filtering pattern from read_jsonrpc_message by looping until timeout while skipping Ping and Pong frames (responding to Ping with Pong as already done elsewhere in the codebase).

Suggested fix
 pub(super) async fn assert_no_message(stream: &mut WsClient, wait_for: Duration) -> Result<()> {
-    match timeout(wait_for, stream.next()).await {
-        Ok(Some(Ok(frame))) => bail!("unexpected frame while waiting for silence: {frame:?}"),
-        Ok(Some(Err(err))) => bail!("unexpected websocket read error: {err}"),
-        Ok(None) => bail!("websocket closed unexpectedly while waiting for silence"),
-        Err(_) => Ok(()),
-    }
+    let deadline = Instant::now() + wait_for;
+    loop {
+        let remaining = deadline.saturating_duration_since(Instant::now());
+        match timeout(remaining, stream.next()).await {
+            Err(_) => return Ok(()),
+            Ok(Some(Ok(WebSocketMessage::Ping(payload)))) => {
+                stream.send(WebSocketMessage::Pong(payload)).await?;
+            }
+            Ok(Some(Ok(WebSocketMessage::Pong(_) | WebSocketMessage::Frame(_)))) => {}
+            Ok(Some(Ok(frame))) => {
+                bail!("unexpected frame while waiting for silence: {frame:?}");
+            }
+            Ok(Some(Err(err))) => bail!("unexpected websocket read error: {err}"),
+            Ok(None) => bail!("websocket closed unexpectedly while waiting for silence"),
+        }
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs` around
lines 291 - 297, The assert_no_message function currently treats any incoming
frame as a failure, causing test flakes when keepalive Ping/Pong frames arrive;
modify assert_no_message to mirror read_jsonrpc_message by looping until the
timeout and filtering control frames: on receiving a Ping, send back a Pong
(using the same send path as elsewhere), ignore incoming Pong frames, continue
the loop (i.e., don't bail) for Ping/Pong and only bail on non-control frames or
errors; keep the timeout semantics so Err(_) still returns Ok(()) when silent.
codex-rs/app-server/src/codex_message_processor.rs (1)

1574-1636: ⚠️ Potential issue | 🟠 Major

Build command/exec from the effective config before starting the proxy.

cwd, env policy, network proxy startup, and Windows sandbox level are all taken from self.config, but the request’s sandbox override is only validated/applied later. That means a command/exec for a different repo cwd or with a per-request sandbox_policy can still run with the wrong network/sandbox setup. Please derive/reload the effective config first, then use that same config for create_env, start_proxy, WindowsSandboxLevel::from_config, and the fallback policy.

Also applies to: 1659-1688

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/src/codex_message_processor.rs` around lines 1574 - 1636,
Construct the effective per-request config (applying the request's sandbox
overrides) before computing cwd, calling create_env, starting the network proxy,
calling WindowsSandboxLevel::from_config, and deriving the fallback sandbox
policy; then use that derived config everywhere those values are taken (replace
uses of self.config with the effective_config when building env, calling
spec.start_proxy, WindowsSandboxLevel::from_config, and when selecting the
sandbox_policy fallback). Do the same fix for the second block around the other
occurrence (the code near the 1659–1688 region) so both proxy startup and
sandbox-level decisions use the same effective_config consistently.
codex-rs/app-server-protocol/schema/json/EventMsg.json (1)

2024-2048: ⚠️ Potential issue | 🟠 Major

Keep elicitation_request backward-compatible.

Both copies of ElicitationRequestEventMsg replace the required top-level message field with required request. That is a wire break for existing generated clients still deserializing the legacy event shape, which conflicts with the stated goal of preserving protocol compatibility. Please make request additive or accept both shapes until the next protocol version.

Also applies to: 7836-7860

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server-protocol/schema/json/EventMsg.json` around lines 2024 -
2048, The schema change replaced the legacy top-level "message" with a required
"request", breaking wire compatibility; update ElicitationRequestEventMsg to
accept either shape by adding an anyOf that requires at least one of "message"
or "request" (e.g. anyOf: [{"required":["message"]},{"required":["request"]}])
and make "request" additive (remove it from the sole required list), then apply
the same fix to the other copy referenced (the other ElicitationRequestEventMsg
instance). Ensure the "request" property remains defined and keep "message"
property present so both legacy and new clients can deserialize.
codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json (1)

2494-2585: ⚠️ Potential issue | 🟠 Major

Enforce the documented command/exec invariants in the schema.

This definition still accepts requests the server says it will reject, e.g. {"command":[]}, {"timeoutMs":-1}, or {"tty":true} without a client-visible processId. That leaks basic contract validation to runtime and will mislead generated clients.

Suggested schema constraints
     "command": {
       "description": "Command argv vector. Empty arrays are rejected.",
+      "minItems": 1,
       "items": {
         "type": "string"
       },
       "type": "array"
     },
@@
     "timeoutMs": {
       "description": "Optional timeout in milliseconds.\n\nWhen omitted, the server default applies. Cannot be combined with `disableTimeout`.",
       "format": "int64",
+      "minimum": 0.0,
       "type": [
         "integer",
         "null"
       ]
     },
@@
-    "required": [
-      "command"
-    ],
+    "required": [
+      "command"
+    ],
+    "allOf": [
+      {
+        "if": {
+          "properties": {
+            "tty": { "const": true }
+          }
+        },
+        "then": {
+          "required": ["processId"],
+          "properties": {
+            "streamStdin": { "const": true },
+            "streamStdoutStderr": { "const": true }
+          }
+        }
+      },
+      {
+        "if": {
+          "properties": {
+            "streamStdin": { "const": true }
+          }
+        },
+        "then": { "required": ["processId"] }
+      },
+      {
+        "if": {
+          "properties": {
+            "streamStdoutStderr": { "const": true }
+          }
+        },
+        "then": { "required": ["processId"] }
+      }
+    ],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json`
around lines 2494 - 2585, Add schema constraints so the JSON accepts only the
documented requests: set "command" to require at least one element (add
"minItems": 1), enforce non-negative timeouts by adding "minimum": 0 to
"timeoutMs", and add conditional rules (JSON Schema if/then) that require
"processId" when "tty" is true or when "streamStdin" or "streamStdoutStderr" are
true; also enforce that "size" is only allowed when "tty" is true via a similar
if/then, and add mutual-exclusion constraints (using "not" + "required" or
"oneOf") to forbid combining "disableOutputCap" with "outputBytesCap" and to
forbid combining "disableTimeout" with "timeoutMs".
♻️ Duplicate comments (4)
codex-rs/app-server/src/bespoke_event_handling.rs (1)

615-674: ⚠️ Potential issue | 🟠 Major

Resolve unsupported elicitation requests instead of dropping them.

For non-ApiVersion::V2 clients this branch currently does nothing, so the core-side elicitation never gets a matching Op::ResolveElicitation and the turn can stay stuck waiting for approval.

Suggested fix
         EventMsg::ElicitationRequest(request) => {
             if matches!(api_version, ApiVersion::V2) {
                 let permission_guard = thread_watch_manager
                     .note_permission_requested(&conversation_id.to_string())
                     .await;
                 let turn_id = match request.turn_id.clone() {
                     Some(turn_id) => Some(turn_id),
                     None => {
                         let state = thread_state.lock().await;
                         state.active_turn_snapshot().map(|turn| turn.id)
                     }
                 };
                 let server_name = request.server_name.clone();
                 let request_body = match request.request.try_into() {
                     Ok(request_body) => request_body,
                     Err(err) => {
                         error!(
                             error = %err,
                             server_name,
                             request_id = ?request.id,
                             "failed to parse typed MCP elicitation schema"
                         );
                         if let Err(err) = conversation
                             .submit(Op::ResolveElicitation {
                                 server_name: request.server_name,
                                 request_id: request.id,
                                 decision: codex_protocol::approvals::ElicitationAction::Cancel,
                                 content: None,
                                 meta: None,
                             })
                             .await
                         {
                             error!("failed to submit ResolveElicitation: {err}");
                         }
                         return;
                     }
                 };
                 let params = McpServerElicitationRequestParams {
                     thread_id: conversation_id.to_string(),
                     turn_id,
                     server_name: request.server_name.clone(),
                     request: request_body,
                 };
                 let (pending_request_id, rx) = outgoing
                     .send_request(ServerRequestPayload::McpServerElicitationRequest(params))
                     .await;
                 tokio::spawn(async move {
                     on_mcp_server_elicitation_response(
                         request.server_name,
                         request.id,
                         pending_request_id,
                         rx,
                         conversation,
                         thread_state,
                         permission_guard,
                     )
                     .await;
                 });
+            } else {
+                error!(
+                    server_name = %request.server_name,
+                    request_id = ?request.id,
+                    "mcp server elicitation is only supported on api v2"
+                );
+                if let Err(err) = conversation
+                    .submit(Op::ResolveElicitation {
+                        server_name: request.server_name,
+                        request_id: request.id,
+                        decision: codex_protocol::approvals::ElicitationAction::Cancel,
+                        content: None,
+                        meta: None,
+                    })
+                    .await
+                {
+                    error!("failed to submit ResolveElicitation: {err}");
+                }
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/src/bespoke_event_handling.rs` around lines 615 - 674,
The EventMsg::ElicitationRequest arm currently ignores non-ApiVersion::V2
clients causing core-side elicitation to never receive an
Op::ResolveElicitation; update the else branch for the ApiVersion check in the
EventMsg::ElicitationRequest handler to call
conversation.submit(Op::ResolveElicitation { server_name:
request.server_name.clone(), request_id: request.id, decision:
codex_protocol::approvals::ElicitationAction::Cancel, content: None, meta: None
}).await and log any submission error (matching the existing error handling used
when request parsing fails) so unsupported elicitation requests are explicitly
resolved instead of dropped.
codex-rs/app-server/src/message_processor.rs (1)

337-340: ⚠️ Potential issue | 🟠 Major

Don't log raw typed notifications.

This new in-process handler repeats the full-payload logging pattern already called out elsewhere. Please log only safe metadata or a constant message here.

🔒 Redaction-safe logging sketch
 pub(crate) async fn process_client_notification(&self, notification: ClientNotification) {
     // Currently, we do not expect to receive any typed notifications from
     // in-process clients, so we just log them.
-    tracing::info!("<- typed notification: {:?}", notification);
+    let _ = notification;
+    tracing::debug!("<- typed notification received");
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/src/message_processor.rs` around lines 337 - 340, The
current process_client_notification implementation logs the entire
ClientNotification payload via tracing::info! which exposes raw typed data;
change it to avoid logging the full payload — replace the tracing::info! call in
process_client_notification to emit only a constant message or a small set of
redaction-safe metadata (e.g., notification type/variant name, id, or other
non-sensitive fields) instead of the entire ClientNotification object, and
remove or redact any sensitive fields before logging.
codex-rs/app-server/src/in_process.rs (1)

252-268: ⚠️ Potential issue | 🟠 Major

Bound the shutdown enqueue wait.

Lines 256-259 can still block forever on a full bounded queue, so shutdown() may never reach the timeout/abort logic on Lines 265-267. That breaks the bounded-shutdown guarantee in the exact wedged state this path needs to handle.

Proposed fix
-        if self
-            .client_tx
-            .send(InProcessClientMessage::Shutdown { done_tx })
-            .await
-            .is_ok()
-        {
+        let shutdown_enqueued = matches!(
+            timeout(
+                SHUTDOWN_TIMEOUT,
+                self.client_tx.send(InProcessClientMessage::Shutdown { done_tx }),
+            )
+            .await,
+            Ok(Ok(()))
+        );
+        if shutdown_enqueued {
             let _ = timeout(SHUTDOWN_TIMEOUT, done_rx).await;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/src/in_process.rs` around lines 252 - 268, The shutdown
method can block forever on awaiting
self.client_tx.send(InProcessClientMessage::Shutdown { done_tx }) if the bounded
channel is full; wrap that send future with the SHUTDOWN_TIMEOUT (e.g.,
timeout(SHUTDOWN_TIMEOUT, self.client_tx.send(...))).await and treat a timeout
as a failed enqueue so execution continues to the runtime abort path: if the
timed send returns Err(elapsed) or Err(send_error) skip waiting on done_rx and
proceed to the runtime_handle timeout/abort logic (using the existing
SHUTDOWN_TIMEOUT and runtime_handle symbols).
codex-rs/app-server-client/src/lib.rs (1)

531-553: ⚠️ Potential issue | 🟠 Major

Keep shutdown() bounded and run cleanup before returning errors.

shutdown() can still fail its contract in two ways: the send on Lines 534-537 can wait forever on a full bounded queue, and the ?? on Line 546 can return before the join/abort cleanup runs. That makes the shutdown error path capable of leaking the worker task.

Proposed fix
 pub async fn shutdown(self) -> IoResult<()> {
     let mut worker_handle = self.worker_handle;
     let (response_tx, response_rx) = oneshot::channel();
-    if self
-        .command_tx
-        .send(ClientCommand::Shutdown { response_tx })
-        .await
-        .is_ok()
-        && let Ok(command_result) = timeout(SHUTDOWN_TIMEOUT, response_rx).await
-    {
-        command_result.map_err(|_| {
-            IoError::new(
-                ErrorKind::BrokenPipe,
-                "in-process app-server shutdown channel is closed",
-            )
-        })??;
-    }
+    let mut shutdown_err = None;
+    let shutdown_enqueued = matches!(
+        timeout(
+            SHUTDOWN_TIMEOUT,
+            self.command_tx.send(ClientCommand::Shutdown { response_tx }),
+        )
+        .await,
+        Ok(Ok(()))
+    );
+    if shutdown_enqueued {
+        if let Ok(command_result) = timeout(SHUTDOWN_TIMEOUT, response_rx).await {
+            if let Err(err) = command_result
+                .map_err(|_| {
+                    IoError::new(
+                        ErrorKind::BrokenPipe,
+                        "in-process app-server shutdown channel is closed",
+                    )
+                })
+                .and_then(|inner| inner)
+            {
+                shutdown_err = Some(err);
+            }
+        }
+    }
 
     if let Err(_elapsed) = timeout(SHUTDOWN_TIMEOUT, &mut worker_handle).await {
         warn!("in-process app-server worker did not shut down within timeout; aborting");
         worker_handle.abort();
         let _ = worker_handle.await;
     }
-    Ok(())
+    if let Some(err) = shutdown_err {
+        return Err(err);
+    }
+    Ok(())
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server-client/src/lib.rs` around lines 531 - 553, The shutdown()
method can block on sending ClientCommand::Shutdown via command_tx and can
return early from the command_result?? before cleaning up worker_handle; fix by
making the send bounded (use try_send or send with timeout using the same
SHUTDOWN_TIMEOUT/select) so it cannot wait forever, and restructure shutdown()
so the worker_handle cleanup (the timeout+abort+await block that references
worker_handle) always runs before any Err is returned — ensure you still await
the response_rx with timeout (timeout(SHUTDOWN_TIMEOUT, response_rx)) but
capture its result and only return an error after executing the worker_handle
cleanup logic; reference command_tx, ClientCommand::Shutdown { response_tx },
response_rx, timeout, SHUTDOWN_TIMEOUT, and worker_handle.
🟡 Minor comments (4)
codex-rs/app-server-protocol/schema/json/v2/CommandExecWriteParams.json-3-23 (1)

3-23: ⚠️ Potential issue | 🟡 Minor

Reject no-op command/exec/write requests.

This schema currently accepts payloads like { "processId": "..." } or { "processId": "...", "closeStdin": false }, even though the contract says the call should write bytes, close stdin, or both. Tighten validation so callers must provide a real action, or relax the description if no-op writes are intentional.

🛠️ Suggested schema tightening
   "required": [
     "processId"
   ],
+  "anyOf": [
+    {
+      "properties": {
+        "deltaBase64": {
+          "type": "string"
+        }
+      },
+      "required": [
+        "deltaBase64"
+      ]
+    },
+    {
+      "properties": {
+        "closeStdin": {
+          "const": true
+        }
+      },
+      "required": [
+        "closeStdin"
+      ]
+    }
+  ],
   "title": "CommandExecWriteParams",
   "type": "object"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server-protocol/schema/json/v2/CommandExecWriteParams.json`
around lines 3 - 23, The schema currently allows no-op objects (e.g., only
"processId") so update CommandExecWriteParams.json to require an actual action:
add a top-level oneOf (or anyOf) that enforces either (A) presence of
"deltaBase64" as a non-empty string (use "type":"string" with "minLength":1 and
disallow null) or (B) presence of "closeStdin" with a boolean value true (or
both), while keeping "processId" required; alternatively, if no-op requests are
intentional, revise the "description" to state that writes may be no-ops.
Reference properties: "deltaBase64", "closeStdin", and "processId".
codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs-229-244 (1)

229-244: ⚠️ Potential issue | 🟡 Minor

Fail fast on duplicate responses too.

This helper already treats duplicate notifications as a protocol error, but a second response with the same id is still silently accepted and overwrites the first one. That can let routing tests pass against broken JSON-RPC behavior.

Possible fix
         match message {
             JSONRPCMessage::Response(candidate) if candidate.id == target_id => {
-                response = Some(candidate);
+                if response.replace(candidate).is_some() {
+                    bail!(
+                        "received duplicate response for id `{id}` before completing paired read"
+                    );
+                }
             }
             JSONRPCMessage::Notification(candidate) if candidate.method == method => {
                 if notification.replace(candidate).is_some() {
                     bail!(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs` around
lines 229 - 244, The loop that reads messages accepts a second Response with the
same id silently; update the JSONRPCMessage::Response(target_id) branch to
mirror the Notification handling: when a matching Response is seen, call
response.replace(candidate) and if that returns Some(_) then bail with a
protocol-error message like "received duplicate response for id `{target_id}`
before completing paired read"; this uses the existing variables response,
target_id and the JSONRPCMessage::Response pattern to locate and change the
assignment to a replace+error path.
codex-rs/app-server-protocol/schema/json/ClientRequest.json-150-159 (1)

150-159: ⚠️ Potential issue | 🟡 Minor

Publish the same command/exec constraints the server already enforces.

The schema still accepts empty command arrays, zero rows/cols, and command/exec/write payloads that omit both deltaBase64 and closeStdin, but codex-rs/app-server/src/command_exec.rs rejects all three at runtime. That drift means generated validators and docs allow requests that are guaranteed to fail on the wire. Please encode those invariants upstream as well (minItems: 1, positive terminal dimensions, and a write-request anyOf) rather than only in server-side validation.

Also applies to: 274-295, 309-331

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server-protocol/schema/json/ClientRequest.json` around lines 150
- 159, The JSON schema currently allows invalid command-exec requests that the
server rejects at runtime; update the CommandExecParams and related definitions
to match server invariants: add "minItems": 1 to the "command" array in
CommandExecParams (and the other command-array schemas referenced at the other
ranges), enforce positive terminal dimensions by setting "minimum": 1 for "rows"
and "cols", and change the command/exec/write request schema to require at least
one of "deltaBase64" or "closeStdin" (use an "anyOf" that ensures either
deltaBase64 exists or closeStdin is true). Reference the symbols
CommandExecParams, command, rows, cols, and the write request fields
deltaBase64/closeStdin when making these edits so generated validators/docs
match runtime checks.
codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json-4605-4610 (1)

4605-4610: ⚠️ Potential issue | 🟡 Minor

saved_path still will not round-trip through history.

The live event and turn-item shapes now expose saved_path, but the persisted/raw image-generation item variants in this same schema still omit it. thread/read / thread/resume will drop the file path after reload, so clients cannot reliably reopen generated images later.

Follow-up fields to add on the persisted/raw variants
@@
         {
           "properties": {
             "id": {
               "type": "string"
             },
             "result": {
               "type": "string"
             },
             "revised_prompt": {
               "type": [
                 "string",
                 "null"
               ]
             },
+            "saved_path": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "status": {
               "type": "string"
             },
@@
         {
           "properties": {
             "id": {
               "type": "string"
             },
             "result": {
               "type": "string"
             },
             "revisedPrompt": {
               "type": [
                 "string",
                 "null"
               ]
             },
+            "savedPath": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "status": {
               "type": "string"
             },

Also applies to: 14628-14633

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json`
around lines 4605 - 4610, The persisted/raw image-generation item variants in
the JSON schema are missing the "saved_path" property so persisted thread/read
and thread/resume lose the file path; update the relevant persisted/raw
image-generation item schema objects to include "saved_path" with the same type
union ["string","null"] as used by the live event and turn-item shapes (ensure
the property name exactly matches "saved_path"), and mirror this change for the
other affected persisted variants referenced (also add to the same variant
schemas noted around lines 14628-14633).
🧹 Nitpick comments (9)
codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json (1)

72-76: Add URI validation to the URL fields.

These properties are documented as URLs, but the schema currently accepts any string. Adding "format": "uri" here would catch malformed marketplace metadata before it reaches UI/browser-launch code.

Also applies to: 90-100

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json` around
lines 72 - 76, The URL properties (e.g., privacyPolicyUrl) currently allow any
string or null; add JSON Schema URI validation by adding "format": "uri" to
those property definitions that use "type": ["string", "null"] so the schema
enforces a well-formed URI (do the same for the other URL fields referenced
around lines 90-100, such as termsOfServiceUrl/iconUrl or equivalent property
names). Ensure the format is applied alongside the existing type array (i.e.,
keep ["string","null"] and add "format": "uri") for each URL property.
codex-rs/app-server-protocol/schema/typescript/v2/CommandExecWriteParams.ts (1)

9-22: Require an actual write or close action.

{ processId }, { processId, closeStdin: false }, and { processId, deltaBase64: null } all satisfy this type even though the contract says the request should write bytes, close stdin, or both. Please tighten the Rust protocol source to require at least one effective action, or reject empty writes server-side before regenerating this schema.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server-protocol/schema/typescript/v2/CommandExecWriteParams.ts`
around lines 9 - 22, The generated type CommandExecWriteParams allows no-op
payloads; ensure the protocol requires at least one effective action by
enforcing (a) deltaBase64 is non-null/non-empty OR (b) closeStdin is true. Fix
this in the Rust protocol source that defines the command/exec write message
(update validation to reject {processId} or {processId, deltaBase64: null} or
{processId, closeStdin:false}), then regen the TypeScript schema so
CommandExecWriteParams reflects the tightened contract (processId plus either a
non-empty deltaBase64 or closeStdin=true).
codex-rs/app-server/tests/suite/v2/thread_name_websocket.rs (2)

140-146: Reuse the existing websocket init helper.

This is a copy of initialize_both_clients from codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs, Lines 132-152. Making the shared helper pub(super) would keep the request-id and timeout setup in one place.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/tests/suite/v2/thread_name_websocket.rs` around lines 140
- 146, Duplicate initialize_both_clients should be removed and the shared helper
made accessible: change the original helper's visibility to pub(super) (the one
that sends initialize requests and waits on read_response_for_id with
DEFAULT_READ_TIMEOUT and specific request IDs), then import/use that helper from
this test instead of re-defining it; ensure you keep using
send_initialize_request, read_response_for_id and DEFAULT_READ_TIMEOUT so the
timeout and request-id behavior remains centralized.

70-80: These assertions are lossy for "no extra messages".

read_response_and_notification_for_method and read_notification_for_method discard non-matching frames on the way to the expected thread/name/updated message (see codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs, Lines 205-253). That means the later assert_no_message only checks what is left in the socket, so an unexpected earlier broadcast can be swallowed and the test still passes.

Also applies to: 118-128

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/tests/suite/v2/thread_name_websocket.rs` around lines 70
- 80, The test currently uses read_response_and_notification_for_method and
read_notification_for_method which discard non-matching frames, allowing earlier
unexpected broadcasts to be swallowed and later assert_no_message to miss them;
modify the test (or the helpers) so that reads do not drop unrelated frames:
either change
read_response_and_notification_for_method/read_notification_for_method to return
all consumed frames (e.g., Vec of frames) or provide a non-destructive
peek/drain helper and then assert that the only frames received are the expected
thread/name/updated notifications and responses, using those returned frames to
validate and then call assert_no_message to ensure no extra frames remain; refer
to the helper functions read_response_and_notification_for_method,
read_notification_for_method and assert_no_message when updating the logic.
codex-rs/app-server/tests/suite/v2/plugin_list.rs (1)

288-288: workspace_default is created but not configured.

The TempDir is created and passed to cwds but has no .git directory, marketplace, or config. If the intent is to test behavior with multiple cwds including an empty workspace, consider adding a brief comment. Otherwise, verify this doesn't cause unintended test behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/tests/suite/v2/plugin_list.rs` at line 288, The TempDir
created as workspace_default (via TempDir::new()) is passed into cwds but left
uninitialized (no .git, marketplace, or config), so either add a short comment
explaining this empty-workspace case is intentional or initialize the temp dir
before use: create a .git directory and any required marketplace/config files or
populate it with the minimal fixtures your test expects; update the test around
workspace_default and cwds to ensure the directory contents match the scenario
being tested (or remove workspace_default if unused).
codex-rs/app-server/src/lib.rs (1)

504-510: Verify error handling behavior for StateRuntime initialization.

The initialization now uses .ok() to silently discard errors, converting failures to None. This means log_db will be unavailable without any warning if initialization fails.

Consider logging a warning when initialization fails so operators can diagnose issues:

Suggested improvement
-    let log_db = codex_state::StateRuntime::init(
-        config.sqlite_home.clone(),
-        config.model_provider_id.clone(),
-    )
-    .await
-    .ok()
-    .map(log_db::start);
+    let log_db = match codex_state::StateRuntime::init(
+        config.sqlite_home.clone(),
+        config.model_provider_id.clone(),
+    )
+    .await
+    {
+        Ok(runtime) => Some(log_db::start(runtime)),
+        Err(err) => {
+            warn!(error = %err, "Failed to initialize state runtime; log_db disabled");
+            None
+        }
+    };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/src/lib.rs` around lines 504 - 510, The code silently
converts StateRuntime::init failures to None by calling .ok(), losing the error;
change this to explicitly handle the Result from
codex_state::StateRuntime::init(config.sqlite_home.clone(),
config.model_provider_id.clone()).await so that on Err you emit a warning with
the error details (e.g., using tracing::warn! or log::warn!) and on Ok you call
.map(log_db::start) or otherwise initialize log_db; specifically replace the
.ok().map(...) chain with a match or .map_err(|e| { warn!("StateRuntime::init
failed: {:?}", e); e }).ok().map(log_db::start) (or equivalent) so failures are
logged while preserving the existing success path.
codex-rs/app-server-protocol/src/protocol/v2.rs (3)

836-855: Consider wrapping Core types instead of exposing them directly.

AdditionalMacOsPermissions directly uses CoreMacOsPreferencesPermission and CoreMacOsAutomationPermission from codex_protocol. This couples the v2 public API surface to internal Core type serialization, which could cause breaking changes if Core types evolve. Other patterns in this file wrap Core types with v2-specific enums/structs.

If intentional for expedience, consider adding a comment explaining this design choice. Otherwise, consider creating v2 wrapper types with From implementations for stability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server-protocol/src/protocol/v2.rs` around lines 836 - 855,
AdditionalMacOsPermissions currently exposes CoreMacOsPreferencesPermission and
CoreMacOsAutomationPermission directly, coupling the v2 API to core types;
either create v2 wrapper types (e.g., V2MacOsPreferencesPermission and
V2MacOsAutomationPermission) and implement From<CoreMacOs...> for each, then
change AdditionalMacOsPermissions fields to use those wrappers and update the
From<CoreMacOsSeatbeltProfileExtensions> implementation to convert into the
wrappers, or if this exposure is intentional, add a clear inline comment above
AdditionalMacOsPermissions documenting the decision and the risk of coupling to
Core types for future maintainers.

1856-1877: Document or validate mutually exclusive options.

CommandExecParams documents that outputBytesCap cannot be combined with disableOutputCap, and timeoutMs cannot be combined with disableTimeout. These constraints are only documented in comments and not enforced at the type level.

Consider either:

  1. Using an enum to make invalid states unrepresentable
  2. Adding explicit validation in the request handler with clear error messages
  3. At minimum, ensure the server validates these constraints and returns informative errors
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server-protocol/src/protocol/v2.rs` around lines 1856 - 1877,
CommandExecParams documents mutually exclusive fields but doesn't enforce them;
add a validation method (e.g., impl CommandExecParams { pub fn validate(&self)
-> Result<(), ProtocolError> }) that checks (1) output_bytes_cap.is_some() &&
disable_output_cap == true and (2) timeout_ms.is_some() && disable_timeout ==
true and returns clear, specific errors naming the offending fields, then call
this validate() from the request handler that constructs/processes
CommandExecParams (e.g., in the command execution request handler such as
handle_command_exec_request or the place that deserializes CommandExecParams) to
reject invalid requests with an informative error; alternatively, if you prefer
type-level safety, replace the pair (output_bytes_cap, disable_output_cap) and
the pair (timeout_ms, disable_timeout) with enums making the mutually exclusive
states unrepresentable.

4440-4448: Verify untagged enum variant ordering for correct deserialization.

McpElicitationPrimitiveSchema uses #[serde(untagged)] with variants ordered as: Enum, String, Number, Boolean. With untagged enums, serde tries variants in order and picks the first that deserializes successfully.

The Enum variant should remain first since it has additional discriminating fields (enum, oneOf, items). However, ensure test coverage validates that:

  • A plain {"type": "string"} deserializes as String, not Enum
  • An enum with {"type": "string", "enum": [...]} deserializes as Enum

The test at lines 5575-5669 appears to cover this, but verify edge cases are handled.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server-protocol/src/protocol/v2.rs` around lines 4440 - 4448,
McpElicitationPrimitiveSchema uses #[serde(untagged)] so variant order matters;
keep Enum as the first variant and add/adjust unit tests to explicitly assert
that deserializing {"type":"string"} yields
McpElicitationPrimitiveSchema::String(...) while deserializing
{"type":"string","enum":[...]} yields McpElicitationPrimitiveSchema::Enum(...).
Update or add tests around the existing coverage (the tests that exercise
primitive schema deserialization) to parse both JSON snippets, assert the enum
variant via pattern matching, and fail if serde picks the wrong variant; if any
flakiness remains, consider making the Enum discriminators more explicit in
McpElicitationEnumSchema (fields like "enum"/"oneOf"/"items") to ensure
deterministic matching.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ec5b445b-00e8-495d-b4fd-57cf3367afe6

📥 Commits

Reviewing files that changed from the base of the PR and between 8379258 and 90088ce.

⛔ Files ignored due to path filters (1)
  • codex-rs/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (193)
  • .devcontainer/Dockerfile
  • .github/workflows/rust-release.yml
  • codex-rs/Cargo.toml
  • codex-rs/app-server-client/Cargo.toml
  • codex-rs/app-server-client/README.md
  • codex-rs/app-server-client/src/lib.rs
  • codex-rs/app-server-protocol/schema/json/ClientRequest.json
  • codex-rs/app-server-protocol/schema/json/EventMsg.json
  • codex-rs/app-server-protocol/schema/json/ServerNotification.json
  • codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
  • codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json
  • codex-rs/app-server-protocol/schema/json/v2/CommandExecOutputDeltaNotification.json
  • codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json
  • codex-rs/app-server-protocol/schema/json/v2/CommandExecResizeParams.json
  • codex-rs/app-server-protocol/schema/json/v2/CommandExecResizeResponse.json
  • codex-rs/app-server-protocol/schema/json/v2/CommandExecResponse.json
  • codex-rs/app-server-protocol/schema/json/v2/CommandExecTerminateParams.json
  • codex-rs/app-server-protocol/schema/json/v2/CommandExecTerminateResponse.json
  • codex-rs/app-server-protocol/schema/json/v2/CommandExecWriteParams.json
  • codex-rs/app-server-protocol/schema/json/v2/CommandExecWriteResponse.json
  • codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json
  • codex-rs/app-server-protocol/schema/json/v2/PluginListParams.json
  • codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json
  • codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json
  • codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json
  • codex-rs/app-server-protocol/schema/json/v2/WindowsSandboxSetupStartParams.json
  • codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts
  • codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts
  • codex-rs/app-server-protocol/schema/typescript/WebSearchContextSize.ts
  • codex-rs/app-server-protocol/schema/typescript/WebSearchLocation.ts
  • codex-rs/app-server-protocol/schema/typescript/WebSearchToolConfig.ts
  • codex-rs/app-server-protocol/schema/typescript/index.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/CommandExecOutputDeltaNotification.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/CommandExecOutputStream.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/CommandExecParams.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/CommandExecResizeParams.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/CommandExecResizeResponse.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/CommandExecResponse.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/CommandExecTerminalSize.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/CommandExecTerminateParams.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/CommandExecTerminateResponse.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/CommandExecWriteParams.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/CommandExecWriteResponse.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/PluginInterface.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/PluginListParams.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/PluginMarketplaceEntry.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/PluginSource.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/PluginSummary.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/ProfileV2.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/ToolsV2.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/WindowsSandboxSetupStartParams.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/index.ts
  • codex-rs/app-server-protocol/src/export.rs
  • codex-rs/app-server-protocol/src/protocol/common.rs
  • codex-rs/app-server-protocol/src/protocol/mappers.rs
  • codex-rs/app-server-protocol/src/protocol/v1.rs
  • codex-rs/app-server-protocol/src/protocol/v2.rs
  • codex-rs/app-server-test-client/src/lib.rs
  • codex-rs/app-server/Cargo.toml
  • codex-rs/app-server/README.md
  • codex-rs/app-server/src/app_server_tracing.rs
  • codex-rs/app-server/src/bespoke_event_handling.rs
  • codex-rs/app-server/src/bin/test_notify_capture.rs
  • codex-rs/app-server/src/codex_message_processor.rs
  • codex-rs/app-server/src/command_exec.rs
  • codex-rs/app-server/src/config_api.rs
  • codex-rs/app-server/src/in_process.rs
  • codex-rs/app-server/src/lib.rs
  • codex-rs/app-server/src/message_processor.rs
  • codex-rs/app-server/src/outgoing_message.rs
  • codex-rs/app-server/tests/common/mcp_process.rs
  • codex-rs/app-server/tests/suite/v2/command_exec.rs
  • codex-rs/app-server/tests/suite/v2/config_rpc.rs
  • codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs
  • codex-rs/app-server/tests/suite/v2/initialize.rs
  • codex-rs/app-server/tests/suite/v2/mod.rs
  • codex-rs/app-server/tests/suite/v2/plugin_install.rs
  • codex-rs/app-server/tests/suite/v2/plugin_list.rs
  • codex-rs/app-server/tests/suite/v2/thread_list.rs
  • codex-rs/app-server/tests/suite/v2/thread_metadata_update.rs
  • codex-rs/app-server/tests/suite/v2/thread_name_websocket.rs
  • codex-rs/app-server/tests/suite/v2/thread_resume.rs
  • codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs
  • codex-rs/app-server/tests/suite/v2/windows_sandbox_setup.rs
  • codex-rs/cli/Cargo.toml
  • codex-rs/cli/src/login.rs
  • codex-rs/cli/src/main.rs
  • codex-rs/cli/tests/debug_clear_memories.rs
  • codex-rs/config/src/config_requirements.rs
  • codex-rs/core/config.schema.json
  • codex-rs/core/src/agent/control.rs
  • codex-rs/core/src/apply_patch.rs
  • codex-rs/core/src/client.rs
  • codex-rs/core/src/client_common.rs
  • codex-rs/core/src/codex.rs
  • codex-rs/core/src/codex_delegate.rs
  • codex-rs/core/src/codex_tests.rs
  • codex-rs/core/src/codex_tests_guardian.rs
  • codex-rs/core/src/compact.rs
  • codex-rs/core/src/compact_remote.rs
  • codex-rs/core/src/config/config_tests.rs
  • codex-rs/core/src/config/managed_features.rs
  • codex-rs/core/src/config/mod.rs
  • codex-rs/core/src/config/network_proxy_spec.rs
  • codex-rs/core/src/config/permissions.rs
  • codex-rs/core/src/config/profile.rs
  • codex-rs/core/src/context_manager/updates.rs
  • codex-rs/core/src/exec.rs
  • codex-rs/core/src/exec_policy.rs
  • codex-rs/core/src/features.rs
  • codex-rs/core/src/features/legacy.rs
  • codex-rs/core/src/git_info.rs
  • codex-rs/core/src/guardian.rs
  • codex-rs/core/src/guardian_prompt.md
  • codex-rs/core/src/guardian_tests.rs
  • codex-rs/core/src/landlock.rs
  • codex-rs/core/src/lib.rs
  • codex-rs/core/src/mcp_tool_call.rs
  • codex-rs/core/src/memories/mod.rs
  • codex-rs/core/src/memories/phase1.rs
  • codex-rs/core/src/memories/phase2.rs
  • codex-rs/core/src/memories/start.rs
  • codex-rs/core/src/memories/tests.rs
  • codex-rs/core/src/memories/usage.rs
  • codex-rs/core/src/memory_trace.rs
  • codex-rs/core/src/mention_syntax.rs
  • codex-rs/core/src/mentions.rs
  • codex-rs/core/src/network_proxy_loader.rs
  • codex-rs/core/src/path_utils.rs
  • codex-rs/core/src/plugins/curated_repo.rs
  • codex-rs/core/src/plugins/injection.rs
  • codex-rs/core/src/plugins/manager.rs
  • codex-rs/core/src/plugins/manifest.rs
  • codex-rs/core/src/plugins/marketplace.rs
  • codex-rs/core/src/plugins/mod.rs
  • codex-rs/core/src/plugins/store.rs
  • codex-rs/core/src/project_doc.rs
  • codex-rs/core/src/realtime_conversation.rs
  • codex-rs/core/src/rollout/list.rs
  • codex-rs/core/src/rollout/metadata.rs
  • codex-rs/core/src/rollout/recorder.rs
  • codex-rs/core/src/rollout/tests.rs
  • codex-rs/core/src/safety.rs
  • codex-rs/core/src/sandboxing/mod.rs
  • codex-rs/core/src/seatbelt.rs
  • codex-rs/core/src/shell_snapshot.rs
  • codex-rs/core/src/skills/injection.rs
  • codex-rs/core/src/skills/invocation_utils.rs
  • codex-rs/core/src/spawn.rs
  • codex-rs/core/src/state/service.rs
  • codex-rs/core/src/state_db.rs
  • codex-rs/core/src/stream_events_utils.rs
  • codex-rs/core/src/tasks/compact.rs
  • codex-rs/core/src/tasks/mod.rs
  • codex-rs/core/src/tasks/regular.rs
  • codex-rs/core/src/tasks/review.rs
  • codex-rs/core/src/tasks/undo.rs
  • codex-rs/core/src/tasks/user_shell.rs
  • codex-rs/core/src/tools/handlers/agent_jobs.rs
  • codex-rs/core/src/tools/handlers/mcp.rs
  • codex-rs/core/src/tools/handlers/multi_agents.rs
  • codex-rs/core/src/tools/handlers/shell.rs
  • codex-rs/core/src/tools/handlers/unified_exec.rs
  • codex-rs/core/src/tools/js_repl/mod.rs
  • codex-rs/core/src/tools/network_approval.rs
  • codex-rs/core/src/tools/orchestrator.rs
  • codex-rs/core/src/tools/registry.rs
  • codex-rs/core/src/tools/runtimes/apply_patch.rs
  • codex-rs/core/src/tools/runtimes/shell.rs
  • codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs
  • codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs
  • codex-rs/core/src/tools/runtimes/unified_exec.rs
  • codex-rs/core/src/tools/sandboxing.rs
  • codex-rs/core/src/tools/spec.rs
  • codex-rs/core/src/turn_timing.rs
  • codex-rs/core/src/unified_exec/mod.rs
  • codex-rs/core/src/unified_exec/process.rs
  • codex-rs/core/src/unified_exec/process_manager.rs
  • codex-rs/core/tests/common/lib.rs
  • codex-rs/core/tests/responses_headers.rs
  • codex-rs/core/tests/suite/approvals.rs
  • codex-rs/core/tests/suite/client.rs
  • codex-rs/core/tests/suite/client_websockets.rs
  • codex-rs/core/tests/suite/exec.rs
  • codex-rs/core/tests/suite/memories.rs
  • codex-rs/core/tests/suite/permissions_messages.rs
  • codex-rs/core/tests/suite/plugins.rs
  • codex-rs/core/tests/suite/realtime_conversation.rs
  • codex-rs/core/tests/suite/request_permissions.rs
  • codex-rs/core/tests/suite/rollout_list_find.rs
  • codex-rs/core/tests/suite/skill_approval.rs
  • codex-rs/core/tests/suite/user_shell_cmd.rs
✅ Files skipped from review due to trivial changes (5)
  • codex-rs/app-server-protocol/src/protocol/v1.rs
  • codex-rs/app-server-protocol/schema/typescript/WebSearchContextSize.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts
  • codex-rs/app-server-protocol/schema/json/v2/CommandExecWriteResponse.json
  • codex-rs/app-server-protocol/schema/typescript/v2/CommandExecResizeResponse.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • codex-rs/app-server-client/README.md
  • codex-rs/app-server-protocol/schema/json/v2/PluginListParams.json
  • codex-rs/Cargo.toml
  • codex-rs/app-server-client/Cargo.toml

Comment on lines 2732 to 2734
"action": {
"$ref": "#/definitions/v2/WebSearchAction"
"$ref": "#/definitions/v2/ResponsesApiWebSearchAction"
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep the existing web-search action wire shape on existing surfaces.

These refs now switch public payloads over to ResponsesApiWebSearchAction, which changes discriminator values from openPage / findInPage to open_page / find_in_page. That is a wire-format break for existing consumers of web_search_end, TurnItem.WebSearch, and web_search_call. Prefer keeping v2/WebSearchAction on these established schemas, or accepting both spellings during migration.

Also applies to: 8238-8240, 13191-13195

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json`
around lines 2732 - 2734, The reference for the "action" property should keep
the existing wire shape by continuing to use v2/WebSearchAction (or accept both
discriminators) instead of switching to ResponsesApiWebSearchAction; update the
schema refs that currently point to
"#/definitions/v2/ResponsesApiWebSearchAction" (including the places around the
shown diff and the other instances at 8238-8240 and 13191-13195) to point to
"#/definitions/v2/WebSearchAction" or to a union/oneOf that accepts both
v2/WebSearchAction and v2/ResponsesApiWebSearchAction so existing consumers of
web_search_end, TurnItem.WebSearch, and web_search_call keep the original
discriminator values ("openPage"/"findInPage") while also allowing the new
snake_case variants during migration.

Comment on lines +5201 to +5301
"ResponsesApiWebSearchAction": {
"oneOf": [
{
"properties": {
"queries": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"query": {
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"search"
],
"title": "SearchResponsesApiWebSearchActionType",
"type": "string"
}
},
"required": [
"type"
],
"title": "SearchResponsesApiWebSearchAction",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"open_page"
],
"title": "OpenPageResponsesApiWebSearchActionType",
"type": "string"
},
"url": {
"type": [
"string",
"null"
]
}
},
"required": [
"type"
],
"title": "OpenPageResponsesApiWebSearchAction",
"type": "object"
},
{
"properties": {
"pattern": {
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"find_in_page"
],
"title": "FindInPageResponsesApiWebSearchActionType",
"type": "string"
},
"url": {
"type": [
"string",
"null"
]
}
},
"required": [
"type"
],
"title": "FindInPageResponsesApiWebSearchAction",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"other"
],
"title": "OtherResponsesApiWebSearchActionType",
"type": "string"
}
},
"required": [
"type"
],
"title": "OtherResponsesApiWebSearchAction",
"type": "object"
}
]
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Require action-specific fields on ResponsesApiWebSearchAction.

Every branch only requires type, so payloads like { "type": "open_page" } and { "type": "find_in_page" } now validate. That removes the schema guarantees generated clients need to render or replay these actions safely. open_page should require a non-null url, find_in_page should require at least pattern (and likely url), and search should require query or queries.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server-protocol/schema/json/EventMsg.json` around lines 5201 -
5301, The ResponsesApiWebSearchAction union currently only requires "type",
allowing invalid payloads; update each variant so action-specific fields are
required: in SearchResponsesApiWebSearchAction require either "query" or
"queries" (e.g. add a oneOf/anyOf requiring "query" OR "queries" and ensure
"queries" is an array of strings), in OpenPageResponsesApiWebSearchAction add
"url" to its required list and make its schema non-null string, and in
FindInPageResponsesApiWebSearchAction add "pattern" (and "url" if intended) to
required and make those non-null strings; adjust the corresponding titles
(SearchResponsesApiWebSearchAction, OpenPageResponsesApiWebSearchAction,
FindInPageResponsesApiWebSearchAction, OtherResponsesApiWebSearchAction) so
generated clients can rely on these fields.

Comment on lines +7 to +17
"cols": {
"description": "Terminal width in character cells.",
"format": "uint16",
"minimum": 0.0,
"type": "integer"
},
"rows": {
"description": "Terminal height in character cells.",
"format": "uint16",
"minimum": 0.0,
"type": "integer"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "CommandExecResizeParams.json" | head -20

Repository: fcoury/oldcodex

Length of output: 133


🏁 Script executed:

find . -type f -path "*schema/json/v2*" | head -20

Repository: fcoury/oldcodex

Length of output: 1607


🏁 Script executed:

# Check if the schema directory structure exists
fd -t f "\.json$" | grep -i "command\|resize" | head -20

Repository: fcoury/oldcodex

Length of output: 1131


🌐 Web query:

JSON Schema Draft-07 format keyword numeric constraint enforcement

💡 Result:

In JSON Schema Draft-07, the format keyword is primarily an annotation and its validation (assertion) behavior is optional—a Draft-07–compliant validator is allowed to ignore format for pass/fail unless it explicitly chooses to implement it as an assertion. This is stated in the Draft-07 Validation spec: format “can … be implemented as assertions”, but that functionality is optional. [1] (Many test suites also treat format validation as optional.) [2]

What this means for numeric constraints

  • Draft-07 format is defined for specific kinds of instance values (most commonly strings, e.g., date-time, email, uri). Draft-07 numeric validation is done with numeric constraint keywords, not format.
  • To enforce numeric constraints, use:
    • type: "number" / type: "integer"
    • minimum / exclusiveMinimum
    • maximum / exclusiveMaximum
    • multipleOf
      These are assertion keywords in Draft-07 (i.e., they must be enforced by a conforming validator). [1]

Practical enforcement guidance

  • If you rely on format for validation, you must choose/configure a validator that enforces formats (and know which formats it supports). For example, Ajv documents how format validation is handled and configured. [3]

Sources: Draft-07 Validation specification [1], Draft-07 format overview noting optional validation [2], Ajv format-validation documentation [3].

[1] (json-schema.org)
[2] (learnjsonschema.com)
[3] (ajv.js.org)

Citations:


🌐 Web query:

JSON Schema uint16 format validation maximum constraint

💡 Result:

  • JSON Schema has no built-in uint16 type or format. In standard JSON Schema, format is generally annotation-only by default (validators may choose to enforce it, but they’re not required to). [2][3]
  • uint16 does exist in the OpenAPI Format Registry (“unsigned 16-bit integer”), but that’s OpenAPI-specific, not a JSON Schema core feature. [1]

How to validate a uint16 range (0…65535) in JSON Schema

Use numeric range keywords—not format:

{
  "type": "integer",
  "minimum": 0,
  "maximum": 65535
}

maximum constrains the instance value to be the keyword value. [4]

If you still want format: "uint16"

You can add it as an annotation (or rely on a validator that supports it as an extension):

{
  "type": "integer",
  "format": "uint16",
  "minimum": 0,
  "maximum": 65535
}

But the range check must still come from minimum/maximum if you need portable, standards-based validation. [2][3][4]

Sources: OpenAPI Format Registry [1]; JSON Schema 2020-12 release notes on format behavior [2]; python-jsonschema docs noting format isn’t mandated to validate [3]; JSON Schema keyword maximum semantics [4].

[1] (spec.openapis.org)
[2] (json-schema.org)
[3] (python-jsonschema.readthedocs.io)
[4] (dev.fyicenter.com)

Citations:


🏁 Script executed:

cat -n ./codex-rs/app-server-protocol/schema/json/v2/CommandExecResizeParams.json

Repository: fcoury/oldcodex

Length of output: 1611


🏁 Script executed:

# Search for CommandExecResizeParams in Rust code to find the backend type definition
rg -t rust "CommandExecResizeParams|ExecResizeParams" --max-count=20

Repository: fcoury/oldcodex

Length of output: 1257


🏁 Script executed:

# Look for struct definitions with cols and rows fields
rg -t rust "struct.*Resize" -A 10 --max-count=10

Repository: fcoury/oldcodex

Length of output: 1935


🏁 Script executed:

# Search for u16 usage in terminal/exec context
rg -t rust "cols|rows" -B 2 -A 2 --max-count=20

Repository: fcoury/oldcodex

Length of output: 50372


🏁 Script executed:

rg -t rust "struct CommandExecTerminalSize" -A 15

Repository: fcoury/oldcodex

Length of output: 1423


🏁 Script executed:

# Also check for serde attributes and deserialization behavior
rg -t rust "CommandExecTerminalSize" -B 2 -A 8 | head -50

Repository: fcoury/oldcodex

Length of output: 4080


Add explicit maximum: 65535.0 bounds to cols and rows fields.

The schema uses format: "uint16" but JSON Schema Draft-07 treats format as optional annotation. Without explicit maximum, validators won't enforce the upper bound, and the schema accepts values above 65535 even though the Rust backend deserializes to u16. Add "maximum": 65535.0 to both fields to match the backend contract and ensure validation consistency.

Suggested schema tightening
         "cols": {
           "description": "Terminal width in character cells.",
           "format": "uint16",
           "minimum": 0.0,
+          "maximum": 65535.0,
           "type": "integer"
         },
         "rows": {
           "description": "Terminal height in character cells.",
           "format": "uint16",
           "minimum": 0.0,
+          "maximum": 65535.0,
           "type": "integer"
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server-protocol/schema/json/v2/CommandExecResizeParams.json`
around lines 7 - 17, The cols and rows integer fields in
CommandExecResizeParams.json currently declare "format": "uint16" but lack
explicit upper bounds; add "maximum": 65535.0 to both the "cols" and "rows"
property objects so the JSON Schema enforces the u16 upper limit and matches the
Rust backend deserialization for those fields.

Comment on lines +1 to +5
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Empty success response for `command/exec/resize`.",
"title": "CommandExecResizeResponse",
"type": "object"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Close this "empty" response schema.

As written, Line 5 still allows arbitrary object properties, so validators will accept non-empty payloads even though the contract says this is an empty success response. Please set additionalProperties to false.

Proposed fix
 {
   "$schema": "http://json-schema.org/draft-07/schema#",
+  "additionalProperties": false,
   "description": "Empty success response for `command/exec/resize`.",
   "title": "CommandExecResizeResponse",
   "type": "object"
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server-protocol/schema/json/v2/CommandExecResizeResponse.json`
around lines 1 - 5, The schema for CommandExecResizeResponse currently defines
an object but allows arbitrary properties; update the JSON Schema
(CommandExecResizeResponse) to explicitly close the object by adding
"additionalProperties": false so validators will reject any non-empty payloads
and enforce the empty-response contract.

Comment on lines +1 to +5
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Empty success response for `command/exec/terminate`.",
"title": "CommandExecTerminateResponse",
"type": "object"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make the terminate response schema match its empty payload contract.

Line 5 currently permits any object shape, which is broader than both the description here and the generated TypeScript Record<string, never> response type. Add additionalProperties: false so the schema rejects stray fields.

Proposed fix
 {
   "$schema": "http://json-schema.org/draft-07/schema#",
+  "additionalProperties": false,
   "description": "Empty success response for `command/exec/terminate`.",
   "title": "CommandExecTerminateResponse",
   "type": "object"
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Empty success response for `command/exec/terminate`.",
"title": "CommandExecTerminateResponse",
"type": "object"
{
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"description": "Empty success response for `command/exec/terminate`.",
"title": "CommandExecTerminateResponse",
"type": "object"
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@codex-rs/app-server-protocol/schema/json/v2/CommandExecTerminateResponse.json`
around lines 1 - 5, The JSON schema "CommandExecTerminateResponse" currently
allows arbitrary object properties though it represents an empty success
response; update the schema for CommandExecTerminateResponse to forbid stray
fields by adding additionalProperties: false to the object definition so the
generated TypeScript type Record<string, never> and runtime validation both
reflect an empty payload contract.

Comment on lines +55 to +56
let resume_resp: JSONRPCResponse = read_response_for_id(&mut ws1, 10).await?;
let resume: ThreadResumeResponse = to_response::<ThreadResumeResponse>(resume_resp)?;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bound these websocket reads with timeout(...).

read_response_for_id, read_notification_for_method, and read_response_and_notification_for_method keep polling until they see a matching frame (see codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs, Lines 190-253). Awaiting them directly here means a missing response or broadcast will hang the test instead of failing fast.

⏱️ Suggested fix
-        let resume_resp: JSONRPCResponse = read_response_for_id(&mut ws1, 10).await?;
+        let resume_resp: JSONRPCResponse =
+            timeout(DEFAULT_READ_TIMEOUT, read_response_for_id(&mut ws1, 10)).await??;
@@
-        let (rename_resp, ws1_notification) =
-            read_response_and_notification_for_method(&mut ws1, 11, "thread/name/updated").await?;
+        let (rename_resp, ws1_notification) = timeout(
+            DEFAULT_READ_TIMEOUT,
+            read_response_and_notification_for_method(&mut ws1, 11, "thread/name/updated"),
+        )
+        .await??;
@@
-        let ws2_notification =
-            read_notification_for_method(&mut ws2, "thread/name/updated").await?;
+        let ws2_notification = timeout(
+            DEFAULT_READ_TIMEOUT,
+            read_notification_for_method(&mut ws2, "thread/name/updated"),
+        )
+        .await??;

Also applies to: 70-77, 118-125

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/app-server/tests/suite/v2/thread_name_websocket.rs` around lines 55
- 56, Wrap all awaits that call read_response_for_id,
read_notification_for_method, and read_response_and_notification_for_method with
a tokio::time::timeout to avoid hanging tests; replace direct awaits like let
resume_resp = read_response_for_id(&mut ws1, 10).await? with a timeout call and
handle the Result returned by timeout (map Err to a test failure by returning an
Err or panicking) so timeouts fail fast. Apply the same pattern to the other
usages in this file (the reads around the resume/notification handling and in
the ranges noted) and ensure the subsequent to_response::<ThreadResumeResponse>
(and similar) still runs only on the successful timed read.

@fcoury fcoury force-pushed the feat/tui-app-server branch 7 times, most recently from d217370 to 1a2e174 Compare March 11, 2026 03:08
@fcoury fcoury force-pushed the feat/tui-app-server branch from d0ec0a3 to 89619ac Compare March 12, 2026 02:19
## Summary
- start a code mode worker once per turn and let it pump nested tool
calls through a dedicated queue
- simplify code mode request/response dispatch around request ids and
generic runner-unavailable errors
- clean up the code mode process API and runner protocol plumbing

## Testing
- not run yet
@fcoury fcoury force-pushed the feat/tui-app-server branch from 89619ac to 1bff842 Compare March 12, 2026 16:15
shaqayeq-oai and others added 2 commits March 12, 2026 09:22
## TL;DR
Bring the Python app-server SDK from `main-with-prs-13953-and-14232`
onto current `main` as a standalone SDK-only PR.

- adds the new `sdk/python` and `sdk/python-runtime` package trees
- keeps the scope to the SDK payload only, without the unrelated
branch-history or workflow changes from the source branch
- regenerates `sdk/python/src/codex_app_server/generated/v2_all.py`
against current `main` schema so the extracted SDK matches today's
protocol definitions

## Validation
- `PYTHONPATH=sdk/python/src python3 -m pytest sdk/python/tests`

Co-authored-by: Codex <noreply@openai.com>
## Summary
- restore `use_linux_sandbox_bwrap` as a removed feature key so older
`--enable` callers parse again
- keep it as a no-op by leaving runtime behavior unchanged
- add regression coverage for the legacy `--enable` path

## Testing
- Not run (updated and pushed quickly)
@fcoury fcoury force-pushed the feat/tui-app-server branch from 1bff842 to ea36017 Compare March 12, 2026 16:52
aibrahim-oai and others added 5 commits March 12, 2026 09:52
## What changed
- `codex-stdio-to-uds` now tolerates `NotConnected` when
`shutdown(Write)` happens after the peer has already closed.
- The socket test was rewritten to send stdin from a fixture file and to
read an exact request payload length instead of waiting on EOF timing.

## Why this fixes the flake
- This one exposed a real cross-platform runtime edge case: on macOS,
the peer can close first after a successful exchange, and
`shutdown(Write)` can report `NotConnected` even though the interaction
already succeeded.
- Treating that specific ordering as a harmless shutdown condition
removes the production-level false failure.
- The old test compounded the problem by depending on EOF timing, which
varies by platform and scheduler. Exact-length IO makes the test
deterministic and focused on the actual data exchange.

## Scope
- Production logic change with matching test rewrite.
…ty (openai#14476)

- **Summary**
- migrate the code mode handler, service, worker, process, runner, and
bridge assets into the `tools/code_mode` module tree
- split Execution, protocol, and handler logic into dedicated files and
relocate the tool definition into `code_mode/spec.rs`
- update core references and tests to stitch the new organization
together
- **Testing**
  - Not run (not requested)
Bring over the TUI-side in-process app-server migration and its follow-on
fixes while keeping the port aligned with current `main`. The TUI now
uses the in-process client flow, replay and approval handling updates,
and the associated snapshot and behavior coverage.

Adapt the port to current branch APIs instead of reviving dirty-branch
shared abstractions. This keeps `SessionSource` explicit, avoids
reintroducing `ClientSurface`, and fills newer approval `skill_metadata`
fields required on this branch.
Add end-to-end TUI support for `request_permissions` in the
in-process app-server path. This wires the new approval request
through the adapter, pending replay tracking, inactive-thread
routing, and a subset-aware approval overlay.

This closes the rebase gap from main where
`item/permissions/requestApproval` reached the protocol before
the TUI adapter handled the new request and response flow.
Seed a child session with the parent thread's granted turn permissions
before submitting its first input.

This preserves turn-scoped `request_permissions` behavior across fresh
sub-agent spawns without turning the grant into a session-wide elevation.
fcoury added 27 commits March 12, 2026 14:07
Keep selected live child threads on the in-process app-server
transport in fresh sessions so normal ops continue to flow through
the same stream that delivers their events. This restores agent
interactions after switching to a live background thread.

Also restore shared `AuthManager` workspace overrides when the
in-process app-server shuts down so temporary external-auth
bridges do not leak session-specific ChatGPT workspace state into
later thread-manager reuse.
Track current turn ids and pending interactive request cleanup per
thread inside the in-process agent loop. Child-thread shutdown now
unsubscribes only that thread instead of tearing down the shared
app-server session.

This prevents primary turn completion from invalidating child prompts,
keeps sub-agent shutdown from killing the whole session, and avoids
interrupts targeting a turn from the wrong thread after view switches.
Translate app-server child `thread/closed` notifications into the
existing routed shutdown path and keep child turn bookkeeping

scoped to the completed thread.

This keeps the agent picker and selected-thread state in sync when
app-server-managed child threads finish, and prevents stale child

turn ids or pending prompts from surviving past completion.
Replace one-shot shared auth mutation with scoped external auth
override guards so nested in-process clients restore only their own

layer on shutdown.

This keeps the latest active refresher and forced ChatGPT workspace
bound to the shared `AuthManager` until the owning client exits,

instead of reverting to stale state when an older client drops.
Keep forced ChatGPT workspace resolution layered when nested external
auth overrides omit their own workspace id.

This prevents a newer in-process client from temporarily clearing an

inherited workspace pin while it is active, which keeps refreshes on
the shared `AuthManager` bound to the expected workspace.
Advance to the next queued `request_user_input` prompt after
interrupting the active one, instead of dismissing the entire

overlay and stranding other threads behind it.

Also render requested permissions from the original profile rather
than expanded picker choices so macOS permission prompts only show

the permission the server actually requested.
When switching away from an app-server-backed thread, remove the
thread from the shared manager and submit `Op::Shutdown` directly to
the removed thread.

This avoids racing the in-process app-server loop against
`remove_thread()`, which could leave the old thread running after a
session switch because shutdown never reached the backing thread.
Treat primary `thread/closed` notifications like shutdown completion in
the in-process agent loop.

This makes externally closed primary sessions tear down through the
normal shutdown-finalization path instead of leaving the old client
alive on a dead transport.
Backfill in-process listener attachment for threads that already exist in a
shared `ThreadManager` when the client initializes.

This keeps preloaded or resumed threads observable through the in-process
app-server path instead of only auto-subscribing future `thread_created`
events.
Skip stale queued `request_user_input` prompts from the interrupted turn
instead of advancing into requests that core has already cleared.

Also let the permissions picker consume plain `o` for search in the
permissions view so cross-thread shortcuts do not steal normal text input.
Capture the originating `thread_id` when interactive prompts are deferred
behind the stream controller, and replay them against that stored thread
instead of the currently selected widget thread.

This keeps approvals and user-input prompts routed to the thread that
originally emitted them after view switches, and updates the chatwidget
tests to cover the real inbound event paths.
Route `request_user_input` confirmation cells through a thread-scoped
app event instead of the global `InsertHistoryCell` path.

This stores local confirmation cells with the originating thread so
background-thread answers do not write into the visible transcript and
still replay when switching back to that thread.
Only the primary live thread should reuse the in-process app-server
op sender when switching views.

This keeps selected secondary threads on the direct `CodexThread`
path until their local response events are routed per thread, which
avoids applying skill, prompt, and history UI updates to the primary
thread by mistake.
Write exec and permission approval decisions through
`InsertThreadHistoryCell` so inactive threads keep the same local
confirmation history as the active thread.

This preserves deny and partial-grant context after switching threads
and updates the TUI tests to accept the thread-scoped history path.
…y in order

Send `Op::Shutdown` directly to the primary core thread when the active
session is app-server-backed, instead of routing that exit through
thread-scoped app-server ops that only unsubscribe the thread stream.

Replace the thread-history replay side channel with an ordered replay
timeline so local-only history cells interleave with buffered events.

This keeps rollback behavior aligned with transcript reconstruction
during thread replay.
Use the live `AuthManager` for local ChatGPT token refreshes in
app-server-backed sessions and route that path through the configured
external auth refresher. This avoids returning stale cached tokens
after unauthorized responses and keeps refresh state aligned with the
shared thread manager.

Treat refreshed ChatGPT account-id mismatches as hard failures during
local refresh handling. This preserves the existing re-authentication
behavior instead of silently switching a running session to a
different account.
Make bottom-pane interrupt cleanup aware of the interrupted thread so
turn-scoped approvals and user-input overlays from other live threads
stay queued. The active thread still clears its own turn-scoped views,
while unscoped views keep the historical interrupt behavior.

Thread-owned bottom-pane overlays now report their `thread_id`, and the
interrupt path filters on that ownership when dismissing views. This
prevents one thread interruption from silently removing actionable
prompts belonging to another thread.
Carry the original event `id` through legacy notification decoding when
app-server includes one in the wrapper payload. TUI replay and
deduplication logic uses non-empty event ids for user-message handling,
so dropping them can reintroduce duplicate transcript entries.

Keep the existing empty-string fallback for legacy payloads that do not
include an event id, and cover both shapes in decoder tests.
Drop the leftover inherited-next-turn permission hooks that no longer
exist in `SessionState`, and reconcile the TUI approval code with the
upstream permissions picker shape after the rebase.

Restore `MessageProcessorArgs.thread_manager` so the in-process
`app-server` still reuses the shared `ThreadManager` expected by the
TUI and `app-server-client`, keeping the rebased branch compiling
without reviving the old branch-specific permissions path.
Adjust the in-process app-server tests to use the current
`ThreadManager::new` signature after the rebase cleanup.

This keeps the shared-manager test coverage compiling without reviving
older constructor arguments that `core` no longer accepts.
Deduplicate in-process permission approvals by `(thread_id, call_id)`
and keep the permissions overlay behavior aligned with the current TUI
flow, including updated replay and test coverage for the `scope` field.

Route shared in-process auth overrides through the existing
`AuthManager` guard stack so nested clients unwind correctly, and drop
an unused TUI test helper to keep the crate warning-free.
@fcoury fcoury force-pushed the feat/tui-app-server branch from ea36017 to f5664bc Compare March 12, 2026 17:08
fcoury added 2 commits March 12, 2026 14:31
Update the rebased test helpers to match the current `MessageProcessorArgs`
and `ThreadManager` interfaces after the latest upstream changes.

Tighten the app-server tracing wait predicate so it waits for spans from
the request trace, and keep `Cargo.lock` in sync with the rebased crate
metadata used by this branch.
Format the rebased app-server tracing test the same way CI's `cargo fmt`
expects so the format job passes again.

Broaden the macOS seatbelt denial matcher to accept the current bash
stderr variant that prefixes denied writes with `line 1:` while keeping
the existing sandbox-apply fallback.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants