Compare commits

..

68 Commits

Author SHA1 Message Date
Yaroslav Volovich
be6d8f3bcf fix(tui): tighten terminal title refresh and caching 2026-02-23 16:52:18 +00:00
Yaroslav Volovich
9f62b3929c fix(title): address review feedback after rebase 2026-02-23 11:53:58 +00:00
Yaroslav Volovich
94a63753ee fix(tui): refresh git branch for title-only terminal title 2026-02-23 11:42:54 +00:00
Yaroslav Volovich
66e76b7afb fix(tui): clear title when sanitization strips content 2026-02-23 11:42:54 +00:00
Yaroslav Volovich
69f68e4bc3 fix(tui): retry terminal title writes after io errors 2026-02-23 11:42:54 +00:00
Yaroslav Volovich
ebdc4d9c86 fix(tui): use neutral /title preview examples 2026-02-23 11:42:54 +00:00
Yaroslav Volovich
fc0c2df63d fix(tui): add ellipses to transient title states 2026-02-23 11:42:54 +00:00
Yaroslav Volovich
5fe79defeb fix(tui): live preview /title and revert on cancel 2026-02-23 11:42:54 +00:00
Yaroslav Volovich
4985f2e6b8 fix(app-server): stabilize running thread resume status 2026-02-23 11:42:53 +00:00
Yaroslav Volovich
a9ec5398c7 tui: add secure /title terminal title config 2026-02-23 11:42:53 +00:00
jif-oai
6fbf19ef5f chore: phase 2 name (#12568) 2026-02-23 11:04:55 +00:00
jif-oai
2b9d0c385f chore: add doc to memories (#12565)
]
2026-02-23 10:52:58 +00:00
jif-oai
cfcbff4c48 chore: awaiter (#12562) 2026-02-23 10:28:24 +00:00
jif-oai
8e9312958d chore: nit name (#12559) 2026-02-23 08:49:41 +00:00
Michael Bolin
956f2f439e refactor: decouple MCP policy construction from escalate server (#12555)
## Why
The current escalate path in `codex-rs/exec-server` still had policy
creation coupled to MCP details, which makes it hard to reuse the shell
execution flow outside the MCP server. This change is part of a broader
goal to split MCP-specific behavior from shared escalation execution so
other handlers (for example a future `ShellCommandHandler`) can reuse it
without depending on MCP request context types.

## What changed
- Added a new `EscalationPolicyFactory` abstraction in `mcp.rs`:
  - `crate`-relative path: `codex-rs/exec-server/src/posix/mcp.rs`
-
https://github.com/openai/codex/blob/main/codex-rs/exec-server/src/posix/mcp.rs#L87-L107
- Made `run_escalate_server` in `mcp.rs` accept a policy factory instead
of constructing `McpEscalationPolicy` directly.
-
https://github.com/openai/codex/blob/main/codex-rs/exec-server/src/posix/mcp.rs#L178-L201
- Introduced `McpEscalationPolicyFactory` that stores MCP-only state
(`RequestContext`, `preserve_program_paths`) and implements the new
trait.
-
https://github.com/openai/codex/blob/main/codex-rs/exec-server/src/posix/mcp.rs#L100-L117
- Updated `shell()` to pass a `McpEscalationPolicyFactory` instance into
`run_escalate_server`, so the server remains the MCP-specific wiring
layer.
-
https://github.com/openai/codex/blob/main/codex-rs/exec-server/src/posix/mcp.rs#L163-L170

## Verification
- Build and test execution was not re-run in this pass; changes are
limited to `mcp.rs` and preserve the existing escalation flow semantics
by only extracting policy construction behind a factory.




---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/12555).
* #12556
* __->__ #12555
2026-02-23 00:31:29 -08:00
pakrym-oai
335a4e1cbc Return image content from view_image (#12553)
Responses API supports image content
2026-02-22 23:00:08 -08:00
Michael Bolin
e8949f4507 test: vendor zsh fork via DotSlash and stabilize zsh-fork tests (#12518)
## Why

The zsh integration tests were still brittle in two ways:

- they relied on `CODEX_TEST_ZSH_PATH` / environment-specific setup, so
they often did not exercise the patched zsh fork that `shell-tool-mcp`
ships
- once the tests consistently used the vendored zsh fork, they exposed
real Linux-specific zsh-fork issues in CI

In particular, the Linux failures were not just test noise:

- the zsh-fork launch path was dropping `ExecRequest.arg0`, so Linux
`codex-linux-sandbox` arg0 dispatch did not run and zsh wrapper-mode
could receive malformed arguments
- the
`turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2`
test uses the zsh exec bridge (which talks to the parent over a Unix
socket), but Linux restricted sandbox seccomp denies `connect(2)`,
causing timeouts on `ubuntu-24.04` x86/arm

This PR makes the zsh tests consistently run against the intended
vendored zsh fork and fixes/hardens the zsh-fork path so the Linux CI
signal is meaningful.

## What Changed

- Added a single shared test-only DotSlash file for the patched zsh fork
at `codex-rs/exec-server/tests/suite/zsh` (analogous to the existing
`bash` test resource).
- Updated both app-server and exec-server zsh tests to use that shared
DotSlash zsh (no duplicate zsh DotSlash file, no `CODEX_TEST_ZSH_PATH`
dependency).
- Updated the app-server zsh-fork test helper to resolve the shared
DotSlash zsh and avoid silently falling back to host zsh.
- Kept the app-server zsh-fork tests configured via `config.toml`, using
a test wrapper path where needed to force `zsh -df` (and rewrite `-lc`
to `-c`) for the subcommand-decline test.
- Hardened the app-server subcommand-decline zsh-fork test for CI
variability:
  - tolerate an extra `/responses` POST with a no-op mock response
- tolerate non-target approval ordering while remaining strict on the
two `/usr/bin/true` approvals and decline behavior
- use `DangerFullAccess` on Linux for this one test because it validates
zsh approval flow, not Linux sandbox socket restrictions
- Fixed zsh-fork process launching on Linux by preserving `req.arg0` in
`ZshExecBridge::execute_shell_request(...)` so `codex-linux-sandbox`
arg0 dispatch continues to work.
- Moved `maybe_run_zsh_exec_wrapper_mode()` under
`arg0_dispatch_or_else(...)` in `app-server` and `cli` so wrapper-mode
handling coexists correctly with arg0-dispatched helper modes.
- Consolidated duplicated `dotslash -- fetch` resolution logic into
shared test support (`core/tests/common/lib.rs`).
- Updated `codex-rs/exec-server/tests/suite/accept_elicitation.rs` to
use DotSlash zsh and hardened the zsh elicitation test for Bazel/zsh
differences by:
  - resolving an absolute `git` path
  - running `git init --quiet .`
- asserting success / `.git` creation instead of relying on banner text

## Verification

- `cargo test -p codex-app-server turn_start_zsh_fork -- --nocapture`
- `cargo test -p codex-exec-server accept_elicitation -- --nocapture`
- `bazel test //codex-rs/exec-server:exec-server-all-test
--test_output=streamed --test_arg=--nocapture
--test_arg=accept_elicitation_for_prompt_rule_with_zsh`
- CI (`rust-ci`) on the final cleaned commit: `Tests — ubuntu-24.04 -
x86_64-unknown-linux-gnu` and `Tests — ubuntu-24.04-arm -
aarch64-unknown-linux-gnu` passed in [run
22291424358](https://github.com/openai/codex/actions/runs/22291424358)
2026-02-22 19:39:56 -08:00
Eric Traut
7e569f1162 Add PR babysitting skill for this repo (#12513)
## PR Notes

This PR adds a project-scoped `babysit-pr` skill for ongoing PR
monitoring (CI, reviews, mergeability).

Simply invoke this skill after creating a PR, and codex will do its best
to get it to a mergeable state:

### What the skill does
* Fixes CI failures related to the PR
* Retries CI failures due to flaky tests
* Addresses code review comments if it agrees with them
* Addresses merge conflicts on main branch

### How the skill works
- Polls PR status on a loop (CI checks, workflow runs, review activity,
mergeability, and review decision).
- Detects new review feedback (including inline comments and automated
Codex review comments) and prompts/handles follow-up work.
- Distinguishes pending vs failed vs passed CI and identifies likely
flaky failures.
- Can retry failed checks/workflows when appropriate.
- Prioritizes actionable code review feedback over flaky CI retries (to
avoid rerunning CI on a SHA that is about to be replaced).
- Continues monitoring after fixes are applied and pushed, rather than
stopping after a progress update.
- Uses a slower backoff polling cadence once CI is green, while still
watching for new review feedback or state changes.
- Treats required review/approval as a blocking condition and keeps
watching until the PR is actually merge-ready (or merged/closed, or
human intervention is needed).

### Intended outcome

Keep the PR moving with minimal manual babysitting by continuously
watching for CI failures, reviewer feedback, and merge blockers, and
responding in the right order until the PR is ready to merge.
2026-02-22 15:36:28 -08:00
Eric Traut
d5fef5c190 Add C# syntax option to highlight selections (#12511)
Summary
- map csharp/c-sharp aliases to the existing C# syntax in the highlight
matcher
- ensure the extension list and tests include .cs and the new aliases so
coverage stays accurate

Testing

<img width="543" height="266" alt="image"
src="https://github.com/user-attachments/assets/e6c8a42f-649c-4c30-b574-421b4287534c"
/>
2026-02-22 12:15:20 -08:00
Eric Traut
5684c82e45 Sort themes case-insensitively in picker (#12509)
## Summary
- order bundled and custom themes together by name while keeping entries
stable across platforms
- update the theme fixture names and tests to assert case-insensitive
ordering
2026-02-22 12:12:36 -08:00
Ahmed Ibrahim
e00fa19328 Revert "Revert "Route inbound realtime text into turn start or steer"" (#12480)
With working tests this time

---------

Co-authored-by: Codex <noreply@openai.com>
2026-02-22 11:54:16 -08:00
Douglas Chimento
2ada9e1b2d feat(tui): support Alt-d delete-forward-word (#12455)
Alt-d should delete the next word. It didn’t. Now it does. Added a small
test so it stays that way.

Details:
File updated:
[codex-rs/tui/src/bottom_pane/textarea.rs](./codex-rs/tui/src/bottom_pane/textarea.rs)
Test added: delete_forward_word_alt_d — verifies Alt-d deletes the next
word and keeps the cursor position correct.

Solves  Issue #12453
2026-02-22 11:22:17 -08:00
jif-oai
0a0caa9df2 Handle orphan exec ends without clobbering active exploring cell (#12313)
Summary
- distinguish exec end handling targets (active tracking, active orphan
history, new cell) so unified exec responses don’t clobber unrelated
exploring cells
- ensure orphan ends flush existing exploring history when complete,
insert standalone history entries, and keep active cells correct
- add regression tests plus a snapshot covering the new behavior and
expose the ExecCell completion result for verification

Fix for https://github.com/openai/codex/issues/12278

---------

Co-authored-by: Josh McKinney <joshka@openai.com>
2026-02-22 14:26:58 +00:00
jif-oai
4666a6e631 feat: monitor role (#12364) 2026-02-22 14:13:56 +00:00
Ahmed Ibrahim
55fc075723 Send events to realtime api (#12423)
- Send assistant messages, ExecCommandBegin, and
PatchApplyBegin/PatchApplyEnd
2026-02-21 23:24:51 -08:00
Dylan Hurd
85b00ae8de fix(core) exec policy parsing 3 (#12485)
## Summary
Quick fix
2026-02-22 06:26:13 +00:00
Won Park
82d3c9ed76 feat(tui) /clear (#12444)
# /clear feature! 

/clear will clear your terminal while preserving the context/state of
the thread.
2026-02-21 22:06:56 -08:00
Max Johnson
37610240ec app-server: retain thread listener across disconnects (#12373)
- keep the per-thread app-server listener alive when the last client
unsubscribes or disconnects
- preserve listener-side active turn history so running `thread/resume`
can merge an in-progress turn snapshot after reconnect
- add `ThreadStateManager` regressions for disconnect/unsubscribe
retention and explicit thread teardown cleanup

Added unit tests, and I manually tested to confirm the fix

---------

Co-authored-by: Codex <noreply@openai.com>
2026-02-22 05:33:33 +00:00
Felipe Coury
c4f1af7a86 feat(tui): syntax highlighting via syntect with theme picker (#11447)
## Summary

Adds syntax highlighting to the TUI for fenced code blocks in markdown
responses and file diffs, plus a `/theme` command with live preview and
persistent theme selection. Uses syntect (~250 grammars, 32 bundled
themes, ~1 MB binary cost) — the same engine behind `bat`, `delta`, and
`xi-editor`. Includes guardrails for large inputs, graceful fallback to
plain text, and SSH-aware clipboard integration for the `/copy` command.

<img width="1554" height="1014" alt="image"
src="https://github.com/user-attachments/assets/38737a79-8717-4715-b857-94cf1ba59b85"
/>

<img width="2354" height="1374" alt="image"
src="https://github.com/user-attachments/assets/25d30a00-c487-4af8-9cb6-63b0695a4be7"
/>

## Problem

Code blocks in the TUI (markdown responses and file diffs) render
without syntax highlighting, making it hard to scan code at a glance.
Users also have no way to pick a color theme that matches their terminal
aesthetic.

## Mental model

The highlighting system has three layers:

1. **Syntax engine** (`render::highlight`) -- a thin wrapper around
syntect + two-face. It owns a process-global `SyntaxSet` (~250 grammars)
and a `RwLock<Theme>` that can be swapped at runtime. All public entry
points accept `(code, lang)` and return ratatui `Span`/`Line` vectors or
`None` when the language is unrecognized or the input exceeds safety
guardrails.

2. **Rendering consumers** -- `markdown_render` feeds fenced code blocks
through the engine; `diff_render` highlights Add/Delete content as a
whole file and Update hunks per-hunk (preserving parser state across
hunk lines). Both callers fall back to plain unstyled text when the
engine returns `None`.

3. **Theme lifecycle** -- at startup the config's `tui.theme` is
resolved to a syntect `Theme` via `set_theme_override`. At runtime the
`/theme` picker calls `set_syntax_theme` to swap themes live; on cancel
it restores the snapshot taken at open. On confirm it persists `[tui]
theme = "..."` to config.toml.

## Non-goals

- Inline diff highlighting (word-level change detection within a line).
- Semantic / LSP-backed highlighting.
- Theme authoring tooling; users supply standard `.tmTheme` files.

## Tradeoffs

| Decision | Upside | Downside |
| ------------------------------------------------ |
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
-----------------------------------------------------------------------------------------------------------------------
|
| syntect over tree-sitter / arborium | ~1 MB binary increase for ~250
grammars + 32 themes; battle-tested crate powering widely-used tools
(`bat`, `delta`, `xi-editor`). tree-sitter would add ~12 MB for 20-30
languages or ~35 MB for full coverage. | Regex-based; less structurally
accurate than tree-sitter for some languages (e.g. language injections
like JS-in-HTML). |
| Global `RwLock<Theme>` | Enables live `/theme` preview without
threading Theme through every call site | Lock contention risk
(mitigated: reads vastly outnumber writes, single UI thread) |
| Skip background / italic / underline from themes | Terminal BG
preserved, avoids ugly rendering on some themes | Themes that rely on
these properties lose fidelity |
| Guardrails: 512 KB / 10k lines | Prevents pathological stalls on huge
diffs or pastes | Very large files render without color |

## Architecture

```
config.toml  ─[tui.theme]─>  set_theme_override()  ─>  THEME (RwLock)
                                                              │
                  ┌───────────────────────────────────────────┘
                  │
  markdown_render ─── highlight_code_to_lines(code, lang) ─> Vec<Line>
  diff_render     ─── highlight_code_to_styled_spans(code, lang) ─> Option<Vec<Vec<Span>>>
                  │
                  │   (None ⇒ plain text fallback)
                  │
  /theme picker   ─── set_syntax_theme(theme)    // live preview swap
                  ─── current_syntax_theme()      // snapshot for cancel
                  ─── resolve_theme_by_name(name) // lookup by kebab-case
```

Key files:

- `tui/src/render/highlight.rs` -- engine, theme management, guardrails
- `tui/src/diff_render.rs` -- syntax-aware diff line wrapping
- `tui/src/theme_picker.rs` -- `/theme` command builder
- `tui/src/bottom_pane/list_selection_view.rs` -- side content panel,
callbacks
- `core/src/config/types.rs` -- `Tui::theme` field
- `core/src/config/edit.rs` -- `syntax_theme_edit()` helper

## Observability

- `tracing::warn` when a configured theme name cannot be resolved.
- `Config::startup_warnings` surfaces the same message as a TUI banner.
- `tracing::error` when persisting theme selection fails.

## Tests

- Unit tests in `highlight.rs`: language coverage, fallback behavior,
CRLF stripping, style conversion, guardrail enforcement, theme name
mapping exhaustiveness.
- Unit tests in `diff_render.rs`: snapshot gallery at multiple terminal
sizes (80x24, 94x35, 120x40), syntax-highlighted wrapping, large-diff
guardrail, rename-to-different-extension highlighting, parser state
preservation across hunk lines.
- Unit tests in `theme_picker.rs`: preview rendering (wide + narrow),
dim overlay on deletions, subtitle truncation, cancel-restore, fallback
for unavailable configured theme.
- Unit tests in `list_selection_view.rs`: side layout geometry, stacked
fallback, buffer clearing, cancel/selection-changed callbacks.
- Integration test in `lib.rs`: theme warning uses the final
(post-resume) config.

## Cargo Deny: Unmaintained Dependency Exceptions

This PR adds two `cargo deny` advisory exceptions for transitive
dependencies pulled in by `syntect v5.3.0`:

| Advisory | Crate | Status |
|----------|-------|--------|
| RUSTSEC-2024-0320 | `yaml-rust` | Unmaintained (maintainer
unreachable) |
| RUSTSEC-2025-0141 | `bincode` | Unmaintained (development ceased;
v1.3.3 considered complete) |

**Why this is safe in our usage:**

- Neither advisory describes a known security vulnerability. Both are
"unmaintained" notices only.
- `bincode` is used by syntect to deserialize pre-compiled syntax sets.
Again, these are **static vendored artifacts** baked into the binary at
build time. No user-supplied bincode data is ever deserialized. - Attack
surface is zero for both crates; exploitation would require a
supply-chain compromise of our own build artifacts.
- These exceptions can be removed when syntect migrates to `yaml-rust2`
and drops `bincode`, or when alternative crates are available upstream.
2026-02-21 20:26:58 -08:00
Alex Kwiatkowski
1dad0a7f4a Make shell detection tests
robust to Nix shell paths (#12476)

## Summary
- Updated `codex-rs/core/src/shell.rs` tests for shell detection to stop
asserting hardcoded shell paths.
- `detects_bash` and `detects_sh` now assert executable basenames
(`bash`, `sh`) rather than `/bin/*`/`/usr/bin/*` absolute paths.
- This keeps behavior the same while avoiding failures in Nix
environments where shells are resolved from `/nix/store/.../bin`.

## Testing
- `nix develop .#default --command sh -lc 'export
PKG_CONFIG_PATH=/nix/store/6az1q591wwlgazzskngr6rl7gmhpyvnc-libcap-2.77-dev/lib/pkgconfig:/nix/store/fgm3pz8486ksh3f94629lpb7xjr2wjp7-openssl-3.6.0-dev/lib/pkgconfig:$PKG_CONFIG_PATH;
export PKG_CONFIG_PATH_FOR_TARGET=$PKG_CONFIG_PATH; cd
/home/alex/workspace/openai/codex/codex-rs && cargo test -p codex-core
--lib detects_bash && cargo test -p codex-core --lib detects_sh'`

## Why
The two failing tests previously hardcoded fixed paths and failed under
the Nix shell due to Nix-provided shell binary locations.

## Links
- Bug report / enhancement request: not publicly filed yet; this was
reproduced in the local Nix environment.
2026-02-21 20:08:02 -08:00
Michael Bolin
b73c4b50a2 fix: make realtime conversation flake test order-insensitive (#12475)
## Why

`codex-core::all` has a flaky test,
`suite::realtime_conversation::conversation_start_audio_text_close_round_trip`,
that assumes a fixed ordering between `conversation.item.create` and
`response.input_audio.delta` requests.

That ordering is not guaranteed: realtime text and audio input are
forwarded through separate queues and a background task, so either
request can be observed first while still being correct behavior.

## What Changed

- Updated the assertion in
`codex-rs/core/tests/suite/realtime_conversation.rs` to compare the two
observed request types order-independently.
- Kept the existing checks that `session.create` is sent first and that
exactly two follow-up requests are recorded.

## Verification

- Re-ran `cargo test -p codex-core --test all
conversation_start_audio_text_close_round_trip` 10 times locally.
2026-02-21 17:06:35 -08:00
Ahmed Ibrahim
5e505ff877 Revert "Route inbound realtime text into turn start or steer" (#12479)
Reverts openai/codex#12469
2026-02-21 15:46:03 -08:00
Ahmed Ibrahim
031d701705 Route inbound realtime text into turn start or steer (#12469)
- Route inbound realtime websocket text into normal user input handling
so it steers an active turn or starts a new one
2026-02-21 15:45:27 -08:00
Felipe Coury
2ba2c57af4 fix(tui): preserve URL clickability across all TUI views (#12067)
## Problem

Long URLs containing `/` and `-` characters are split across multiple
terminal lines by `textwrap`'s default hyphenation rules. This breaks
terminal link detection: emulators can no longer identify the URL as
clickable, and copy-paste yields a truncated fragment. The issue affects
every view that renders user or agent text — exec output, history cells,
markdown, the app-link setup screen, and the VT100 scrollback path.

A secondary bug compounds the first: `desired_height()` calculations
count logical lines rather than viewport rows. When a URL overflows its
line and wraps visually, the height budget is too small, causing content
to clip or leave gaps.

Here is how the complete URL is interpreted by the terminal before
(first line only) and after (complete URL):

| Before | After |
|---|---|
| <img width="777" height="1002" alt="Screenshot 2026-02-17 at 7 59 11
PM"
src="https://github.com/user-attachments/assets/193a89a0-7e56-49c5-8b76-53499a76e7e3"
/> | <img width="777" height="1002" alt="Screenshot 2026-02-17 at 7 58
40 PM"
src="https://github.com/user-attachments/assets/0b9b4c14-aafb-439f-9ffe-f6bba556f95e"
/> |

## Mental model

The TUI now treats URL-like tokens as atomic units that must never be
split by the wrapping engine. Every call site that previously used
`word_wrap_*` has been migrated to `adaptive_wrap_*`, which inspects
each line for URL-like tokens and switches wrapping strategy
accordingly:

- **Non-URL lines** follow the existing `textwrap` path unchanged (word
boundaries, optional indentation, hyphenation).
- **URL-only lines** (with at most decorative markers like `│`, `-`,
`1.`) are emitted unwrapped so terminal link detection works; ratatui's
`Wrap { trim: false }` handles the final character wrap at render time.
- **Mixed lines** (URL + substantive non-URL prose) flow through
`adaptive_wrap_line` so prose wraps naturally at word boundaries while
URL tokens remain unsplit.

Height measurement everywhere now delegates to
`Paragraph::line_count(width)`, which accounts for the visual row cost
of overflowed lines. This single source of truth replaces ad-hoc line
counting in individual cells.

For terminal scrollback (the VT100 path that prints history when the TUI
exits), URL-only lines are emitted unwrapped so the terminal's own link
detector can find them. Mixed URL+prose lines use adaptive wrapping so
surrounding text wraps naturally. Continuation rows are pre-cleared to
avoid stale content artifacts.

## Non-goals

- Full RFC 3986 URL parsing. The detector is a conservative heuristic
that covers `scheme://host`, bare domains (`example.com/path`),
`localhost:port`, and IPv4 hosts. IPv6 (`[::1]:8080`) and exotic schemes
are intentionally excluded from v1.
- Changing wrapping behavior for non-URL content.
- Reflowing or reformatting existing terminal scrollback on resize.

## Tradeoffs

| Decision | Upside | Downside |
|----------|--------|----------|
| Heuristic URL detection vs. full parser | Fast, zero-alloc on the hot
path; conservative enough to reject file paths like `src/main.rs` |
False negatives on obscure URL formats (they get split as before) |
| Adaptive (three-path) wrapping | Non-URL lines are untouched — no
behavior change, no perf cost; mixed lines wrap prose naturally while
preserving URLs | Three wrapping strategies to reason about when
debugging layout |
| Row-based truncation with line-unit ellipsis | Accurate viewport
budget; stable "N lines omitted" count across terminal widths |
`truncate_lines_middle` is more complex (must compute per-line row cost)
|
| Unwrapped URL-only lines in scrollback | Terminal emulators detect
clickable links; copy-paste gets the full URL | TUI and scrollback
formatting diverge for URL-only lines |
| Default `desired_height` via `Paragraph::line_count` | DRY — most
cells inherit correct measurement | Cells with custom layout must
remember to override |

## Architecture

```mermaid
flowchart TD
    A["adaptive_wrap_*()"] --> B{"line_contains_url_like?"}
    B -- No URL tokens --> C["word_wrap_line<br/>(textwrap default)"]
    B -- Has URL tokens --> D{"mixed URL + prose?"}
    D -- "URL-only<br/>(+ decorative markers)" --> E["emit unwrapped<br/>(terminal char-wraps)"]
    D -- "Mixed<br/>(URL + substantive text)" --> F["adaptive_wrap_line<br/>(AsciiSpace + custom WordSplitter)"]
    C --> G["Paragraph::line_count(w)<br/>(single height truth)"]
    E --> G
    F --> G
```

**Changed files:**

| File | Role |
|------|------|
| `wrapping.rs` | URL detection heuristics, mixed-line detection,
`adaptive_wrap_*` functions, custom `WordSplitter` |
| `exec_cell/render.rs` | Row-aware `truncate_lines_middle`, adaptive
wrapping for command/output display |
| `history_cell.rs` | Migrate all cell types to `adaptive_wrap_*`;
default `desired_height` via `Paragraph::line_count` |
| `insert_history.rs` | Three-path scrollback wrapping (unwrapped
URL-only, adaptive mixed, word-wrapped text); continuation row clearing
|
| `app_link_view.rs` | Adaptive wrapping for setup URL; `desired_height`
via `Paragraph::line_count` |
| `markdown_render.rs` | Adaptive wrapping in `finish_paragraph` |
| `model_migration.rs` | Viewport-aware wrapping for narrow-pane
markdown |
| `pager_overlay.rs` | `Wrap { trim: false }` for transcript and
streaming chunks |
| `queued_user_messages.rs` | Migrate to `adaptive_wrap_lines` |
| `status/card.rs` | Migrate to `adaptive_wrap_lines` |

## Observability

- **Ellipsis message** in truncated exec output reports omitted count in
logical lines (stable across resize) rather than viewport rows
(fluctuates).
- URL detection is deterministic and stateless — no hidden caching or
memoization to go stale.
- Height mismatch bugs surface immediately as visual clipping or gaps;
the `Paragraph::line_count` path is the same code ratatui uses at render
time, so measurement and rendering cannot diverge.

## Tests

26 new unit tests across 7 files, covering:

- **URL integrity**: assert a URL-like token appears on exactly one
rendered line (not split across two).
- **Height accuracy**: compare `desired_height()` against
`Paragraph::line_count()` for URL-containing content.
- **Row-aware truncation**: verify ellipsis counts logical lines and
output fits within the row budget.
- **Scrollback rendering**: VT100 backend tests confirm prefix and URL
land on the same row; continuation rows are cleared; mixed URL+prose
lines wrap prose while preserving URL tokens.
- **Mixed URL+prose detection**: `line_has_mixed_url_and_non_url_tokens`
correctly distinguishes lines with substantive non-URL text from lines
with only decorative markers alongside a URL.
- **Heuristic correctness**: positive matches (`https://...`,
`example.com/path`, `localhost:3000/api`, `192.168.1.1:8080/health`) and
negative matches (`src/main.rs`, `foo/bar`, `hello-world`).

## Risks and open items

1. **URL-like tokens in code output** (e.g. `example.com/api` inside a
JSON blob) will trigger URL-preserving wrap on that line. This is
acceptable — the worst case is a slightly wider line, not broken output.
2. **Very long non-URL tokens on a URL line** can only break at
character boundaries (the custom splitter emits all char indices for
non-URL words). On extremely narrow terminals this could overflow, but
narrow terminals already degrade gracefully.
3. **No IPv6 support** — `[::1]:8080/path` will be treated as a non-URL
and may get split. Can be added later without API changes.

Fixes #5457
2026-02-21 15:31:41 -08:00
Michael Bolin
66d5d34e6e core: preserve constrained approval/sandbox policies in TurnContext (#12473) 2026-02-21 14:40:24 -08:00
Michael Bolin
f33ac830aa fix: make skills loader tests hermetic with ~/.agents skills (#12474) 2026-02-21 14:40:13 -08:00
Eric Traut
3586fcb802 Improve token usage estimate for images (#12419)
Fixes #11845.

Adjust context/token estimation for inline image `data:*;base64,...`
URLs so we
do not count the raw base64 payload as model-visible text.

What changed:
- keep the existing JSON-length estimator as the baseline
- detect only inline base64 `data:` image URLs in message and
function-call
  output content items
- subtract only the base64 payload bytes (preserving data URL prefix +
JSON
  overhead)
- add a fixed per-image estimate of 340 bytes (~85 tokens at the repo’s
  4-bytes/token heuristic)

This avoids large overestimates from MCP image tool outputs while
leaving normal
image URLs (`https://`, `file://`, non-base64 `data:` URLs) unchanged.

Tests:
- message image data URL estimate regression
- function-call output image data URL estimate regression
- non-base64 image URLs unchanged
- non-base64 `data:` URLs unchanged
- `data:application/octet-stream;base64,...` adjusted
- multiple inline images apply multiple fixed costs
- text-only items unchanged
2026-02-21 14:25:36 -08:00
pakrym-oai
b17148f13a Prefer v2 websockets if available (#12428)
And also cleanup settings flow to avoid reading many separate flags.

---------

Co-authored-by: Codex <noreply@openai.com>
2026-02-21 20:08:04 +00:00
Eric Traut
a6b2bacb5b Prevent replayed runtime events from forcing active status (#12420)
Fixes #11852

Resume replay was applying transient runtime events (`TurnStarted`,
`StreamError`) as if they were live, which could leave the TUI stuck in
a stale `Working` / `Reconnecting...` state after resuming an
interrupted reconnect.

This change makes replay transcript-oriented for these events by:
- skipping retry-status restoration for replayed non-stream events
- ignoring replayed `TurnStarted` for task-running state
- ignoring replayed `StreamError` for reconnect/status UI

Also adds TUI regression tests and snapshot coverage for the interrupted
reconnect replay case.
2026-02-21 11:55:03 -08:00
sayan-oai
5a635f3427 profile-level model_catalog_json overrie (#12410)
enable `model-catalog_json` config value on `ConfigProfile` as well
2026-02-21 19:39:02 +00:00
viyatb-oai
b3202cbd58 feat(linux-sandbox): implement proxy-only egress via TCP-UDS-TCP bridge (#11293)
## Summary
- Implement Linux proxy-only routing in `codex-rs/linux-sandbox` with a
two-stage bridge: host namespace `loopback TCP proxy endpoint -> UDS`,
then bwrap netns `loopback TCP listener -> host UDS`.
- Add hidden `--proxy-route-spec` plumbing for outer-to-inner stage
handoff.
- Fail closed in proxy mode when no valid loopback proxy endpoints can
be routed.
- Introduce explicit network seccomp modes: `Restricted` (legacy
restricted networking) and `ProxyRouted` (allow INET/INET6 for routed
proxy access, deny `AF_UNIX` and `socketpair`).
- Enforce that proxy bridge/routing is bwrap-only by validating
`--apply-seccomp-then-exec` requires `--use-bwrap-sandbox`.
- Keep landlock-only flows unchanged (no proxy bridge behavior outside
bwrap).

---------

Co-authored-by: Codex <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
2026-02-21 18:16:34 +00:00
pakrym-oai
e7b6f38b58 Delete AggregatedStream (#12441)
Used only in test
2026-02-21 08:50:27 +00:00
Michael Bolin
f5d7a74568 chore: delete empty codex-rs/code file (#12440)
This file was added in https://github.com/openai/codex/pull/4195, but I
think it may have been a mistake?
2026-02-21 08:44:55 +00:00
Michael Bolin
85ce91a5b3 refactor(core): move embedded system skills into codex-skills crate (#12435)
## Why

`codex-core` was carrying the embedded system-skill sample assets (and a
`build.rs` that walks those files to register rerun triggers). Those
assets change infrequently, but any change under `codex-core` still ties
them to `codex-core`'s build/cache lifecycle.

This change moves the embedded system-skills packaging into a dedicated
`codex-skills` crate so it can be cached independently. That reduces
unnecessary invalidation/rebuild pressure on `codex-core` when the
skills bundle is the only thing that changes.

## What Changed

- Added a new `codex-rs/skills` crate (`codex-skills`) with:
  - `Cargo.toml`
  - `BUILD.bazel`
  - `build.rs` to track skill asset file changes for Cargo rebuilds
- `src/lib.rs` containing the embedded system-skills install/cache logic
previously in `codex-core`
- Moved the embedded sample skill assets from
`codex-rs/core/src/skills/assets/samples` to
`codex-rs/skills/src/assets/samples`.
- Updated `codex-rs/core/Cargo.toml` to depend on `codex-skills` and
removed `codex-core`'s direct `include_dir` dependency.
- Removed `codex-core`'s `build.rs`.
- Replaced `codex-rs/core/src/skills/system.rs` implementation with a
thin re-export wrapper to keep existing `codex-core` call sites
unchanged.
- Updated workspace manifests/lockfile (`codex-rs/Cargo.toml`,
`codex-rs/Cargo.lock`) for the new crate.
2026-02-21 08:34:08 +00:00
Michael Bolin
2fe4be1aa9 fix: codex-arg0 no longer depends on codex-core (#12434)
## Why

`codex-rs/arg0` only needed two things from `codex-core`:

- the `find_codex_home()` wrapper
- the special argv flag used for the internal `apply_patch`
self-invocation path

That made `codex-arg0` depend on `codex-core` for a very small surface
area. This change removes that dependency edge and moves the shared
`apply_patch` invocation flag to a more natural boundary
(`codex-apply-patch`) while keeping the contract explicitly documented.

## What Changed

- Moved the internal `apply_patch` argv[1] flag constant out of
`codex-core` and into `codex-apply-patch`.
- Renamed the constant to `CODEX_CORE_APPLY_PATCH_ARG1` and documented
that it is part of the Codex core process-invocation contract (even
though it now lives in `codex-apply-patch`).
- Updated `arg0`, the core apply-patch runtime, and the `codex-exec`
apply-patch test to import the constant from `codex-apply-patch`.
- Updated `codex-rs/arg0` to call
`codex_utils_home_dir::find_codex_home()` directly instead of
`codex_core::config::find_codex_home()`.
- Removed the `codex-core` dependency from `codex-rs/arg0` and added the
needed direct dependency on `codex-utils-home-dir`.
- Added `codex-apply-patch` as a dev-dependency for `codex-rs/exec`
tests (the apply-patch test now imports the moved constant directly).

## Verification

- `cargo test -p codex-apply-patch`
- `cargo test -p codex-arg0`
- `cargo test -p codex-core --lib apply_patch`
- `cargo test -p codex-exec
test_standalone_exec_cli_can_use_apply_patch`
- `cargo shear`
2026-02-21 00:20:42 -08:00
Michael Bolin
1af2a37ada chore: remove codex-core public protocol/shell re-exports (#12432)
## Why

`codex-rs/core/src/lib.rs` re-exported a broad set of types and modules
from `codex-protocol` and `codex-shell-command`. That made it easy for
workspace crates to import those APIs through `codex-core`, which in
turn hides dependency edges and makes it harder to reduce compile-time
coupling over time.

This change removes those public re-exports so call sites must import
from the source crates directly. Even when a crate still depends on
`codex-core` today, this makes dependency boundaries explicit and
unblocks future work to drop `codex-core` dependencies where possible.

## What Changed

- Removed public re-exports from `codex-rs/core/src/lib.rs` for:
- `codex_protocol::protocol` and related protocol/model types (including
`InitialHistory`)
  - `codex_protocol::config_types` (`protocol_config_types`)
- `codex_shell_command::{bash, is_dangerous_command, is_safe_command,
parse_command, powershell}`
- Migrated workspace Rust call sites to import directly from:
  - `codex_protocol::protocol`
  - `codex_protocol::config_types`
  - `codex_protocol::models`
  - `codex_shell_command`
- Added explicit `Cargo.toml` dependencies (`codex-protocol` /
`codex-shell-command`) in crates that now import those crates directly.
- Kept `codex-core` internal modules compiling by using `pub(crate)`
aliases in `core/src/lib.rs` (internal-only, not part of the public
API).
- Updated the two utility crates that can already drop a `codex-core`
dependency edge entirely:
  - `codex-utils-approval-presets`
  - `codex-utils-cli`

## Verification

- `cargo test -p codex-utils-approval-presets`
- `cargo test -p codex-utils-cli`
- `cargo check --workspace --all-targets`
- `just clippy`
2026-02-20 23:45:35 -08:00
pakrym-oai
a87c9c3299 Collapse waited message (#12430)
<img width="1349" height="148" alt="image"
src="https://github.com/user-attachments/assets/98c96523-4cec-4bb1-9998-59d38e0bebb8"
/>
2026-02-20 23:32:59 -08:00
Michael Bolin
1a220ad77d chore: move config diagnostics out of codex-core (#12427)
## Why

Compiling `codex-rs/core` is a bottleneck for local iteration, so this
change continues the ongoing extraction of config-related functionality
out of `codex-core` and into `codex-config`.

The goal is not just to move code, but to reduce `codex-core` ownership
and indirection so more code depends on `codex-config` directly.

## What Changed

- Moved config diagnostics logic from
`core/src/config_loader/diagnostics.rs` into
`config/src/diagnostics.rs`.
- Updated `codex-core` to use `codex-config` diagnostics types/functions
directly where possible.
- Removed the `core/src/config_loader/diagnostics.rs` shim module
entirely; the remaining `ConfigToml`-specific calls are in
`core/src/config_loader/mod.rs`.
- Moved `CONFIG_TOML_FILE` into `codex-config` and updated existing
references to use `codex_config::CONFIG_TOML_FILE` directly.
- Added a direct `codex-config` dependency to `codex-cli` for its
`CONFIG_TOML_FILE` use.
2026-02-20 23:19:29 -08:00
Charley Cunningham
bb0ac5be70 Fix compaction context reinjection and model baselines (#12252)
## Summary
- move regular-turn context diff/full-context persistence into
`run_turn` so pre-turn compaction runs before incoming context updates
are recorded
- after successful pre-turn compaction, rely on a cleared
`reference_context_item` to trigger full context reinjection on the
follow-up regular turn (manual `/compact` keeps replacement history
summary-only and also clears the baseline)
- preserve `<model_switch>` when full context is reinjected, and inject
it *before* the rest of the full-context items
- scope `reference_context_item` and `previous_model` to regular user
turns only so standalone tasks (`/compact`, shell, review, undo) cannot
suppress future reinjection or `<model_switch>` behavior
- make context-diff persistence + `reference_context_item` updates
explicit in the regular-turn path, with clearer docs/comments around the
invariant
- stop persisting local `/compact` `RolloutItem::TurnContext` snapshots
(only regular turns persist `TurnContextItem` now)
- simplify resume/fork previous-model/reference-baseline hydration by
looking up the last surviving turn context from rollout lifecycle
events, including rollback and compaction-crossing handling
- remove the legacy fallback that guessed from bare `TurnContext`
rollouts without lifecycle events
- update compaction/remote-compaction/model-visible snapshots and
compact test assertions (including remote compaction mock response
shape)

## Why
We were persisting incoming context items before spawning the regular
turn task, which let pre-turn compaction requests accidentally include
incoming context diffs without the new user message. Fixing that exposed
follow-on baseline issues around `/compact`, resume/fork, and standalone
tasks that could cause duplicate context injection or suppress
`<model_switch>` instructions.

This PR re-centers the invariants around regular turns:
- regular turns persist model-visible context diffs/full reinjection and
update the `reference_context_item`
- standalone tasks do not advance those regular-turn baselines
- compaction clears the baseline when replacement history may have
stripped the referenced context diffs

## Follow-ups (TODOs left in code)
- `TODO(ccunningham)`: fix rollback/backtracking baseline handling more
comprehensively
- `TODO(ccunningham)`: include pending incoming context items in
pre-turn compaction threshold estimation
- `TODO(ccunningham)`: inject updated personality spec alongside
`<model_switch>` so some model-switch paths can avoid forced full
reinjection
- `TODO(ccunningham)`: review task turn lifecycle
(`TurnStarted`/`TurnComplete`) behavior and emit task-start context
diffs for task types that should have them (excluding `/compact`)

## Validation
- `just fmt`
- CI should cover the updated compaction/resume/model-visible snapshot
expectations and rollout-hydration behavior
- I did **not** rerun the full local test suite after the latest
resume-lookup / rollout-persistence simplifications
2026-02-20 23:13:08 -08:00
Michael Bolin
264fc444b6 feat: discourage the use of the --all-features flag (#12429)
## Why

Developers are frequently running low on disk space, and routine use of
`--all-features` contributes to larger Cargo build caches in `target/`
by compiling additional feature combinations.

This change updates local workflow guidance to avoid `--all-features` by
default and reserve it for cases where full feature coverage is
specifically needed.

## What Changed

- Updated `AGENTS.md` guidance for `codex-rs` to recommend `cargo test`
/ `just test` for full-suite local runs, and to call out the disk-usage
cost of routine `--all-features` usage.
- Updated the root `justfile` so `just fix` and `just clippy` no longer
pass `--all-features` by default.
- Updated `docs/install.md` to explicitly describe `cargo test
--all-features` as an optional heavier-weight run (more build time and
`target/` disk usage).

## Verification

- Confirmed the `justfile` parses and the recipes list successfully with
`just --list`.
2026-02-20 23:02:24 -08:00
Dylan Hurd
a8b4b569fb fix(core) Filter non-matching prefix rules (#12314)
## Summary
`gpt-5.3-codex` really likes to write complicated shell scripts, and
suggest a partial prefix_rule that wouldn't actually approve the
command. We should only show the `prefix_rule` suggestion from the model
if it would actually fully approve the command the user is seeing.

This will technically cause more instances of overly-specific
suggestions when we fallback, but I think the UX is clearer,
particularly when the model doesn't necessarily understand the current
limitations of execpolicy parsing.

## Testing
 - [x] Add unit tests
 - [x] Add integration tests
2026-02-20 22:02:35 -08:00
Michael Bolin
1779feb6a7 ignore v1 in JSON schema codegen (#12408)
## Why

The generated unnamespaced JSON envelope schemas (`ClientRequest` and
`ServerNotification`) still contained both v1 and v2 variants, which
pulled legacy v1/core types and v2 types into the same `definitions`
graph. That caused `schemars` to produce numeric suffix names (for
example `AskForApproval2`, `ByteRange2`, `MessagePhase2`).

This PR moves JSON codegen toward v2-only output while preserving the
unnamespaced envelope artifacts, and avoids reintroducing numeric-suffix
tolerance by removing the v1/internal-only variants that caused the
collisions in those envelope schemas.

## What Changed

- In `codex-rs/app-server-protocol/src/export.rs`, JSON generation now
excludes v1 schema artifacts (`v1/*`) while continuing to emit
unnamespaced/root JSON schemas and the JSON bundle.
- Added a narrow JSON v1 allowlist (`JSON_V1_ALLOWLIST`) so
`InitializeParams` and `InitializeResponse` are still emitted.
- Added JSON-only post-processing for the mixed envelope schemas before
collision checks run:
- `ClientRequest`: strips v1 request variants from the generated `oneOf`
using the temporary `V1_CLIENT_REQUEST_METHODS` list
- `ServerNotification`: strips v1 notifications plus the internal-only
`rawResponseItem/completed` notification using the temporary
`EXCLUDED_SERVER_NOTIFICATION_METHODS_FOR_JSON` list
- Added a temporary local-definition pruning pass for those envelope
schemas so now-unreferenced v1/core definitions are removed from
`definitions` after method filtering.
- Updated the variant-title naming heuristic for single-property literal
object variants to use the literal value (when available), avoiding
collisions like multiple `state`-only variants all deriving the same
title.
- Collision handling remains fail-fast (no numeric suffix fallback map
in this PR path).

## Verification

- `just write-app-server-schema`

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/12408).
* __->__ #12408
* #12406
2026-02-20 21:36:12 -08:00
Yaroslav Volovich
dca9c40dd5 test(app-server): wait for turn/completed in turn_start tests (#12376)
## Summary
- switch a few app-server `turn_start` tests from
`codex/event/task_complete` waits to `turn/completed` waits
- avoid matching unrelated/background `task_complete` events
- keep this flaky test fix separate from the /title feature PR

## Why
On Windows ARM CI, these tests can return early after observing a
generic `codex/event/task_complete` notification from another task. That
can leave the mock Responses server with fewer calls than expected and
fail the test with a wiremock verification mismatch.

Using `turn/completed` matches the app-server turn lifecycle
notification the tests actually care about.

## Validation
- `cargo test -p codex-app-server
turn_start_updates_sandbox_and_cwd_between_turns_v2 -- --nocapture`
- `cargo test -p codex-app-server turn_start_exec_approval_ --
--nocapture`
- `just fmt`
2026-02-20 21:15:21 -08:00
Michael Bolin
48af93399e feat: use OAI Responses API MessagePhase type directly in App Server v2 (#12422)
https://github.com/openai/codex/pull/10455 introduced the `phase` field,
and then https://github.com/openai/codex/pull/12072 introduced a
`MessagePhase` type in `v2.rs` that paralleled the `MessagePhase` type
in `codex-rs/protocol/src/models.rs`.

The app server protocol prefers `camelCase` while the Responses API uses
`snake_case`, so this meant we had two versions of `MessagePhase` with
different serialization rules. When the app server protocol refers to
types from the Responses API, we use the wire format of the the
Responses API even though it is inconsistent with the app server API.

This PR deletes `MessagePhase` from `v2.rs` and consolidates on the
Responses API version to eliminate confusion.
2026-02-20 20:43:36 -08:00
Michael Bolin
a73efab8dd fix: address flakiness in thread_resume_rejoins_running_thread_even_with_override_mismatch (#12381)
## Why
`thread/resume` responses for already-running threads can be reported as
`Idle` even while a turn is still in progress. This is caused by a
timing window where the runtime watch state has not yet observed the
running-thread transition, so API clients can receive stale status
information at resume time.

Possibly related: https://github.com/openai/codex/pull/11786

## What
- Add a shared status normalization helper, `resolve_thread_status`, in
`codex-rs/app-server/src/thread_status.rs` that resolves
`Idle`/`NotLoaded` to `Active { active_flags: [] }` when an in-progress
turn is known.
- Reuse this helper across thread response paths in
`codex-rs/app-server/src/codex_message_processor.rs` (including
`thread/start`, `thread/unarchive`, `thread/read`, `thread/resume`,
`thread/fork`, and review/thread-started notification responses).
- In `handle_pending_thread_resume_request`, use both the in-memory
`active_turn_snapshot` and the resumed rollout turns to decide whether a
turn is in progress before resolving thread status for the response.
- Extend `thread_status` tests to validate the new status-resolution
behavior directly.

## Verification
- `cargo test -p codex-app-server
suite::v2::thread_resume::thread_resume_rejoins_running_thread_even_with_override_mismatch`
2026-02-20 20:36:04 -08:00
Ahmed Ibrahim
b237f7cbb1 Add experimental realtime websocket backend prompt override (#12418)
- add top-level `experimental_realtime_ws_backend_prompt` config key
(experimental / do not use) and include it in config schema
- apply the override only to `Op::RealtimeConversation` websocket
`backend_prompt`, with config + realtime tests
2026-02-20 20:10:51 -08:00
Charley Cunningham
4c1744afb2 Improve Plan mode reasoning selection flow (#12303)
Addresses https://github.com/openai/codex/issues/11013

## Summary
- add a Plan implementation path in the TUI that lets users choose
reasoning before switching to Default mode and implementing
- add Plan-mode reasoning scope handling (Plan-only override vs
all-modes default), including config/schema/docs plumbing for
`plan_mode_reasoning_effort`
- remove the hardcoded Plan preset medium default and make the reasoning
popup reflect the active Plan override as `(current)`
- split the collaboration-mode switch notification UI hint into #12307
to keep this diff focused

If I have `plan_mode_reasoning_effort = "medium"` set in my
`config.toml`:
<img width="699" height="127" alt="Screenshot 2026-02-20 at 6 59 37 PM"
src="https://github.com/user-attachments/assets/b33abf04-6b7a-49ed-b2e9-d24b99795369"
/>

If I don't have `plan_mode_reasoning_effort` set in my `config.toml`:
<img width="704" height="129" alt="Screenshot 2026-02-20 at 7 01 51 PM"
src="https://github.com/user-attachments/assets/88a086d4-d2f1-49c7-8be4-f6f0c0fa1b8d"
/>

## Codex author
`codex resume 019c78a2-726b-7fe3-adac-3fa4523dcc2a`
2026-02-20 20:08:56 -08:00
Ahmed Ibrahim
7ae5d88016 Add experimental realtime websocket URL override (#12416)
- add top-level `experimental_realtime_ws_base_url` config key
(experimental / do not use) and include it in config schema
- apply the override only to `Op::RealtimeConversation` websocket
transport, with config + realtime tests
2026-02-20 19:51:20 -08:00
Rohan Godha
0644ba7b7e fix(nix): include libcap dependency on linux builds (#12415)
commit 923f931121 introduced a dependency
on `libcap`. This PR fixes the nix build by including `libcap` in nix's
build inputs

issue number: #12102. @etraut-openai gave me permission to open pr 

Testing:
running `nix run .#codex-rs` works on both macos (aarch64) and nixos
(x86-64)
2026-02-20 19:32:15 -08:00
Ahmed Ibrahim
6817f0be8a Wire realtime api to core (#12268)
- Introduce `RealtimeConversationManager` for realtime API management 
- Add `op::conversation` to start conversation, insert audio, insert
text, and close conversation.
- emit conversation lifecycle and realtime events.
- Move shared realtime payload types into codex-protocol and add core
e2e websocket tests for start/replace/transport-close paths.

Things to consider:
- Should we use the same `op::` and `Events` channel to carry audio? I
think we should try this simple approach and later we can create
separate one if the channels got congested.
- Sending text updates to the client: we can start simple and later
restrict that.
- Provider auth isn't wired for now intentionally
2026-02-20 19:06:35 -08:00
natea-oai
936e744c93 Add field to Thread object for the latest rename set for a given thread (#12301)
Exposes through the app server updated names set for a thread. This
enables other surfaces to use the core as the source of truth for thread
naming. `threadName` is gathered using the helper functions used to
interact with `session_index.jsonl`, and is hydrated in:
- `thread/list`
- `thread/read`
- `thread/resume`
- `thread/unarchive`
- `thread/rollback`

We don't do this for `thread/start` and `thread/fork`.
2026-02-20 18:26:57 -08:00
Michael Bolin
53bcfaf42d fix: explicitly list name collisions in JSON schema generation (#12406)
## Why

JSON schema codegen was silently resolving naming collisions by
appending numeric suffixes (for example `...2`, `...3`). That makes the
generated schema names unstable: removing an earlier colliding type can
cause a later type to be renumbered, which is a breaking change for
consumers that referenced the old generated name.

This PR makes those collisions explicit and reviewable.

Though note that once we remove `v1` from the codegen, we will no longer
support naming collisions. Or rather, naming collisions will have to be
handled explicitly rather than the numeric suffix approach.

## What Changed

- In `codex-rs/app-server-protocol/src/export.rs`, replaced implicit
numeric suffix collision handling for generated variant titles with
explicit special-case maps.
- Added a panic when a collision occurs without an entry in the map, so
new collisions fail loudly instead of silently renaming generated schema
types.
- Added the currently required special cases so existing generated names
remain stable.
- Extended the same approach to numbered `definitions` / `$defs`
collisions (for example `MessagePhase2`-style names) so those are also
explicitly tracked.

## Verification

- Ran targeted generator-path test:
- `cargo test -p codex-app-server-protocol
generate_json_filters_experimental_fields_and_methods -- --nocapture`


---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/12406).
* #12408
* __->__ #12406
2026-02-20 17:51:53 -08:00
Matthew Zeng
36a2a9fdbb [apps] Bump MCP tool call timeout. (#12405)
- [x] Bump MCP tool call timeout.
2026-02-20 17:35:07 -08:00
Felipe Coury
a5d0757ed1 fix(tui): queued-message edit shortcut unreachable in some terminals (#12240)
## Problem

The TUI's "edit queued message" shortcut (Alt+Up) is either silently
swallowed or recognized as another key combination by Apple Terminal,
Warp, and VSCode's integrated terminal on macOS. Users in those
environments see the hint but pressing the keys does nothing.

## Mental model

When a model turn is in progress the user can still type follow-up
messages. These are queued and displayed below the composer with a hint
line showing how to pop the most recent one back into the editor. The
hint text and the actual key handler must agree on which shortcut is
used, and that shortcut must actually reach the TUI—i.e. it must not be
intercepted by the host terminal.

Three terminals are known to intercept Alt+Up: Apple Terminal (remaps it
to cursor movement), Warp (consumes it for its own command palette), and
VSCode (maps it to "move line up"). For these we use Shift+Left instead.

<p align="center">
<img width="283" height="182" alt="image"
src="https://github.com/user-attachments/assets/4a9c5d13-6e47-4157-bb41-28b4ce96a914"
/>
</p>

| macOS Native Terminal | Warp | VSCode Terminal |
|---|---|---|
| <img width="1557" height="1010" alt="SCR-20260219-kigi"
src="https://github.com/user-attachments/assets/f4ff52f8-119e-407b-a3f3-52f564c36d70"
/> | <img width="1479" height="1261" alt="SCR-20260219-krrf"
src="https://github.com/user-attachments/assets/5807d7c4-17ae-4a2b-aa27-238fd49d90fd"
/> | <img width="1612" height="1312" alt="SCR-20260219-ksbz"
src="https://github.com/user-attachments/assets/1cedb895-6966-4d63-ac5f-0eea0f7057e8"
/> |

## Non-goals

- Making the binding user-configurable at runtime (deferred to a broader
keybinding-config effort).
- Remapping any other shortcuts that might be terminal-specific.

## Tradeoffs

- **Exhaustive match instead of a wildcard default.** The
`queued_message_edit_binding_for_terminal` function explicitly lists
every `TerminalName` variant. This is intentional: adding a new terminal
to the enum will produce a compile error, forcing the author to decide
which binding that terminal should use.

- **Binding lives on `ChatWidget`, hint lives on `QueuedUserMessages`.**
The key event handler that actually acts on the press is in
`ChatWidget`, but the rendered hint text is inside `QueuedUserMessages`.
These are kept in sync by `ChatWidget` calling
`bottom_pane.set_queued_message_edit_binding(self.queued_message_edit_binding)`
during construction. A mismatch would show the wrong hint but would not
lose data.

## Architecture

```mermaid
  graph TD
      TI["terminal_info().name"] --> FN["queued_message_edit_binding_for_terminal(name)"]
      FN --> KB["KeyBinding"]
      KB --> CW["ChatWidget.queued_message_edit_binding<br/><i>key event matching</i>"]
      KB --> BP["BottomPane.set_queued_message_edit_binding()"]
      BP --> QUM["QueuedUserMessages.edit_binding<br/><i>rendered in hint line</i>"]

      subgraph "Special terminals (Shift+Left)"
          AT["Apple Terminal"]
          WT["Warp"]
          VS["VSCode"]
      end

      subgraph "Default (Alt+Up)"
          GH["Ghostty"]
          IT["iTerm2"]
          OT["Others…"]
      end

      AT --> FN
      WT --> FN
      VS --> FN
      GH --> FN
      IT --> FN
      OT --> FN
```

No new crates or public API surface. The only cross-crate dependency
added is `codex_core::terminal::{TerminalName, terminal_info}`, which
already existed for telemetry.

## Observability

No new logging. Terminal detection already emits a `tracing::debug!` log
line at startup with the detected terminal name, which is sufficient to
diagnose binding mismatches.

## Tests

- Existing `alt_up_edits_most_recent_queued_message` test is preserved
and explicitly sets the Alt+Up binding to isolate from the host
terminal.
- New parameterized async tests verify Shift+Left works for Apple
Terminal, Warp, and VSCode.
- A sync unit test asserts the mapping table covers the three special
terminals (Shift+Left) and that iTerm2 still gets Alt+Up.
  
Fixes #4490
2026-02-20 16:56:41 -08:00
Matthew Zeng
4ebdddaa34 [apps] Fix gateway url. (#12403)
- [x] Fix connectors gateway url.
2026-02-21 00:47:15 +00:00
Charley Cunningham
021e39b303 Show model/reasoning hint when switching modes (#12307)
## Summary
- show an info message when switching collaboration modes changes the
effective model or reasoning
- include the target mode in the message (for example `... for Plan
mode.`)
- add TUI tests for model-change and reasoning-only change notifications
on mode switch

<img width="715" height="184" alt="Screenshot 2026-02-20 at 2 01 40 PM"
src="https://github.com/user-attachments/assets/18d1beb3-ab87-4e1c-9ada-a10218520420"
/>
2026-02-20 15:22:10 -08:00
sayan-oai
65b9fe8f30 clarify model_catalog_json only applied on startup (#12379)
# External (non-OpenAI) Pull Request Requirements

Before opening this Pull Request, please read the dedicated
"Contributing" markdown file or your PR may be closed:
https://github.com/openai/codex/blob/main/docs/contributing.md

If your PR conforms to our contribution guidelines, replace this text
with a detailed and high quality description of your changes.

Include a link to a bug report or enhancement request.
2026-02-20 15:04:36 -08:00
viyatb-oai
64f3827d10 Move sanitizer into codex-secrets (#12306)
## Summary
- move the sanitizer implementation into `codex-secrets`
(`secrets/src/sanitizer.rs`) and re-export `redact_secrets`
- switch `codex-core` to depend on/import `codex-secrets` for sanitizer
usage
- remove the old `utils/sanitizer` crate wiring and refresh lockfiles

## Testing
- `just fmt`
- `cargo test -p codex-secrets`
- `cargo test -p codex-core --no-run`
- `cargo clippy -p codex-secrets -p codex-core --all-targets
--all-features -- -D warnings`
- `just bazel-lock-update`
- `just bazel-lock-check`

## Notes
- not run: `cargo test --all-features` (full workspace suite)
2026-02-20 22:47:54 +00:00
384 changed files with 42009 additions and 3988 deletions

View File

@@ -0,0 +1,185 @@
---
name: babysit-pr
description: Babysit a GitHub pull request after creation by continuously polling CI checks/workflow runs, new review comments, and mergeability state until the PR is ready to merge (or merged/closed). Diagnose failures, retry likely flaky failures up to 3 times, auto-fix/push branch-related issues when appropriate, and stop only when user help is required (for example CI infrastructure issues, exhausted flaky retries, or ambiguous/blocking situations). Use when the user asks Codex to monitor a PR, watch CI, handle review comments, or keep an eye on failures and feedback on an open PR.
---
# PR Babysitter
## Objective
Babysit a PR persistently until one of these terminal outcomes occurs:
- The PR is merged or closed.
- CI is successful, there are no unaddressed review comments surfaced by the watcher, required review approval is not blocking merge, and there are no potential merge conflicts (PR is mergeable / not reporting conflict risk).
- A situation requires user help (for example CI infrastructure issues, repeated flaky failures after retry budget is exhausted, permission problems, or ambiguity that cannot be resolved safely).
Do not stop merely because a single snapshot returns `idle` while checks are still pending.
## Inputs
Accept any of the following:
- No PR argument: infer the PR from the current branch (`--pr auto`)
- PR number
- PR URL
## Core Workflow
1. When the user asks to "monitor"/"watch"/"babysit" a PR, start with the watcher's continuous mode (`--watch`) unless you are intentionally doing a one-shot diagnostic snapshot.
2. Run the watcher script to snapshot PR/CI/review state (or consume each streamed snapshot from `--watch`).
3. Inspect the `actions` list in the JSON response.
4. If `diagnose_ci_failure` is present, inspect failed run logs and classify the failure.
5. If the failure is likely caused by the current branch, patch code locally, commit, and push.
6. If `process_review_comment` is present, inspect surfaced review items and decide whether to address them.
7. If a review item is actionable and correct, patch code locally, commit, and push.
8. If the failure is likely flaky/unrelated and `retry_failed_checks` is present, rerun failed jobs with `--retry-failed-now`.
9. If both actionable review feedback and `retry_failed_checks` are present, prioritize review feedback first; a new commit will retrigger CI, so avoid rerunning flaky checks on the old SHA unless you intentionally defer the review change.
10. On every loop, verify mergeability / merge-conflict status (for example via `gh pr view`) in addition to CI and review state.
11. After any push or rerun action, immediately return to step 1 and continue polling on the updated SHA/state.
12. If you had been using `--watch` before pausing to patch/commit/push, relaunch `--watch` yourself in the same turn immediately after the push (do not wait for the user to re-invoke the skill).
13. Repeat polling until the PR is green + review-clean + mergeable, `stop_pr_closed` appears, or a user-help-required blocker is reached.
14. Maintain terminal/session ownership: while babysitting is active, keep consuming watcher output in the same turn; do not leave a detached `--watch` process running and then end the turn as if monitoring were complete.
## Commands
### One-shot snapshot
```bash
python3 .codex/skills/babysit-pr/scripts/gh_pr_watch.py --pr auto --once
```
### Continuous watch (JSONL)
```bash
python3 .codex/skills/babysit-pr/scripts/gh_pr_watch.py --pr auto --watch
```
### Trigger flaky retry cycle (only when watcher indicates)
```bash
python3 .codex/skills/babysit-pr/scripts/gh_pr_watch.py --pr auto --retry-failed-now
```
### Explicit PR target
```bash
python3 .codex/skills/babysit-pr/scripts/gh_pr_watch.py --pr <number-or-url> --once
```
## CI Failure Classification
Use `gh` commands to inspect failed runs before deciding to rerun.
- `gh run view <run-id> --json jobs,name,workflowName,conclusion,status,url,headSha`
- `gh run view <run-id> --log-failed`
Prefer treating failures as branch-related when logs point to changed code (compile/test/lint/typecheck/snapshots/static analysis in touched areas).
Prefer treating failures as flaky/unrelated when logs show transient infra/external issues (timeouts, runner provisioning failures, registry/network outages, GitHub Actions infra errors).
If classification is ambiguous, perform one manual diagnosis attempt before choosing rerun.
Read `.codex/skills/babysit-pr/references/heuristics.md` for a concise checklist.
## Review Comment Handling
The watcher surfaces review items from:
- PR issue comments
- Inline review comments
- Review submissions (COMMENT / APPROVED / CHANGES_REQUESTED)
It intentionally surfaces Codex reviewer bot feedback (for example comments/reviews from `chatgpt-codex-connector[bot]`) in addition to human reviewer feedback. Most unrelated bot noise should still be ignored.
For safety, the watcher only auto-surfaces trusted human review authors (for example repo OWNER/MEMBER/COLLABORATOR, plus the authenticated operator) and approved review bots such as Codex.
On a fresh watcher state file, existing pending review feedback may be surfaced immediately (not only comments that arrive after monitoring starts). This is intentional so already-open review comments are not missed.
When you agree with a comment and it is actionable:
1. Patch code locally.
2. Commit with `codex: address PR review feedback (#<n>)`.
3. Push to the PR head branch.
4. Resume watching on the new SHA immediately (do not stop after reporting the push).
5. If monitoring was running in `--watch` mode, restart `--watch` immediately after the push in the same turn; do not wait for the user to ask again.
If you disagree or the comment is non-actionable/already addressed, record it as handled by continuing the watcher loop (the script de-duplicates surfaced items via state after surfacing them).
If a code review comment/thread is already marked as resolved in GitHub, treat it as non-actionable and safely ignore it unless new unresolved follow-up feedback appears.
## Git Safety Rules
- Work only on the PR head branch.
- Avoid destructive git commands.
- Do not switch branches unless necessary to recover context.
- Before editing, check for unrelated uncommitted changes. If present, stop and ask the user.
- After each successful fix, commit and `git push`, then re-run the watcher.
- If you interrupted a live `--watch` session to make the fix, restart `--watch` immediately after the push in the same turn.
- Do not run multiple concurrent `--watch` processes for the same PR/state file; keep one watcher session active and reuse it until it stops or you intentionally restart it.
- A push is not a terminal outcome; continue the monitoring loop unless a strict stop condition is met.
Commit message defaults:
- `codex: fix CI failure on PR #<n>`
- `codex: address PR review feedback (#<n>)`
## Monitoring Loop Pattern
Use this loop in a live Codex session:
1. Run `--once`.
2. Read `actions`.
3. First check whether the PR is now merged or otherwise closed; if so, report that terminal state and stop polling immediately.
4. Check CI summary, new review items, and mergeability/conflict status.
5. Diagnose CI failures and classify branch-related vs flaky/unrelated.
6. Process actionable review comments before flaky reruns when both are present; if a review fix requires a commit, push it and skip rerunning failed checks on the old SHA.
7. Retry failed checks only when `retry_failed_checks` is present and you are not about to replace the current SHA with a review/CI fix commit.
8. If you pushed a commit or triggered a rerun, report the action briefly and continue polling (do not stop).
9. After a review-fix push, proactively restart continuous monitoring (`--watch`) in the same turn unless a strict stop condition has already been reached.
10. If everything is passing, mergeable, not blocked on required review approval, and there are no unaddressed review items, report success and stop.
11. If blocked on a user-help-required issue (infra outage, exhausted flaky retries, unclear reviewer request, permissions), report the blocker and stop.
12. Otherwise sleep according to the polling cadence below and repeat.
When the user explicitly asks to monitor/watch/babysit a PR, prefer `--watch` so polling continues autonomously in one command. Use repeated `--once` snapshots only for debugging, local testing, or when the user explicitly asks for a one-shot check.
Do not stop to ask the user whether to continue polling; continue autonomously until a strict stop condition is met or the user explicitly interrupts.
Do not hand control back to the user after a review-fix push just because a new SHA was created; restarting the watcher and re-entering the poll loop is part of the same babysitting task.
If a `--watch` process is still running and no strict stop condition has been reached, the babysitting task is still in progress; keep streaming/consuming watcher output instead of ending the turn.
## Polling Cadence
Use adaptive polling and continue monitoring even after CI turns green:
- While CI is not green (pending/running/queued or failing): poll every 1 minute.
- After CI turns green: start at every 1 minute, then back off exponentially when there is no change (for example 1m, 2m, 4m, 8m, 16m, 32m), capping at every 1 hour.
- Reset the green-state polling interval back to 1 minute whenever anything changes (new commit/SHA, check status changes, new review comments, mergeability changes, review decision changes).
- If CI stops being green again (new commit, rerun, or regression): return to 1-minute polling.
- If any poll shows the PR is merged or otherwise closed: stop polling immediately and report the terminal state.
## Stop Conditions (Strict)
Stop only when one of the following is true:
- PR merged or closed (stop as soon as a poll/snapshot confirms this).
- PR is ready to merge: CI succeeded, no surfaced unaddressed review comments, not blocked on required review approval, and no merge conflict risk.
- User intervention is required and Codex cannot safely proceed alone.
Keep polling when:
- `actions` contains only `idle` but checks are still pending.
- CI is still running/queued.
- Review state is quiet but CI is not terminal.
- CI is green but mergeability is unknown/pending.
- CI is green and mergeable, but the PR is still open and you are waiting for possible new review comments or merge-conflict changes per the green-state cadence.
- The PR is green but blocked on review approval (`REVIEW_REQUIRED` / similar); continue polling on the green-state cadence and surface any new review comments without asking for confirmation to keep watching.
## Output Expectations
Provide concise progress updates while monitoring and a final summary that includes:
- During long unchanged monitoring periods, avoid emitting a full update on every poll; summarize only status changes plus occasional heartbeat updates.
- Treat push confirmations, intermediate CI snapshots, and review-action updates as progress updates only; do not emit the final summary or end the babysitting session unless a strict stop condition is met.
- A user request to "monitor" is not satisfied by a couple of sample polls; remain in the loop until a strict stop condition or an explicit user interruption.
- A review-fix commit + push is not a completion event; immediately resume live monitoring (`--watch`) in the same turn and continue reporting progress updates.
- When CI first transitions to all green for the current SHA, emit a one-time celebratory progress update (do not repeat it on every green poll). Preferred style: `🚀 CI is all green! 33/33 passed. Still on watch for review approval.`
- Do not send the final summary while a watcher terminal is still running unless the watcher has emitted/confirmed a strict stop condition; otherwise continue with progress updates.
- Final PR SHA
- CI status summary
- Mergeability / conflict status
- Fixes pushed
- Flaky retry cycles used
- Remaining unresolved failures or review comments
## References
- Heuristics and decision tree: `.codex/skills/babysit-pr/references/heuristics.md`
- GitHub CLI/API details used by the watcher: `.codex/skills/babysit-pr/references/github-api-notes.md`

View File

@@ -0,0 +1,4 @@
interface:
display_name: "PR Babysitter"
short_description: "Watch PR CI, reviews, and merge conflicts"
default_prompt: "Babysit the current PR: monitor CI, reviewer comments, and merge-conflict status (prefer the watchers --watch mode for live monitoring); fix valid issues, push updates, and rerun flaky failures up to 3 times. Keep exactly one watcher session active for the PR (do not leave duplicate --watch terminals running). If you pause monitoring to patch review/CI feedback, restart --watch yourself immediately after the push in the same turn. If a watcher is still running and no strict stop condition has been reached, the task is still in progress: keep consuming watcher output and sending progress updates instead of ending the turn. Continue polling autonomously after any push/rerun until a strict terminal stop condition is reached or the user interrupts."

View File

@@ -0,0 +1,72 @@
# GitHub CLI / API Notes For `babysit-pr`
## Primary commands used
### PR metadata
- `gh pr view --json number,url,state,mergedAt,closedAt,headRefName,headRefOid,headRepository,headRepositoryOwner`
Used to resolve PR number, URL, branch, head SHA, and closed/merged state.
### PR checks summary
- `gh pr checks --json name,state,bucket,link,workflow,event,startedAt,completedAt`
Used to compute pending/failed/passed counts and whether the current CI round is terminal.
### Workflow runs for head SHA
- `gh api repos/{owner}/{repo}/actions/runs -X GET -f head_sha=<sha> -f per_page=100`
Used to discover failed workflow runs and rerunnable run IDs.
### Failed log inspection
- `gh run view <run-id> --json jobs,name,workflowName,conclusion,status,url,headSha`
- `gh run view <run-id> --log-failed`
Used by Codex to classify branch-related vs flaky/unrelated failures.
### Retry failed jobs only
- `gh run rerun <run-id> --failed`
Reruns only failed jobs (and dependencies) for a workflow run.
## Review-related endpoints
- Issue comments on PR:
- `gh api repos/{owner}/{repo}/issues/<pr_number>/comments?per_page=100`
- Inline PR review comments:
- `gh api repos/{owner}/{repo}/pulls/<pr_number>/comments?per_page=100`
- Review submissions:
- `gh api repos/{owner}/{repo}/pulls/<pr_number>/reviews?per_page=100`
## JSON fields consumed by the watcher
### `gh pr view`
- `number`
- `url`
- `state`
- `mergedAt`
- `closedAt`
- `headRefName`
- `headRefOid`
### `gh pr checks`
- `bucket` (`pass`, `fail`, `pending`, `skipping`)
- `state`
- `name`
- `workflow`
- `link`
### Actions runs API (`workflow_runs[]`)
- `id`
- `name`
- `status`
- `conclusion`
- `html_url`
- `head_sha`

View File

@@ -0,0 +1,58 @@
# CI / Review Heuristics
## CI classification checklist
Treat as **branch-related** when logs clearly indicate a regression caused by the PR branch:
- Compile/typecheck/lint failures in files or modules touched by the branch
- Deterministic unit/integration test failures in changed areas
- Snapshot output changes caused by UI/text changes in the branch
- Static analysis violations introduced by the latest push
- Build script/config changes in the PR causing a deterministic failure
Treat as **likely flaky or unrelated** when evidence points to transient or external issues:
- DNS/network/registry timeout errors while fetching dependencies
- Runner image provisioning or startup failures
- GitHub Actions infrastructure/service outages
- Cloud/service rate limits or transient API outages
- Non-deterministic failures in unrelated integration tests with known flake patterns
If uncertain, inspect failed logs once before choosing rerun.
## Decision tree (fix vs rerun vs stop)
1. If PR is merged/closed: stop.
2. If there are failed checks:
- Diagnose first.
- If branch-related: fix locally, commit, push.
- If likely flaky/unrelated and all checks for the current SHA are terminal: rerun failed jobs.
- If checks are still pending: wait.
3. If flaky reruns for the same SHA reach the configured limit (default 3): stop and report persistent failure.
4. Independently, process any new human review comments.
## Review comment agreement criteria
Address the comment when:
- The comment is technically correct.
- The change is actionable in the current branch.
- The requested change does not conflict with the users intent or recent guidance.
- The change can be made safely without unrelated refactors.
Do not auto-fix when:
- The comment is ambiguous and needs clarification.
- The request conflicts with explicit user instructions.
- The proposed change requires product/design decisions the user has not made.
- The codebase is in a dirty/unrelated state that makes safe editing uncertain.
## Stop-and-ask conditions
Stop and ask the user instead of continuing automatically when:
- The local worktree has unrelated uncommitted changes.
- `gh` auth/permissions fail.
- The PR branch cannot be pushed.
- CI failures persist after the flaky retry budget.
- Reviewer feedback requires a product decision or cross-team coordination.

View File

@@ -0,0 +1,805 @@
#!/usr/bin/env python3
"""Watch GitHub PR CI and review activity for Codex PR babysitting workflows."""
import argparse
import json
import os
import re
import subprocess
import sys
import tempfile
import time
from pathlib import Path
from urllib.parse import urlparse
FAILED_RUN_CONCLUSIONS = {
"failure",
"timed_out",
"cancelled",
"action_required",
"startup_failure",
"stale",
}
PENDING_CHECK_STATES = {
"QUEUED",
"IN_PROGRESS",
"PENDING",
"WAITING",
"REQUESTED",
}
REVIEW_BOT_LOGIN_KEYWORDS = {
"codex",
}
TRUSTED_AUTHOR_ASSOCIATIONS = {
"OWNER",
"MEMBER",
"COLLABORATOR",
}
MERGE_BLOCKING_REVIEW_DECISIONS = {
"REVIEW_REQUIRED",
"CHANGES_REQUESTED",
}
MERGE_CONFLICT_OR_BLOCKING_STATES = {
"BLOCKED",
"DIRTY",
"DRAFT",
"UNKNOWN",
}
GREEN_STATE_MAX_POLL_SECONDS = 60 * 60
class GhCommandError(RuntimeError):
pass
def parse_args():
parser = argparse.ArgumentParser(
description=(
"Normalize PR/CI/review state for Codex PR babysitting and optionally "
"trigger flaky reruns."
)
)
parser.add_argument("--pr", default="auto", help="auto, PR number, or PR URL")
parser.add_argument("--repo", help="Optional OWNER/REPO override")
parser.add_argument("--poll-seconds", type=int, default=30, help="Watch poll interval")
parser.add_argument(
"--max-flaky-retries",
type=int,
default=3,
help="Max rerun cycles per head SHA before stop recommendation",
)
parser.add_argument("--state-file", help="Path to state JSON file")
parser.add_argument("--once", action="store_true", help="Emit one snapshot and exit")
parser.add_argument("--watch", action="store_true", help="Continuously emit JSONL snapshots")
parser.add_argument(
"--retry-failed-now",
action="store_true",
help="Rerun failed jobs for current failed workflow runs when policy allows",
)
parser.add_argument(
"--json",
action="store_true",
help="Emit machine-readable output (default behavior for --once and --retry-failed-now)",
)
args = parser.parse_args()
if args.poll_seconds <= 0:
parser.error("--poll-seconds must be > 0")
if args.max_flaky_retries < 0:
parser.error("--max-flaky-retries must be >= 0")
if args.watch and args.retry_failed_now:
parser.error("--watch cannot be combined with --retry-failed-now")
if not args.once and not args.watch and not args.retry_failed_now:
args.once = True
return args
def _format_gh_error(cmd, err):
stdout = (err.stdout or "").strip()
stderr = (err.stderr or "").strip()
parts = [f"GitHub CLI command failed: {' '.join(cmd)}"]
if stdout:
parts.append(f"stdout: {stdout}")
if stderr:
parts.append(f"stderr: {stderr}")
return "\n".join(parts)
def gh_text(args, repo=None):
cmd = ["gh"]
# `gh api` does not accept `-R/--repo` on all gh versions. The watcher's
# API calls use explicit endpoints (e.g. repos/{owner}/{repo}/...), so the
# repo flag is unnecessary there.
if repo and (not args or args[0] != "api"):
cmd.extend(["-R", repo])
cmd.extend(args)
try:
proc = subprocess.run(cmd, check=True, capture_output=True, text=True)
except FileNotFoundError as err:
raise GhCommandError("`gh` command not found") from err
except subprocess.CalledProcessError as err:
raise GhCommandError(_format_gh_error(cmd, err)) from err
return proc.stdout
def gh_json(args, repo=None):
raw = gh_text(args, repo=repo).strip()
if not raw:
return None
try:
return json.loads(raw)
except json.JSONDecodeError as err:
raise GhCommandError(f"Failed to parse JSON from gh output for {' '.join(args)}") from err
def parse_pr_spec(pr_spec):
if pr_spec == "auto":
return {"mode": "auto", "value": None}
if re.fullmatch(r"\d+", pr_spec):
return {"mode": "number", "value": pr_spec}
parsed = urlparse(pr_spec)
if parsed.scheme and parsed.netloc and "/pull/" in parsed.path:
return {"mode": "url", "value": pr_spec}
raise ValueError("--pr must be 'auto', a PR number, or a PR URL")
def pr_view_fields():
return (
"number,url,state,mergedAt,closedAt,headRefName,headRefOid,"
"headRepository,headRepositoryOwner,mergeable,mergeStateStatus,reviewDecision"
)
def checks_fields():
return "name,state,bucket,link,workflow,event,startedAt,completedAt"
def resolve_pr(pr_spec, repo_override=None):
parsed = parse_pr_spec(pr_spec)
cmd = ["pr", "view"]
if parsed["value"] is not None:
cmd.append(parsed["value"])
cmd.extend(["--json", pr_view_fields()])
data = gh_json(cmd, repo=repo_override)
if not isinstance(data, dict):
raise GhCommandError("Unexpected PR payload from `gh pr view`")
pr_url = str(data.get("url") or "")
repo = (
repo_override
or extract_repo_from_pr_url(pr_url)
or extract_repo_from_pr_view(data)
)
if not repo:
raise GhCommandError("Unable to determine OWNER/REPO for the PR")
state = str(data.get("state") or "")
merged = bool(data.get("mergedAt"))
closed = bool(data.get("closedAt")) or state.upper() == "CLOSED"
return {
"number": int(data["number"]),
"url": pr_url,
"repo": repo,
"head_sha": str(data.get("headRefOid") or ""),
"head_branch": str(data.get("headRefName") or ""),
"state": state,
"merged": merged,
"closed": closed,
"mergeable": str(data.get("mergeable") or ""),
"merge_state_status": str(data.get("mergeStateStatus") or ""),
"review_decision": str(data.get("reviewDecision") or ""),
}
def extract_repo_from_pr_view(data):
head_repo = data.get("headRepository")
head_owner = data.get("headRepositoryOwner")
owner = None
name = None
if isinstance(head_owner, dict):
owner = head_owner.get("login") or head_owner.get("name")
elif isinstance(head_owner, str):
owner = head_owner
if isinstance(head_repo, dict):
name = head_repo.get("name")
repo_owner = head_repo.get("owner")
if not owner and isinstance(repo_owner, dict):
owner = repo_owner.get("login") or repo_owner.get("name")
elif isinstance(head_repo, str):
name = head_repo
if owner and name:
return f"{owner}/{name}"
return None
def extract_repo_from_pr_url(pr_url):
parsed = urlparse(pr_url)
parts = [p for p in parsed.path.split("/") if p]
if len(parts) >= 4 and parts[2] == "pull":
return f"{parts[0]}/{parts[1]}"
return None
def load_state(path):
if path.exists():
try:
data = json.loads(path.read_text())
except json.JSONDecodeError as err:
raise RuntimeError(f"State file is not valid JSON: {path}") from err
if not isinstance(data, dict):
raise RuntimeError(f"State file must contain an object: {path}")
return data, False
return {
"pr": {},
"started_at": None,
"last_seen_head_sha": None,
"retries_by_sha": {},
"seen_issue_comment_ids": [],
"seen_review_comment_ids": [],
"seen_review_ids": [],
"last_snapshot_at": None,
}, True
def save_state(path, state):
path.parent.mkdir(parents=True, exist_ok=True)
payload = json.dumps(state, indent=2, sort_keys=True) + "\n"
fd, tmp_name = tempfile.mkstemp(prefix=f"{path.name}.", suffix=".tmp", dir=path.parent)
tmp_path = Path(tmp_name)
try:
with os.fdopen(fd, "w", encoding="utf-8") as tmp_file:
tmp_file.write(payload)
os.replace(tmp_path, path)
except Exception:
try:
tmp_path.unlink(missing_ok=True)
except OSError:
pass
raise
def default_state_file_for(pr):
repo_slug = pr["repo"].replace("/", "-")
return Path(f"/tmp/codex-babysit-pr-{repo_slug}-pr{pr['number']}.json")
def get_pr_checks(pr_spec, repo):
parsed = parse_pr_spec(pr_spec)
cmd = ["pr", "checks"]
if parsed["value"] is not None:
cmd.append(parsed["value"])
cmd.extend(["--json", checks_fields()])
data = gh_json(cmd, repo=repo)
if data is None:
return []
if not isinstance(data, list):
raise GhCommandError("Unexpected payload from `gh pr checks`")
return data
def is_pending_check(check):
bucket = str(check.get("bucket") or "").lower()
state = str(check.get("state") or "").upper()
return bucket == "pending" or state in PENDING_CHECK_STATES
def summarize_checks(checks):
pending_count = 0
failed_count = 0
passed_count = 0
for check in checks:
bucket = str(check.get("bucket") or "").lower()
if is_pending_check(check):
pending_count += 1
if bucket == "fail":
failed_count += 1
if bucket == "pass":
passed_count += 1
return {
"pending_count": pending_count,
"failed_count": failed_count,
"passed_count": passed_count,
"all_terminal": pending_count == 0,
}
def get_workflow_runs_for_sha(repo, head_sha):
endpoint = f"repos/{repo}/actions/runs"
data = gh_json(
["api", endpoint, "-X", "GET", "-f", f"head_sha={head_sha}", "-f", "per_page=100"],
repo=repo,
)
if not isinstance(data, dict):
raise GhCommandError("Unexpected payload from actions runs API")
runs = data.get("workflow_runs") or []
if not isinstance(runs, list):
raise GhCommandError("Expected `workflow_runs` to be a list")
return runs
def failed_runs_from_workflow_runs(runs, head_sha):
failed_runs = []
for run in runs:
if not isinstance(run, dict):
continue
if str(run.get("head_sha") or "") != head_sha:
continue
conclusion = str(run.get("conclusion") or "")
if conclusion not in FAILED_RUN_CONCLUSIONS:
continue
failed_runs.append(
{
"run_id": run.get("id"),
"workflow_name": run.get("name") or run.get("display_title") or "",
"status": str(run.get("status") or ""),
"conclusion": conclusion,
"html_url": str(run.get("html_url") or ""),
}
)
failed_runs.sort(key=lambda item: (str(item.get("workflow_name") or ""), str(item.get("run_id") or "")))
return failed_runs
def get_authenticated_login():
data = gh_json(["api", "user"])
if not isinstance(data, dict) or not data.get("login"):
raise GhCommandError("Unable to determine authenticated GitHub login from `gh api user`")
return str(data["login"])
def comment_endpoints(repo, pr_number):
return {
"issue_comment": f"repos/{repo}/issues/{pr_number}/comments",
"review_comment": f"repos/{repo}/pulls/{pr_number}/comments",
"review": f"repos/{repo}/pulls/{pr_number}/reviews",
}
def gh_api_list_paginated(endpoint, repo=None, per_page=100):
items = []
page = 1
while True:
sep = "&" if "?" in endpoint else "?"
page_endpoint = f"{endpoint}{sep}per_page={per_page}&page={page}"
payload = gh_json(["api", page_endpoint], repo=repo)
if payload is None:
break
if not isinstance(payload, list):
raise GhCommandError(f"Unexpected paginated payload from gh api {endpoint}")
items.extend(payload)
if len(payload) < per_page:
break
page += 1
return items
def normalize_issue_comments(items):
out = []
for item in items:
if not isinstance(item, dict):
continue
out.append(
{
"kind": "issue_comment",
"id": str(item.get("id") or ""),
"author": extract_login(item.get("user")),
"author_association": str(item.get("author_association") or ""),
"created_at": str(item.get("created_at") or ""),
"body": str(item.get("body") or ""),
"path": None,
"line": None,
"url": str(item.get("html_url") or ""),
}
)
return out
def normalize_review_comments(items):
out = []
for item in items:
if not isinstance(item, dict):
continue
line = item.get("line")
if line is None:
line = item.get("original_line")
out.append(
{
"kind": "review_comment",
"id": str(item.get("id") or ""),
"author": extract_login(item.get("user")),
"author_association": str(item.get("author_association") or ""),
"created_at": str(item.get("created_at") or ""),
"body": str(item.get("body") or ""),
"path": item.get("path"),
"line": line,
"url": str(item.get("html_url") or ""),
}
)
return out
def normalize_reviews(items):
out = []
for item in items:
if not isinstance(item, dict):
continue
out.append(
{
"kind": "review",
"id": str(item.get("id") or ""),
"author": extract_login(item.get("user")),
"author_association": str(item.get("author_association") or ""),
"created_at": str(item.get("submitted_at") or item.get("created_at") or ""),
"body": str(item.get("body") or ""),
"path": None,
"line": None,
"url": str(item.get("html_url") or ""),
}
)
return out
def extract_login(user_obj):
if isinstance(user_obj, dict):
return str(user_obj.get("login") or "")
return ""
def is_bot_login(login):
return bool(login) and login.endswith("[bot]")
def is_actionable_review_bot_login(login):
if not is_bot_login(login):
return False
lower_login = login.lower()
return any(keyword in lower_login for keyword in REVIEW_BOT_LOGIN_KEYWORDS)
def is_trusted_human_review_author(item, authenticated_login):
author = str(item.get("author") or "")
if not author:
return False
if authenticated_login and author == authenticated_login:
return True
association = str(item.get("author_association") or "").upper()
return association in TRUSTED_AUTHOR_ASSOCIATIONS
def fetch_new_review_items(pr, state, fresh_state, authenticated_login=None):
repo = pr["repo"]
pr_number = pr["number"]
endpoints = comment_endpoints(repo, pr_number)
issue_payload = gh_api_list_paginated(endpoints["issue_comment"], repo=repo)
review_comment_payload = gh_api_list_paginated(endpoints["review_comment"], repo=repo)
review_payload = gh_api_list_paginated(endpoints["review"], repo=repo)
issue_items = normalize_issue_comments(issue_payload)
review_comment_items = normalize_review_comments(review_comment_payload)
review_items = normalize_reviews(review_payload)
all_items = issue_items + review_comment_items + review_items
seen_issue = {str(x) for x in state.get("seen_issue_comment_ids") or []}
seen_review_comment = {str(x) for x in state.get("seen_review_comment_ids") or []}
seen_review = {str(x) for x in state.get("seen_review_ids") or []}
# On a brand-new state file, surface existing review activity instead of
# silently treating it as seen. This avoids missing already-pending review
# feedback when monitoring starts after comments were posted.
new_items = []
for item in all_items:
item_id = item.get("id")
if not item_id:
continue
author = item.get("author") or ""
if not author:
continue
if is_bot_login(author):
if not is_actionable_review_bot_login(author):
continue
elif not is_trusted_human_review_author(item, authenticated_login):
continue
kind = item["kind"]
if kind == "issue_comment" and item_id in seen_issue:
continue
if kind == "review_comment" and item_id in seen_review_comment:
continue
if kind == "review" and item_id in seen_review:
continue
new_items.append(item)
if kind == "issue_comment":
seen_issue.add(item_id)
elif kind == "review_comment":
seen_review_comment.add(item_id)
elif kind == "review":
seen_review.add(item_id)
new_items.sort(key=lambda item: (item.get("created_at") or "", item.get("kind") or "", item.get("id") or ""))
state["seen_issue_comment_ids"] = sorted(seen_issue)
state["seen_review_comment_ids"] = sorted(seen_review_comment)
state["seen_review_ids"] = sorted(seen_review)
return new_items
def current_retry_count(state, head_sha):
retries = state.get("retries_by_sha") or {}
value = retries.get(head_sha, 0)
try:
return int(value)
except (TypeError, ValueError):
return 0
def set_retry_count(state, head_sha, count):
retries = state.get("retries_by_sha")
if not isinstance(retries, dict):
retries = {}
retries[head_sha] = int(count)
state["retries_by_sha"] = retries
def unique_actions(actions):
out = []
seen = set()
for action in actions:
if action not in seen:
out.append(action)
seen.add(action)
return out
def is_pr_ready_to_merge(pr, checks_summary, new_review_items):
if pr["closed"] or pr["merged"]:
return False
if not checks_summary["all_terminal"]:
return False
if checks_summary["failed_count"] > 0 or checks_summary["pending_count"] > 0:
return False
if new_review_items:
return False
if str(pr.get("mergeable") or "") != "MERGEABLE":
return False
if str(pr.get("merge_state_status") or "") in MERGE_CONFLICT_OR_BLOCKING_STATES:
return False
if str(pr.get("review_decision") or "") in MERGE_BLOCKING_REVIEW_DECISIONS:
return False
return True
def recommend_actions(pr, checks_summary, failed_runs, new_review_items, retries_used, max_retries):
actions = []
if pr["closed"] or pr["merged"]:
if new_review_items:
actions.append("process_review_comment")
actions.append("stop_pr_closed")
return unique_actions(actions)
if is_pr_ready_to_merge(pr, checks_summary, new_review_items):
actions.append("stop_ready_to_merge")
return unique_actions(actions)
if new_review_items:
actions.append("process_review_comment")
has_failed_pr_checks = checks_summary["failed_count"] > 0
if has_failed_pr_checks:
if checks_summary["all_terminal"] and retries_used >= max_retries:
actions.append("stop_exhausted_retries")
else:
actions.append("diagnose_ci_failure")
if checks_summary["all_terminal"] and failed_runs and retries_used < max_retries:
actions.append("retry_failed_checks")
if not actions:
actions.append("idle")
return unique_actions(actions)
def collect_snapshot(args):
pr = resolve_pr(args.pr, repo_override=args.repo)
state_path = Path(args.state_file) if args.state_file else default_state_file_for(pr)
state, fresh_state = load_state(state_path)
if not state.get("started_at"):
state["started_at"] = int(time.time())
# `gh pr checks -R <repo>` requires an explicit PR/branch/url argument.
# After resolving `--pr auto`, reuse the concrete PR number.
checks = get_pr_checks(str(pr["number"]), repo=pr["repo"])
checks_summary = summarize_checks(checks)
workflow_runs = get_workflow_runs_for_sha(pr["repo"], pr["head_sha"])
failed_runs = failed_runs_from_workflow_runs(workflow_runs, pr["head_sha"])
authenticated_login = get_authenticated_login()
new_review_items = fetch_new_review_items(
pr,
state,
fresh_state=fresh_state,
authenticated_login=authenticated_login,
)
retries_used = current_retry_count(state, pr["head_sha"])
actions = recommend_actions(
pr,
checks_summary,
failed_runs,
new_review_items,
retries_used,
args.max_flaky_retries,
)
state["pr"] = {"repo": pr["repo"], "number": pr["number"]}
state["last_seen_head_sha"] = pr["head_sha"]
state["last_snapshot_at"] = int(time.time())
save_state(state_path, state)
snapshot = {
"pr": pr,
"checks": checks_summary,
"failed_runs": failed_runs,
"new_review_items": new_review_items,
"actions": actions,
"retry_state": {
"current_sha_retries_used": retries_used,
"max_flaky_retries": args.max_flaky_retries,
},
}
return snapshot, state_path
def retry_failed_now(args):
snapshot, state_path = collect_snapshot(args)
pr = snapshot["pr"]
checks_summary = snapshot["checks"]
failed_runs = snapshot["failed_runs"]
retries_used = snapshot["retry_state"]["current_sha_retries_used"]
max_retries = snapshot["retry_state"]["max_flaky_retries"]
result = {
"snapshot": snapshot,
"state_file": str(state_path),
"rerun_attempted": False,
"rerun_count": 0,
"rerun_run_ids": [],
"reason": None,
}
if pr["closed"] or pr["merged"]:
result["reason"] = "pr_closed"
return result
if checks_summary["failed_count"] <= 0:
result["reason"] = "no_failed_pr_checks"
return result
if not failed_runs:
result["reason"] = "no_failed_runs"
return result
if not checks_summary["all_terminal"]:
result["reason"] = "checks_still_pending"
return result
if retries_used >= max_retries:
result["reason"] = "retry_budget_exhausted"
return result
for run in failed_runs:
run_id = run.get("run_id")
if run_id in (None, ""):
continue
gh_text(["run", "rerun", str(run_id), "--failed"], repo=pr["repo"])
result["rerun_run_ids"].append(run_id)
if result["rerun_run_ids"]:
state, _ = load_state(state_path)
new_count = current_retry_count(state, pr["head_sha"]) + 1
set_retry_count(state, pr["head_sha"], new_count)
state["last_snapshot_at"] = int(time.time())
save_state(state_path, state)
result["rerun_attempted"] = True
result["rerun_count"] = len(result["rerun_run_ids"])
result["reason"] = "rerun_triggered"
else:
result["reason"] = "failed_runs_missing_ids"
return result
def print_json(obj):
sys.stdout.write(json.dumps(obj, sort_keys=True) + "\n")
sys.stdout.flush()
def print_event(event, payload):
print_json({"event": event, "payload": payload})
def is_ci_green(snapshot):
checks = snapshot.get("checks") or {}
return (
bool(checks.get("all_terminal"))
and int(checks.get("failed_count") or 0) == 0
and int(checks.get("pending_count") or 0) == 0
)
def snapshot_change_key(snapshot):
pr = snapshot.get("pr") or {}
checks = snapshot.get("checks") or {}
review_items = snapshot.get("new_review_items") or []
return (
str(pr.get("head_sha") or ""),
str(pr.get("state") or ""),
str(pr.get("mergeable") or ""),
str(pr.get("merge_state_status") or ""),
str(pr.get("review_decision") or ""),
int(checks.get("passed_count") or 0),
int(checks.get("failed_count") or 0),
int(checks.get("pending_count") or 0),
tuple(
(str(item.get("kind") or ""), str(item.get("id") or ""))
for item in review_items
if isinstance(item, dict)
),
tuple(snapshot.get("actions") or []),
)
def run_watch(args):
poll_seconds = args.poll_seconds
last_change_key = None
while True:
snapshot, state_path = collect_snapshot(args)
print_event(
"snapshot",
{
"snapshot": snapshot,
"state_file": str(state_path),
"next_poll_seconds": poll_seconds,
},
)
actions = set(snapshot.get("actions") or [])
if (
"stop_pr_closed" in actions
or "stop_exhausted_retries" in actions
or "stop_ready_to_merge" in actions
):
print_event("stop", {"actions": snapshot.get("actions"), "pr": snapshot.get("pr")})
return 0
current_change_key = snapshot_change_key(snapshot)
changed = current_change_key != last_change_key
green = is_ci_green(snapshot)
if not green:
poll_seconds = args.poll_seconds
elif changed or last_change_key is None:
poll_seconds = args.poll_seconds
else:
poll_seconds = min(poll_seconds * 2, GREEN_STATE_MAX_POLL_SECONDS)
last_change_key = current_change_key
time.sleep(poll_seconds)
def main():
args = parse_args()
try:
if args.retry_failed_now:
print_json(retry_failed_now(args))
return 0
if args.watch:
return run_watch(args)
snapshot, state_path = collect_snapshot(args)
snapshot["state_file"] = str(state_path)
print_json(snapshot)
return 0
except (GhCommandError, RuntimeError, ValueError) as err:
sys.stderr.write(f"gh_pr_watch.py error: {err}\n")
return 1
except KeyboardInterrupt:
sys.stderr.write("gh_pr_watch.py interrupted\n")
return 130
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -24,7 +24,7 @@ In the codex-rs folder where the rust code lives:
Run `just fmt` (in `codex-rs` directory) automatically after you have finished making Rust code changes; do not ask for approval to run it. Additionally, run the tests:
1. Run the test for the specific project that was changed. For example, if changes were made in `codex-rs/tui`, run `cargo test -p codex-tui`.
2. Once those pass, if any changes were made in common, core, or protocol, run the complete test suite with `cargo test --all-features`. project-specific or individual tests can be run without asking the user, but do ask the user before running the complete test suite.
2. Once those pass, if any changes were made in common, core, or protocol, run the complete test suite with `cargo test` (or `just test` if `cargo-nextest` is installed). Avoid `--all-features` for routine local runs because it expands the build matrix and can significantly increase `target/` disk usage; use it only when you specifically need full feature coverage. project-specific or individual tests can be run without asking the user, but do ask the user before running the complete test suite.
Before finalizing a large change to `codex-rs`, run `just fix -p <project>` (in `codex-rs` directory) to fix any linter issues in the code. Prefer scoping with `-p` to avoid slow workspacewide Clippy builds; only run `just fix` without `-p` if you changed shared crates. Do not re-run tests after running `fix` or `fmt`.

9
MODULE.bazel.lock generated
View File

@@ -646,6 +646,7 @@
"basic-toml_0.1.10": "{\"dependencies\":[{\"features\":[\"serde\"],\"kind\":\"dev\",\"name\":\"semver\",\"req\":\"^1.0.17\"},{\"name\":\"serde\",\"req\":\"^1.0.194\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.194\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.194\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.99\"}],\"features\":{}}",
"bech32_0.9.1": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[],\"strict\":[]}}",
"beef_0.5.2": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.105\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.105\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"const_fn\":[],\"default\":[],\"impl_serde\":[\"serde\"]}}",
"bincode_1.3.3": "{\"dependencies\":[{\"name\":\"serde\",\"req\":\"^1.0.63\"},{\"kind\":\"dev\",\"name\":\"serde_bytes\",\"req\":\"^0.11\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.27\"}],\"features\":{\"i128\":[]}}",
"bit-set_0.5.3": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bit-vec\",\"req\":\"^0.6.1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.3\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"bit-vec/std\"]}}",
"bit-vec_0.6.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"rand_xorshift\",\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"serde_no_std\":[\"serde/alloc\"],\"serde_std\":[\"std\",\"serde/std\"],\"std\":[]}}",
"bitflags_1.3.2": "{\"dependencies\":[{\"name\":\"compiler_builtins\",\"optional\":true,\"req\":\"^0.1.2\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.3\"}],\"features\":{\"default\":[],\"example_generated\":[],\"rustc-dep-of-std\":[\"core\",\"compiler_builtins\"]}}",
@@ -954,6 +955,7 @@
"libredox_0.1.12": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2\"},{\"name\":\"ioslice\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"redox_syscall\",\"optional\":true,\"req\":\"^0.7\"}],\"features\":{\"call\":[],\"default\":[\"call\",\"std\",\"redox_syscall\"],\"mkns\":[\"ioslice\"],\"std\":[]}}",
"libsqlite3-sys_0.30.1": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"runtime\"],\"kind\":\"build\",\"name\":\"bindgen\",\"optional\":true,\"req\":\"^0.69\"},{\"kind\":\"build\",\"name\":\"cc\",\"optional\":true,\"req\":\"^1.1.6\"},{\"name\":\"openssl-sys\",\"optional\":true,\"req\":\"^0.9.103\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"optional\":true,\"req\":\"^0.3.19\"},{\"kind\":\"build\",\"name\":\"prettyplease\",\"optional\":true,\"req\":\"^0.2.20\"},{\"default_features\":false,\"kind\":\"build\",\"name\":\"quote\",\"optional\":true,\"req\":\"^1.0.36\"},{\"features\":[\"full\",\"extra-traits\",\"visit-mut\"],\"kind\":\"build\",\"name\":\"syn\",\"optional\":true,\"req\":\"^2.0.72\"},{\"kind\":\"build\",\"name\":\"vcpkg\",\"optional\":true,\"req\":\"^0.2.15\"}],\"features\":{\"buildtime_bindgen\":[\"bindgen\",\"pkg-config\",\"vcpkg\"],\"bundled\":[\"cc\",\"bundled_bindings\"],\"bundled-sqlcipher\":[\"bundled\"],\"bundled-sqlcipher-vendored-openssl\":[\"bundled-sqlcipher\",\"openssl-sys/vendored\"],\"bundled-windows\":[\"cc\",\"bundled_bindings\"],\"bundled_bindings\":[],\"default\":[\"min_sqlite_version_3_14_0\"],\"in_gecko\":[],\"loadable_extension\":[\"prettyplease\",\"quote\",\"syn\"],\"min_sqlite_version_3_14_0\":[\"pkg-config\",\"vcpkg\"],\"preupdate_hook\":[\"buildtime_bindgen\"],\"session\":[\"preupdate_hook\",\"buildtime_bindgen\"],\"sqlcipher\":[],\"unlock_notify\":[],\"wasm32-wasi-vfs\":[],\"with-asan\":[]}}",
"libz-sys_1.1.23": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.98\"},{\"kind\":\"build\",\"name\":\"cmake\",\"optional\":true,\"req\":\"^0.1.50\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.43\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.9\"},{\"kind\":\"build\",\"name\":\"vcpkg\",\"req\":\"^0.2.11\"}],\"features\":{\"asm\":[],\"default\":[\"libc\",\"stock-zlib\"],\"static\":[],\"stock-zlib\":[],\"zlib-ng\":[\"libc\",\"cmake\"],\"zlib-ng-no-cmake-experimental-community-maintained\":[\"libc\"]}}",
"linked-hash-map_0.5.6": "{\"dependencies\":[{\"name\":\"heapsize\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{\"heapsize_impl\":[\"heapsize\"],\"nightly\":[],\"serde_impl\":[\"serde\"]}}",
"linux-keyutils_0.2.4": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bitflags\",\"req\":\"^2.4\"},{\"default_features\":false,\"features\":[\"std\",\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.4.11\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.132\"},{\"kind\":\"dev\",\"name\":\"zeroize\",\"req\":\"^1.5.7\"}],\"features\":{\"default\":[],\"std\":[\"bitflags/std\"]}}",
"linux-raw-sys_0.11.0": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.100\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"}],\"features\":{\"auxvec\":[],\"bootparam\":[],\"btrfs\":[],\"default\":[\"std\",\"general\",\"errno\"],\"elf\":[],\"elf_uapi\":[],\"errno\":[],\"general\":[],\"if_arp\":[],\"if_ether\":[],\"if_packet\":[],\"image\":[],\"io_uring\":[],\"ioctl\":[],\"landlock\":[],\"loop_device\":[],\"mempolicy\":[],\"net\":[],\"netlink\":[],\"no_std\":[],\"prctl\":[],\"ptrace\":[],\"rustc-dep-of-std\":[\"core\",\"no_std\"],\"std\":[],\"system\":[],\"xdp\":[]}}",
"linux-raw-sys_0.4.15": "{\"dependencies\":[{\"name\":\"compiler_builtins\",\"optional\":true,\"req\":\"^0.1.49\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.100\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"}],\"features\":{\"bootparam\":[],\"btrfs\":[],\"default\":[\"std\",\"general\",\"errno\"],\"elf\":[],\"elf_uapi\":[],\"errno\":[],\"general\":[],\"if_arp\":[],\"if_ether\":[],\"if_packet\":[],\"io_uring\":[],\"ioctl\":[],\"landlock\":[],\"loop_device\":[],\"mempolicy\":[],\"net\":[],\"netlink\":[],\"no_std\":[],\"prctl\":[],\"ptrace\":[],\"rustc-dep-of-std\":[\"core\",\"compiler_builtins\",\"no_std\"],\"std\":[],\"system\":[],\"xdp\":[]}}",
@@ -1031,6 +1033,8 @@
"oid-registry_0.8.1": "{\"dependencies\":[{\"name\":\"asn1-rs\",\"req\":\"^0.7\"}],\"features\":{\"crypto\":[\"kdf\",\"pkcs1\",\"pkcs7\",\"pkcs9\",\"pkcs12\",\"nist_algs\",\"x962\"],\"default\":[\"registry\"],\"kdf\":[],\"ms_spc\":[],\"nist_algs\":[],\"pkcs1\":[],\"pkcs12\":[],\"pkcs7\":[],\"pkcs9\":[],\"registry\":[],\"x500\":[],\"x509\":[],\"x962\":[]}}",
"once_cell_1.21.3": "{\"dependencies\":[{\"name\":\"critical-section\",\"optional\":true,\"req\":\"^1.1.3\"},{\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"critical-section\",\"req\":\"^1.1.3\"},{\"default_features\":false,\"name\":\"parking_lot_core\",\"optional\":true,\"req\":\"^0.9.10\"},{\"default_features\":false,\"name\":\"portable-atomic\",\"optional\":true,\"req\":\"^1.8\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.10.6\"}],\"features\":{\"alloc\":[\"race\"],\"atomic-polyfill\":[\"critical-section\"],\"critical-section\":[\"dep:critical-section\",\"portable-atomic\"],\"default\":[\"std\"],\"parking_lot\":[\"dep:parking_lot_core\"],\"portable-atomic\":[\"dep:portable-atomic\"],\"race\":[],\"std\":[\"alloc\"],\"unstable\":[]}}",
"once_cell_polyfill_1.70.2": "{\"dependencies\":[],\"features\":{\"default\":[]}}",
"onig_6.5.1": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2.4.0\"},{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(windows)\"},{\"name\":\"once_cell\",\"req\":\"^1.12\"},{\"default_features\":false,\"name\":\"onig_sys\",\"req\":\"^69.9.1\"}],\"features\":{\"default\":[\"generate\"],\"generate\":[\"onig_sys/generate\"],\"posix-api\":[\"onig_sys/posix-api\"],\"print-debug\":[\"onig_sys/print-debug\"],\"std-pattern\":[]}}",
"onig_sys_69.9.1": "{\"dependencies\":[{\"features\":[\"runtime\"],\"kind\":\"build\",\"name\":\"bindgen\",\"optional\":true,\"req\":\"^0.71\"},{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.16\"}],\"features\":{\"default\":[\"generate\"],\"generate\":[\"bindgen\"],\"posix-api\":[],\"print-debug\":[]}}",
"opaque-debug_0.3.1": "{\"dependencies\":[],\"features\":{}}",
"openssl-macros_0.1.1": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}",
"openssl-probe_0.1.6": "{\"dependencies\":[],\"features\":{}}",
@@ -1073,6 +1077,7 @@
"pkcs1_0.7.5": "{\"dependencies\":[{\"features\":[\"db\"],\"kind\":\"dev\",\"name\":\"const-oid\",\"req\":\"^0.9\"},{\"features\":[\"oid\"],\"name\":\"der\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"pkcs8\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"spki\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"alloc\":[\"der/alloc\",\"zeroize\",\"pkcs8?/alloc\"],\"pem\":[\"alloc\",\"der/pem\",\"pkcs8?/pem\"],\"std\":[\"der/std\",\"alloc\"],\"zeroize\":[\"der/zeroize\"]}}",
"pkcs8_0.10.2": "{\"dependencies\":[{\"features\":[\"oid\"],\"name\":\"der\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"},{\"name\":\"pkcs5\",\"optional\":true,\"req\":\"^0.7\"},{\"default_features\":false,\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"spki\",\"req\":\"^0.7.1\"},{\"default_features\":false,\"name\":\"subtle\",\"optional\":true,\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"3des\":[\"encryption\",\"pkcs5/3des\"],\"alloc\":[\"der/alloc\",\"der/zeroize\",\"spki/alloc\"],\"des-insecure\":[\"encryption\",\"pkcs5/des-insecure\"],\"encryption\":[\"alloc\",\"pkcs5/alloc\",\"pkcs5/pbes2\",\"rand_core\"],\"getrandom\":[\"rand_core/getrandom\"],\"pem\":[\"alloc\",\"der/pem\",\"spki/pem\"],\"sha1-insecure\":[\"encryption\",\"pkcs5/sha1-insecure\"],\"std\":[\"alloc\",\"der/std\",\"spki/std\"]}}",
"pkg-config_0.3.32": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"}],\"features\":{}}",
"plist_1.8.0": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.22.0\"},{\"name\":\"indexmap\",\"req\":\"^2.1.0\"},{\"name\":\"quick_xml\",\"package\":\"quick-xml\",\"req\":\"^0.38.0\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.2\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.2\"},{\"kind\":\"dev\",\"name\":\"serde_yaml\",\"req\":\"^0.8.21\"},{\"features\":[\"parsing\",\"formatting\"],\"name\":\"time\",\"req\":\"^0.3.30\"}],\"features\":{\"default\":[\"serde\"],\"enable_unstable_features_that_may_break_with_minor_version_bumps\":[]}}",
"png_0.18.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"approx\",\"req\":\"^0.5.1\"},{\"name\":\"bitflags\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"byteorder\",\"req\":\"^1.5.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.0\"},{\"name\":\"crc32fast\",\"req\":\"^1.2.0\"},{\"default_features\":false,\"features\":[\"cargo_bench_support\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"fdeflate\",\"req\":\"^0.3.3\"},{\"name\":\"flate2\",\"req\":\"^1.0.35\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3\"},{\"features\":[\"simd\"],\"name\":\"miniz_oxide\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9.2\"}],\"features\":{\"benchmarks\":[],\"unstable\":[\"crc32fast/nightly\"],\"zlib-rs\":[\"flate2/zlib-rs\"]}}",
"polling_3.11.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"concurrent-queue\",\"req\":\"^2.2.0\",\"target\":\"cfg(windows)\"},{\"kind\":\"dev\",\"name\":\"easy-parallel\",\"req\":\"^3.1.0\"},{\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^2.0.0\"},{\"name\":\"hermit-abi\",\"req\":\"^0.5.0\",\"target\":\"cfg(target_os = \\\"hermit\\\")\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.9\",\"target\":\"cfg(windows)\"},{\"default_features\":false,\"features\":[\"event\",\"fs\",\"pipe\",\"process\",\"std\",\"time\"],\"name\":\"rustix\",\"req\":\"^1.0.5\",\"target\":\"cfg(any(unix, target_os = \\\"fuchsia\\\", target_os = \\\"vxworks\\\"))\"},{\"kind\":\"dev\",\"name\":\"signal-hook\",\"req\":\"^0.3.17\",\"target\":\"cfg(all(unix, not(target_os=\\\"vita\\\")))\"},{\"kind\":\"dev\",\"name\":\"socket2\",\"req\":\"^0.6.0\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.37\"},{\"features\":[\"Wdk_Foundation\",\"Wdk_Storage_FileSystem\",\"Win32_Foundation\",\"Win32_Networking_WinSock\",\"Win32_Security\",\"Win32_Storage_FileSystem\",\"Win32_System_IO\",\"Win32_System_LibraryLoader\",\"Win32_System_Threading\",\"Win32_System_WindowsProgramming\"],\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{}}",
"poly1305_0.8.0": "{\"dependencies\":[{\"name\":\"cpufeatures\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"},{\"name\":\"opaque-debug\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"universal-hash\",\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"std\":[\"universal-hash/std\"]}}",
@@ -1275,6 +1280,7 @@
"syn_2.0.114": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"automod\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1\",\"target\":\"cfg(not(miri))\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1.0.91\"},{\"default_features\":false,\"name\":\"quote\",\"optional\":true,\"req\":\"^1.0.35\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1\",\"target\":\"cfg(not(miri))\"},{\"kind\":\"dev\",\"name\":\"ref-cast\",\"req\":\"^1\"},{\"features\":[\"blocking\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.13\",\"target\":\"cfg(not(miri))\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"syn-test-suite\",\"req\":\"^0\"},{\"kind\":\"dev\",\"name\":\"tar\",\"req\":\"^0.4.16\",\"target\":\"cfg(not(miri))\"},{\"kind\":\"dev\",\"name\":\"termcolor\",\"req\":\"^1\"},{\"name\":\"unicode-ident\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.3.2\",\"target\":\"cfg(not(miri))\"}],\"features\":{\"clone-impls\":[],\"default\":[\"derive\",\"parsing\",\"printing\",\"clone-impls\",\"proc-macro\"],\"derive\":[],\"extra-traits\":[],\"fold\":[],\"full\":[],\"parsing\":[],\"printing\":[\"dep:quote\"],\"proc-macro\":[\"proc-macro2/proc-macro\",\"quote?/proc-macro\"],\"test\":[\"syn-test-suite/all-features\"],\"visit\":[],\"visit-mut\":[]}}",
"sync_wrapper_1.0.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-core\",\"optional\":true,\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"}],\"features\":{\"futures\":[\"futures-core\"]}}",
"synstructure_0.13.2": "{\"dependencies\":[{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"default_features\":false,\"name\":\"quote\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"derive\",\"parsing\",\"printing\",\"clone-impls\",\"visit\",\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"synstructure_test_traits\",\"req\":\"^0.1\"}],\"features\":{\"default\":[\"proc-macro\"],\"proc-macro\":[\"proc-macro2/proc-macro\",\"syn/proc-macro\",\"quote/proc-macro\"]}}",
"syntect_5.3.0": "{\"dependencies\":[{\"name\":\"bincode\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"name\":\"fancy-regex\",\"optional\":true,\"req\":\"^0.16.2\"},{\"name\":\"flate2\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"fnv\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"getopts\",\"req\":\"^0.2\"},{\"name\":\"once_cell\",\"req\":\"^1.8\"},{\"default_features\":false,\"name\":\"onig\",\"optional\":true,\"req\":\"^6.5.1\"},{\"name\":\"plist\",\"optional\":true,\"req\":\"^1.3\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"public-api\",\"req\":\"^0.50.1\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.0\"},{\"name\":\"regex-syntax\",\"optional\":true,\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rustdoc-json\",\"req\":\"^0.9.7\"},{\"kind\":\"dev\",\"name\":\"rustup-toolchain\",\"req\":\"^0.1.5\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_derive\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"thiserror\",\"req\":\"^2.0.12\"},{\"name\":\"walkdir\",\"req\":\"^2.0\"},{\"name\":\"yaml-rust\",\"optional\":true,\"req\":\"^0.4.5\"}],\"features\":{\"default\":[\"default-onig\"],\"default-fancy\":[\"parsing\",\"default-syntaxes\",\"default-themes\",\"html\",\"plist-load\",\"yaml-load\",\"dump-load\",\"dump-create\",\"regex-fancy\"],\"default-onig\":[\"parsing\",\"default-syntaxes\",\"default-themes\",\"html\",\"plist-load\",\"yaml-load\",\"dump-load\",\"dump-create\",\"regex-onig\"],\"default-syntaxes\":[\"parsing\",\"dump-load\"],\"default-themes\":[\"dump-load\"],\"dump-create\":[\"flate2\",\"bincode\"],\"dump-load\":[\"flate2\",\"bincode\"],\"html\":[\"parsing\"],\"metadata\":[\"parsing\",\"plist-load\",\"dep:serde_json\"],\"parsing\":[\"regex-syntax\",\"fnv\",\"dump-create\",\"dump-load\"],\"plist-load\":[\"plist\",\"dep:serde_json\"],\"regex-fancy\":[\"fancy-regex\"],\"regex-onig\":[\"onig\"],\"yaml-load\":[\"yaml-rust\",\"parsing\"]}}",
"sys-locale_0.3.2": "{\"dependencies\":[{\"name\":\"js-sys\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", not(unix)))\"},{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"wasm-bindgen\",\"optional\":true,\"req\":\"^0.2\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", not(unix)))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", not(unix)))\"},{\"features\":[\"Window\",\"WorkerGlobalScope\",\"Navigator\",\"WorkerNavigator\"],\"name\":\"web-sys\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", not(unix)))\"}],\"features\":{\"js\":[\"js-sys\",\"wasm-bindgen\",\"web-sys\"]}}",
"system-configuration-sys_0.6.0": "{\"dependencies\":[{\"name\":\"core-foundation-sys\",\"req\":\"^0.8\"},{\"name\":\"libc\",\"req\":\"^0.2.149\"}],\"features\":{}}",
"system-configuration_0.6.1": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2\"},{\"name\":\"core-foundation\",\"req\":\"^0.9\"},{\"name\":\"system-configuration-sys\",\"req\":\"^0.6\"}],\"features\":{}}",
@@ -1338,13 +1344,13 @@
"tracing-test_0.2.5": "{\"dependencies\":[{\"features\":[\"rt-multi-thread\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1\"},{\"name\":\"tracing-core\",\"req\":\"^0.1\"},{\"features\":[\"env-filter\"],\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"name\":\"tracing-test-macro\",\"req\":\"^0.2.5\"}],\"features\":{\"no-env-filter\":[\"tracing-test-macro/no-env-filter\"]}}",
"tracing_0.1.44": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.6\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.21\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.17\"},{\"kind\":\"dev\",\"name\":\"log\",\"req\":\"^0.4.17\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.9\"},{\"name\":\"tracing-attributes\",\"optional\":true,\"req\":\"^0.1.31\"},{\"default_features\":false,\"name\":\"tracing-core\",\"req\":\"^0.1.36\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.38\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"}],\"features\":{\"async-await\":[],\"attributes\":[\"tracing-attributes\"],\"default\":[\"std\",\"attributes\"],\"log-always\":[\"log\"],\"max_level_debug\":[],\"max_level_error\":[],\"max_level_info\":[],\"max_level_off\":[],\"max_level_trace\":[],\"max_level_warn\":[],\"release_max_level_debug\":[],\"release_max_level_error\":[],\"release_max_level_info\":[],\"release_max_level_off\":[],\"release_max_level_trace\":[],\"release_max_level_warn\":[],\"std\":[\"tracing-core/std\"],\"valuable\":[\"tracing-core/valuable\"]}}",
"tree-sitter-bash_0.25.1": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.1\"},{\"kind\":\"dev\",\"name\":\"tree-sitter\",\"req\":\"^0.25\"},{\"name\":\"tree-sitter-language\",\"req\":\"^0.1\"}],\"features\":{}}",
"tree-sitter-highlight_0.25.10": "{\"dependencies\":[{\"name\":\"regex\",\"req\":\"^1.11.1\"},{\"name\":\"streaming-iterator\",\"req\":\"^0.1.9\"},{\"name\":\"thiserror\",\"req\":\"^2.0.11\"},{\"name\":\"tree-sitter\",\"req\":\"^0.25.10\"}],\"features\":{}}",
"tree-sitter-language_0.1.7": "{\"dependencies\":[],\"features\":{}}",
"tree-sitter_0.25.10": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"bindgen\",\"optional\":true,\"req\":\"^0.71.1\"},{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.2.10\"},{\"default_features\":false,\"features\":[\"unicode\"],\"name\":\"regex\",\"req\":\"^1.11.1\"},{\"default_features\":false,\"name\":\"regex-syntax\",\"req\":\"^0.8.5\"},{\"features\":[\"preserve_order\"],\"kind\":\"build\",\"name\":\"serde_json\",\"req\":\"^1.0.137\"},{\"name\":\"streaming-iterator\",\"req\":\"^0.1.9\"},{\"name\":\"tree-sitter-language\",\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"cranelift\",\"gc-drc\"],\"name\":\"wasmtime-c-api\",\"optional\":true,\"package\":\"wasmtime-c-api-impl\",\"req\":\"^29.0.1\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"regex/std\",\"regex/perf\",\"regex-syntax/unicode\"],\"wasm\":[\"std\",\"wasmtime-c-api\"]}}",
"tree_magic_mini_3.2.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.0\"},{\"name\":\"memchr\",\"req\":\"^2.0\"},{\"name\":\"nom\",\"req\":\"^8.0\"},{\"default_features\":false,\"name\":\"petgraph\",\"req\":\"^0.8.0\"},{\"name\":\"tree_magic_db\",\"optional\":true,\"req\":\"^3.0\"}],\"features\":{\"with-gpl-data\":[\"dep:tree_magic_db\"]}}",
"try-lock_0.2.5": "{\"dependencies\":[],\"features\":{}}",
"ts-rs-macros_11.1.0": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"full\",\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2.0.28\"},{\"name\":\"termcolor\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"no-serde-warnings\":[],\"serde-compat\":[\"termcolor\"]}}",
"ts-rs_11.1.0": "{\"dependencies\":[{\"features\":[\"serde\"],\"name\":\"bigdecimal\",\"optional\":true,\"req\":\">=0.0.13, <0.5\"},{\"name\":\"bson\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"bytes\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4\"},{\"features\":[\"serde\"],\"kind\":\"dev\",\"name\":\"chrono\",\"req\":\"^0.4\"},{\"name\":\"dprint-plugin-typescript\",\"optional\":true,\"req\":\"=0.95\"},{\"name\":\"heapless\",\"optional\":true,\"req\":\">=0.7, <0.9\"},{\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"ordered-float\",\"optional\":true,\"req\":\">=3, <6\"},{\"name\":\"semver\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"name\":\"smol_str\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"sync\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.40\"},{\"name\":\"ts-rs-macros\",\"req\":\"=11.1.0\"},{\"name\":\"url\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"bigdecimal-impl\":[\"bigdecimal\"],\"bson-uuid-impl\":[\"bson\"],\"bytes-impl\":[\"bytes\"],\"chrono-impl\":[\"chrono\"],\"default\":[\"serde-compat\"],\"format\":[\"dprint-plugin-typescript\"],\"heapless-impl\":[\"heapless\"],\"import-esm\":[],\"indexmap-impl\":[\"indexmap\"],\"no-serde-warnings\":[\"ts-rs-macros/no-serde-warnings\"],\"ordered-float-impl\":[\"ordered-float\"],\"semver-impl\":[\"semver\"],\"serde-compat\":[\"ts-rs-macros/serde-compat\"],\"serde-json-impl\":[\"serde_json\"],\"smol_str-impl\":[\"smol_str\"],\"tokio-impl\":[\"tokio\"],\"url-impl\":[\"url\"],\"uuid-impl\":[\"uuid\"]}}",
"two-face_0.5.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"cargo-lock\",\"req\":\"^10.1.0\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.44.3\"},{\"default_features\":false,\"features\":[\"read\"],\"kind\":\"dev\",\"name\":\"object\",\"req\":\"^0.36.7\"},{\"name\":\"serde\",\"req\":\"^1.0.228\"},{\"name\":\"serde_derive\",\"req\":\"^1.0.228\"},{\"kind\":\"dev\",\"name\":\"similar\",\"req\":\"^2.7.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"strum\",\"req\":\"^0.26.3\"},{\"default_features\":false,\"features\":[\"dump-load\",\"parsing\"],\"name\":\"syntect\",\"req\":\"^5.3.0\"},{\"default_features\":false,\"features\":[\"html\"],\"kind\":\"dev\",\"name\":\"syntect\",\"req\":\"^5.3.0\"},{\"kind\":\"dev\",\"name\":\"toml\",\"req\":\"^0.8.23\"},{\"default_features\":false,\"features\":[\"std\",\"xxhash64\"],\"kind\":\"dev\",\"name\":\"twox-hash\",\"req\":\"^2.1.2\"}],\"features\":{\"default\":[\"syntect-onig\"],\"syntect-default-fancy\":[\"syntect-fancy\",\"syntect/default-fancy\"],\"syntect-default-onig\":[\"syntect-onig\",\"syntect/default-onig\"],\"syntect-fancy\":[\"syntect/regex-fancy\"],\"syntect-onig\":[\"syntect/regex-onig\"]}}",
"type-map_0.5.1": "{\"dependencies\":[{\"name\":\"rustc-hash\",\"req\":\"^2\"}],\"features\":{}}",
"typenum_1.19.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"scale-info\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"const-generics\":[],\"force_unix_path_separator\":[],\"i128\":[],\"no_std\":[],\"scale_info\":[\"scale-info/derive\"],\"strict\":[]}}",
"uds_windows_1.1.0": "{\"dependencies\":[{\"name\":\"memoffset\",\"req\":\"^0.9.0\"},{\"name\":\"tempfile\",\"req\":\"^3\",\"target\":\"cfg(windows)\"},{\"features\":[\"winsock2\",\"ws2def\",\"minwinbase\",\"ntdef\",\"processthreadsapi\",\"handleapi\",\"ws2tcpip\",\"winbase\"],\"name\":\"winapi\",\"req\":\"^0.3.9\",\"target\":\"cfg(windows)\"}],\"features\":{}}",
@@ -1488,6 +1494,7 @@
"x509-parser_0.18.1": "{\"dependencies\":[{\"features\":[\"datetime\"],\"name\":\"asn1-rs\",\"req\":\"^0.7.0\"},{\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"data-encoding\",\"req\":\"^2.2.1\"},{\"features\":[\"bigint\"],\"name\":\"der-parser\",\"req\":\"^10.0\"},{\"name\":\"lazy_static\",\"req\":\"^1.4\"},{\"name\":\"nom\",\"req\":\"^7.0\"},{\"features\":[\"crypto\",\"x509\",\"x962\"],\"name\":\"oid-registry\",\"req\":\"^0.8.1\"},{\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17.12\"},{\"name\":\"rusticata-macros\",\"req\":\"^4.0\"},{\"name\":\"thiserror\",\"req\":\"^2.0\"},{\"features\":[\"formatting\"],\"name\":\"time\",\"req\":\"^0.3.35\"}],\"features\":{\"default\":[],\"validate\":[],\"verify\":[\"ring\"],\"verify-aws\":[\"aws-lc-rs\"]}}",
"xdg-home_1.3.0": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"features\":[\"Win32_Foundation\",\"Win32_UI_Shell\",\"Win32_System_Com\"],\"name\":\"windows-sys\",\"req\":\"^0.59\",\"target\":\"cfg(windows)\"}],\"features\":{}}",
"xz2_0.1.7": "{\"dependencies\":[{\"name\":\"futures\",\"optional\":true,\"req\":\"^0.1.26\"},{\"name\":\"lzma-sys\",\"req\":\"^0.1.18\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.0\"},{\"kind\":\"dev\",\"name\":\"tokio-core\",\"req\":\"^0.1.17\"},{\"name\":\"tokio-io\",\"optional\":true,\"req\":\"^0.1.12\"}],\"features\":{\"static\":[\"lzma-sys/static\"],\"tokio\":[\"tokio-io\",\"futures\"]}}",
"yaml-rust_0.4.5": "{\"dependencies\":[{\"name\":\"linked-hash-map\",\"req\":\"^0.5.3\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9\"}],\"features\":{}}",
"yansi_1.0.1": "{\"dependencies\":[{\"name\":\"is-terminal\",\"optional\":true,\"req\":\"^0.4.11\"}],\"features\":{\"_nightly\":[],\"alloc\":[],\"default\":[\"std\"],\"detect-env\":[\"std\"],\"detect-tty\":[\"is-terminal\",\"std\"],\"hyperlink\":[\"std\"],\"std\":[\"alloc\"]}}",
"yasna_0.5.2": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"std\"],\"name\":\"bit-vec\",\"optional\":true,\"req\":\"^0.6.1\"},{\"name\":\"num-bigint\",\"optional\":true,\"req\":\"^0.4\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"num-traits\",\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.1\"}],\"features\":{\"default\":[],\"std\":[]}}",
"yoke-derive_0.8.1": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.61\"},{\"name\":\"quote\",\"req\":\"^1.0.28\"},{\"features\":[\"fold\"],\"name\":\"syn\",\"req\":\"^2.0.21\"},{\"name\":\"synstructure\",\"req\":\"^0.13.0\"}],\"features\":{}}",

150
codex-rs/Cargo.lock generated
View File

@@ -852,6 +852,15 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1"
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]]
name = "bit-set"
version = "0.5.3"
@@ -1309,6 +1318,7 @@ dependencies = [
"codex-login",
"codex-protocol",
"codex-rmcp-client",
"codex-shell-command",
"codex-utils-absolute-path",
"codex-utils-cargo-bin",
"codex-utils-cli",
@@ -1397,8 +1407,8 @@ version = "0.0.0"
dependencies = [
"anyhow",
"codex-apply-patch",
"codex-core",
"codex-linux-sandbox",
"codex-utils-home-dir",
"dotenvy",
"tempfile",
"tokio",
@@ -1470,6 +1480,7 @@ dependencies = [
"codex-arg0",
"codex-chatgpt",
"codex-cloud-tasks",
"codex-config",
"codex-core",
"codex-exec",
"codex-execpolicy",
@@ -1600,10 +1611,13 @@ dependencies = [
"pretty_assertions",
"serde",
"serde_json",
"serde_path_to_error",
"sha2",
"thiserror 2.0.18",
"tokio",
"toml 0.9.11+spec-1.1.0",
"toml_edit 0.24.0+spec-1.1.0",
"tracing",
]
[[package]]
@@ -1637,14 +1651,15 @@ dependencies = [
"codex-otel",
"codex-protocol",
"codex-rmcp-client",
"codex-secrets",
"codex-shell-command",
"codex-skills",
"codex-state",
"codex-utils-absolute-path",
"codex-utils-cargo-bin",
"codex-utils-home-dir",
"codex-utils-pty",
"codex-utils-readiness",
"codex-utils-sanitizer",
"codex-utils-string",
"codex-windows-sandbox",
"core-foundation 0.9.4",
@@ -1658,7 +1673,6 @@ dependencies = [
"futures",
"http 1.4.0",
"image",
"include_dir",
"indexmap 2.13.0",
"indoc",
"insta",
@@ -1681,7 +1695,6 @@ dependencies = [
"seccompiler",
"serde",
"serde_json",
"serde_path_to_error",
"serde_yaml",
"serial_test",
"sha1",
@@ -1731,6 +1744,7 @@ dependencies = [
"anyhow",
"assert_cmd",
"clap",
"codex-apply-patch",
"codex-arg0",
"codex-cloud-requirements",
"codex-core",
@@ -1770,7 +1784,10 @@ dependencies = [
"clap",
"codex-core",
"codex-execpolicy",
"codex-protocol",
"codex-shell-command",
"codex-utils-cargo-bin",
"core_test_support",
"exec_server_test_support",
"libc",
"maplit",
@@ -1907,15 +1924,18 @@ dependencies = [
"cc",
"clap",
"codex-core",
"codex-protocol",
"codex-utils-absolute-path",
"landlock",
"libc",
"pkg-config",
"pretty_assertions",
"seccompiler",
"serde",
"serde_json",
"tempfile",
"tokio",
"url",
]
[[package]]
@@ -1963,6 +1983,7 @@ dependencies = [
"codex-arg0",
"codex-core",
"codex-protocol",
"codex-shell-command",
"codex-utils-cli",
"codex-utils-json-to-toml",
"core_test_support",
@@ -2151,6 +2172,7 @@ dependencies = [
"keyring",
"pretty_assertions",
"rand 0.9.2",
"regex",
"schemars 0.8.22",
"serde",
"serde_json",
@@ -2179,6 +2201,15 @@ dependencies = [
"which",
]
[[package]]
name = "codex-skills"
version = "0.0.0"
dependencies = [
"codex-utils-absolute-path",
"include_dir",
"thiserror 2.0.18",
]
[[package]]
name = "codex-state"
version = "0.0.0"
@@ -2236,6 +2267,7 @@ dependencies = [
"codex-login",
"codex-otel",
"codex-protocol",
"codex-shell-command",
"codex-state",
"codex-utils-absolute-path",
"codex-utils-approval-presets",
@@ -2275,6 +2307,7 @@ dependencies = [
"strum 0.27.2",
"strum_macros 0.27.2",
"supports-color 3.0.2",
"syntect",
"tempfile",
"textwrap 0.16.2",
"thiserror 2.0.18",
@@ -2285,8 +2318,7 @@ dependencies = [
"tracing",
"tracing-appender",
"tracing-subscriber",
"tree-sitter-bash",
"tree-sitter-highlight",
"two-face",
"unicode-segmentation",
"unicode-width 0.2.1",
"url",
@@ -2316,7 +2348,7 @@ dependencies = [
name = "codex-utils-approval-presets"
version = "0.0.0"
dependencies = [
"codex-core",
"codex-protocol",
]
[[package]]
@@ -2342,7 +2374,6 @@ name = "codex-utils-cli"
version = "0.0.0"
dependencies = [
"clap",
"codex-core",
"codex-protocol",
"pretty_assertions",
"serde",
@@ -2435,17 +2466,11 @@ name = "codex-utils-sandbox-summary"
version = "0.0.0"
dependencies = [
"codex-core",
"codex-protocol",
"codex-utils-absolute-path",
"pretty_assertions",
]
[[package]]
name = "codex-utils-sanitizer"
version = "0.0.0"
dependencies = [
"regex",
]
[[package]]
name = "codex-utils-sleep-inhibitor"
version = "0.0.0"
@@ -3543,6 +3568,7 @@ version = "0.0.0"
dependencies = [
"anyhow",
"codex-core",
"codex-protocol",
"codex-utils-cargo-bin",
"rmcp",
"serde_json",
@@ -5141,6 +5167,12 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-keyutils"
version = "0.2.4"
@@ -5948,6 +5980,28 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "onig"
version = "6.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0"
dependencies = [
"bitflags 2.10.0",
"libc",
"once_cell",
"onig_sys",
]
[[package]]
name = "onig_sys"
version = "69.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc"
dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "opaque-debug"
version = "0.3.1"
@@ -6365,6 +6419,19 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "plist"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
dependencies = [
"base64 0.22.1",
"indexmap 2.13.0",
"quick-xml",
"serde",
"time",
]
[[package]]
name = "png"
version = "0.18.0"
@@ -8870,6 +8937,27 @@ dependencies = [
"syn 2.0.114",
]
[[package]]
name = "syntect"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925"
dependencies = [
"bincode",
"flate2",
"fnv",
"once_cell",
"onig",
"plist",
"regex-syntax 0.8.8",
"serde",
"serde_derive",
"serde_json",
"thiserror 2.0.18",
"walkdir",
"yaml-rust",
]
[[package]]
name = "sys-locale"
version = "0.3.2"
@@ -9606,18 +9694,6 @@ dependencies = [
"tree-sitter-language",
]
[[package]]
name = "tree-sitter-highlight"
version = "0.25.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adc5f880ad8d8f94e88cb81c3557024cf1a8b75e3b504c50481ed4f5a6006ff3"
dependencies = [
"regex",
"streaming-iterator",
"thiserror 2.0.18",
"tree-sitter",
]
[[package]]
name = "tree-sitter-language"
version = "0.1.7"
@@ -9685,6 +9761,17 @@ dependencies = [
"utf-8",
]
[[package]]
name = "two-face"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b285c51f8a6ade109ed4566d33ac4fb289fb5d6cf87ed70908a5eaf65e948e34"
dependencies = [
"serde",
"serde_derive",
"syntect",
]
[[package]]
name = "type-map"
version = "0.5.1"
@@ -10949,6 +11036,15 @@ dependencies = [
"lzma-sys",
]
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "yansi"
version = "1.0.1"

View File

@@ -17,6 +17,7 @@ members = [
"cli",
"config",
"shell-command",
"skills",
"core",
"hooks",
"secrets",
@@ -53,7 +54,6 @@ members = [
"utils/cli",
"utils/elapsed",
"utils/sandbox-summary",
"utils/sanitizer",
"utils/sleep-inhibitor",
"utils/approval-presets",
"utils/oss",
@@ -113,6 +113,7 @@ codex-responses-api-proxy = { path = "responses-api-proxy" }
codex-rmcp-client = { path = "rmcp-client" }
codex-secrets = { path = "secrets" }
codex-shell-command = { path = "shell-command" }
codex-skills = { path = "skills" }
codex-state = { path = "state" }
codex-stdio-to-uds = { path = "stdio-to-uds" }
codex-tui = { path = "tui" }
@@ -131,7 +132,6 @@ codex-utils-pty = { path = "utils/pty" }
codex-utils-readiness = { path = "utils/readiness" }
codex-utils-rustls-provider = { path = "utils/rustls-provider" }
codex-utils-sandbox-summary = { path = "utils/sandbox-summary" }
codex-utils-sanitizer = { path = "utils/sanitizer" }
codex-utils-sleep-inhibitor = { path = "utils/sleep-inhibitor" }
codex-utils-string = { path = "utils/string" }
codex-windows-sandbox = { path = "windows-sandbox-rs" }
@@ -276,7 +276,7 @@ tracing-subscriber = "0.3.22"
tracing-test = "0.2.5"
tree-sitter = "0.25.10"
tree-sitter-bash = "0.25"
tree-sitter-highlight = "0.25.10"
syntect = "5"
ts-rs = "11"
tungstenite = { version = "0.27.0", features = ["deflate", "proxy"] }
uds_windows = "1.1.0"

View File

@@ -0,0 +1,114 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"FileChange": {
"oneOf": [
{
"properties": {
"content": {
"type": "string"
},
"type": {
"enum": [
"add"
],
"title": "AddFileChangeType",
"type": "string"
}
},
"required": [
"content",
"type"
],
"title": "AddFileChange",
"type": "object"
},
{
"properties": {
"content": {
"type": "string"
},
"type": {
"enum": [
"delete"
],
"title": "DeleteFileChangeType",
"type": "string"
}
},
"required": [
"content",
"type"
],
"title": "DeleteFileChange",
"type": "object"
},
{
"properties": {
"move_path": {
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"update"
],
"title": "UpdateFileChangeType",
"type": "string"
},
"unified_diff": {
"type": "string"
}
},
"required": [
"type",
"unified_diff"
],
"title": "UpdateFileChange",
"type": "object"
}
]
},
"ThreadId": {
"type": "string"
}
},
"properties": {
"callId": {
"description": "Use to correlate this with [codex_protocol::protocol::PatchApplyBeginEvent] and [codex_protocol::protocol::PatchApplyEndEvent].",
"type": "string"
},
"conversationId": {
"$ref": "#/definitions/ThreadId"
},
"fileChanges": {
"additionalProperties": {
"$ref": "#/definitions/FileChange"
},
"type": "object"
},
"grantRoot": {
"description": "When set, the agent is asking the user to allow writes under this root for the remainder of the session (unclear if this is honored today).",
"type": [
"string",
"null"
]
},
"reason": {
"description": "Optional explanatory reason (e.g. request for extra write access).",
"type": [
"string",
"null"
]
}
},
"required": [
"callId",
"conversationId",
"fileChanges"
],
"title": "ApplyPatchApprovalParams",
"type": "object"
}

View File

@@ -0,0 +1,73 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ReviewDecision": {
"description": "User's decision in response to an ExecApprovalRequest.",
"oneOf": [
{
"description": "User has approved this command and the agent should execute it.",
"enum": [
"approved"
],
"type": "string"
},
{
"additionalProperties": false,
"description": "User has approved this command and wants to apply the proposed execpolicy amendment so future matching commands are permitted.",
"properties": {
"approved_execpolicy_amendment": {
"properties": {
"proposed_execpolicy_amendment": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"proposed_execpolicy_amendment"
],
"type": "object"
}
},
"required": [
"approved_execpolicy_amendment"
],
"title": "ApprovedExecpolicyAmendmentReviewDecision",
"type": "object"
},
{
"description": "User has approved this command and wants to automatically approve any future identical instances (`command` and `cwd` match exactly) for the remainder of the session.",
"enum": [
"approved_for_session"
],
"type": "string"
},
{
"description": "User has denied this command and the agent should not execute it, but it should continue the session and try something else.",
"enum": [
"denied"
],
"type": "string"
},
{
"description": "User has denied this command and the agent should not do anything until the user's next command.",
"enum": [
"abort"
],
"type": "string"
}
]
}
},
"properties": {
"decision": {
"$ref": "#/definitions/ReviewDecision"
}
},
"required": [
"decision"
],
"title": "ApplyPatchApprovalResponse",
"type": "object"
}

View File

@@ -0,0 +1,33 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ChatgptAuthTokensRefreshReason": {
"oneOf": [
{
"description": "Codex attempted a backend request and received `401 Unauthorized`.",
"enum": [
"unauthorized"
],
"type": "string"
}
]
}
},
"properties": {
"previousAccountId": {
"description": "Workspace/account identifier that Codex was previously using.\n\nClients that manage multiple accounts/workspaces can use this as a hint to refresh the token for the correct workspace.\n\nThis may be `null` when the prior auth state did not include a workspace identifier (`chatgpt_account_id`).",
"type": [
"string",
"null"
]
},
"reason": {
"$ref": "#/definitions/ChatgptAuthTokensRefreshReason"
}
},
"required": [
"reason"
],
"title": "ChatgptAuthTokensRefreshParams",
"type": "object"
}

View File

@@ -0,0 +1,23 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"accessToken": {
"type": "string"
},
"chatgptAccountId": {
"type": "string"
},
"chatgptPlanType": {
"type": [
"string",
"null"
]
}
},
"required": [
"accessToken",
"chatgptAccountId"
],
"title": "ChatgptAuthTokensRefreshResponse",
"type": "object"
}

View File

@@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"oneOf": [
{
"properties": {
"method": {
"enum": [
"initialized"
],
"title": "InitializedNotificationMethod",
"type": "string"
}
},
"required": [
"method"
],
"title": "InitializedNotification",
"type": "object"
}
],
"title": "ClientNotification"
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,216 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"CommandAction": {
"oneOf": [
{
"properties": {
"command": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"type": {
"enum": [
"read"
],
"title": "ReadCommandActionType",
"type": "string"
}
},
"required": [
"command",
"name",
"path",
"type"
],
"title": "ReadCommandAction",
"type": "object"
},
{
"properties": {
"command": {
"type": "string"
},
"path": {
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"listFiles"
],
"title": "ListFilesCommandActionType",
"type": "string"
}
},
"required": [
"command",
"type"
],
"title": "ListFilesCommandAction",
"type": "object"
},
{
"properties": {
"command": {
"type": "string"
},
"path": {
"type": [
"string",
"null"
]
},
"query": {
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"search"
],
"title": "SearchCommandActionType",
"type": "string"
}
},
"required": [
"command",
"type"
],
"title": "SearchCommandAction",
"type": "object"
},
{
"properties": {
"command": {
"type": "string"
},
"type": {
"enum": [
"unknown"
],
"title": "UnknownCommandActionType",
"type": "string"
}
},
"required": [
"command",
"type"
],
"title": "UnknownCommandAction",
"type": "object"
}
]
},
"NetworkApprovalContext": {
"properties": {
"host": {
"type": "string"
},
"protocol": {
"$ref": "#/definitions/NetworkApprovalProtocol"
}
},
"required": [
"host",
"protocol"
],
"type": "object"
},
"NetworkApprovalProtocol": {
"enum": [
"http",
"https",
"socks5Tcp",
"socks5Udp"
],
"type": "string"
}
},
"properties": {
"approvalId": {
"description": "Unique identifier for this specific approval callback.\n\nFor regular shell/unified_exec approvals, this is null.\n\nFor zsh-exec-bridge subcommand approvals, multiple callbacks can belong to one parent `itemId`, so `approvalId` is a distinct opaque callback id (a UUID) used to disambiguate routing.",
"type": [
"string",
"null"
]
},
"command": {
"description": "The command to be executed.",
"type": [
"string",
"null"
]
},
"commandActions": {
"description": "Best-effort parsed command actions for friendly display.",
"items": {
"$ref": "#/definitions/CommandAction"
},
"type": [
"array",
"null"
]
},
"cwd": {
"description": "The command's working directory.",
"type": [
"string",
"null"
]
},
"itemId": {
"type": "string"
},
"networkApprovalContext": {
"anyOf": [
{
"$ref": "#/definitions/NetworkApprovalContext"
},
{
"type": "null"
}
],
"description": "Optional context for managed-network approval prompts."
},
"proposedExecpolicyAmendment": {
"description": "Optional proposed execpolicy amendment to allow similar commands without prompting.",
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"reason": {
"description": "Optional explanatory reason (e.g. request for network access).",
"type": [
"string",
"null"
]
},
"threadId": {
"type": "string"
},
"turnId": {
"type": "string"
}
},
"required": [
"itemId",
"threadId",
"turnId"
],
"title": "CommandExecutionRequestApprovalParams",
"type": "object"
}

View File

@@ -0,0 +1,72 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"CommandExecutionApprovalDecision": {
"oneOf": [
{
"description": "User approved the command.",
"enum": [
"accept"
],
"type": "string"
},
{
"description": "User approved the command and future identical commands should run without prompting.",
"enum": [
"acceptForSession"
],
"type": "string"
},
{
"additionalProperties": false,
"description": "User approved the command, and wants to apply the proposed execpolicy amendment so future matching commands can run without prompting.",
"properties": {
"acceptWithExecpolicyAmendment": {
"properties": {
"execpolicy_amendment": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"execpolicy_amendment"
],
"type": "object"
}
},
"required": [
"acceptWithExecpolicyAmendment"
],
"title": "AcceptWithExecpolicyAmendmentCommandExecutionApprovalDecision",
"type": "object"
},
{
"description": "User denied the command. The agent will continue the turn.",
"enum": [
"decline"
],
"type": "string"
},
{
"description": "User denied the command. The turn will also be immediately interrupted.",
"enum": [
"cancel"
],
"type": "string"
}
]
}
},
"properties": {
"decision": {
"$ref": "#/definitions/CommandExecutionApprovalDecision"
}
},
"required": [
"decision"
],
"title": "CommandExecutionRequestApprovalResponse",
"type": "object"
}

View File

@@ -0,0 +1,27 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"arguments": true,
"callId": {
"type": "string"
},
"threadId": {
"type": "string"
},
"tool": {
"type": "string"
},
"turnId": {
"type": "string"
}
},
"required": [
"arguments",
"callId",
"threadId",
"tool",
"turnId"
],
"title": "DynamicToolCallParams",
"type": "object"
}

View File

@@ -0,0 +1,66 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
"properties": {
"text": {
"type": "string"
},
"type": {
"enum": [
"inputText"
],
"title": "InputTextDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"text",
"type"
],
"title": "InputTextDynamicToolCallOutputContentItem",
"type": "object"
},
{
"properties": {
"imageUrl": {
"type": "string"
},
"type": {
"enum": [
"inputImage"
],
"title": "InputImageDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"imageUrl",
"type"
],
"title": "InputImageDynamicToolCallOutputContentItem",
"type": "object"
}
]
}
},
"properties": {
"contentItems": {
"items": {
"$ref": "#/definitions/DynamicToolCallOutputContentItem"
},
"type": "array"
},
"success": {
"type": "boolean"
}
},
"required": [
"contentItems",
"success"
],
"title": "DynamicToolCallResponse",
"type": "object"
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,165 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ParsedCommand": {
"oneOf": [
{
"properties": {
"cmd": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"description": "(Best effort) Path to the file being read by the command. When possible, this is an absolute path, though when relative, it should be resolved against the `cwd`` that will be used to run the command to derive the absolute path.",
"type": "string"
},
"type": {
"enum": [
"read"
],
"title": "ReadParsedCommandType",
"type": "string"
}
},
"required": [
"cmd",
"name",
"path",
"type"
],
"title": "ReadParsedCommand",
"type": "object"
},
{
"properties": {
"cmd": {
"type": "string"
},
"path": {
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"list_files"
],
"title": "ListFilesParsedCommandType",
"type": "string"
}
},
"required": [
"cmd",
"type"
],
"title": "ListFilesParsedCommand",
"type": "object"
},
{
"properties": {
"cmd": {
"type": "string"
},
"path": {
"type": [
"string",
"null"
]
},
"query": {
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"search"
],
"title": "SearchParsedCommandType",
"type": "string"
}
},
"required": [
"cmd",
"type"
],
"title": "SearchParsedCommand",
"type": "object"
},
{
"properties": {
"cmd": {
"type": "string"
},
"type": {
"enum": [
"unknown"
],
"title": "UnknownParsedCommandType",
"type": "string"
}
},
"required": [
"cmd",
"type"
],
"title": "UnknownParsedCommand",
"type": "object"
}
]
},
"ThreadId": {
"type": "string"
}
},
"properties": {
"approvalId": {
"description": "Identifier for this specific approval callback.",
"type": [
"string",
"null"
]
},
"callId": {
"description": "Use to correlate this with [codex_protocol::protocol::ExecCommandBeginEvent] and [codex_protocol::protocol::ExecCommandEndEvent].",
"type": "string"
},
"command": {
"items": {
"type": "string"
},
"type": "array"
},
"conversationId": {
"$ref": "#/definitions/ThreadId"
},
"cwd": {
"type": "string"
},
"parsedCmd": {
"items": {
"$ref": "#/definitions/ParsedCommand"
},
"type": "array"
},
"reason": {
"type": [
"string",
"null"
]
}
},
"required": [
"callId",
"command",
"conversationId",
"cwd",
"parsedCmd"
],
"title": "ExecCommandApprovalParams",
"type": "object"
}

View File

@@ -0,0 +1,73 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ReviewDecision": {
"description": "User's decision in response to an ExecApprovalRequest.",
"oneOf": [
{
"description": "User has approved this command and the agent should execute it.",
"enum": [
"approved"
],
"type": "string"
},
{
"additionalProperties": false,
"description": "User has approved this command and wants to apply the proposed execpolicy amendment so future matching commands are permitted.",
"properties": {
"approved_execpolicy_amendment": {
"properties": {
"proposed_execpolicy_amendment": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"proposed_execpolicy_amendment"
],
"type": "object"
}
},
"required": [
"approved_execpolicy_amendment"
],
"title": "ApprovedExecpolicyAmendmentReviewDecision",
"type": "object"
},
{
"description": "User has approved this command and wants to automatically approve any future identical instances (`command` and `cwd` match exactly) for the remainder of the session.",
"enum": [
"approved_for_session"
],
"type": "string"
},
{
"description": "User has denied this command and the agent should not execute it, but it should continue the session and try something else.",
"enum": [
"denied"
],
"type": "string"
},
{
"description": "User has denied this command and the agent should not do anything until the user's next command.",
"enum": [
"abort"
],
"type": "string"
}
]
}
},
"properties": {
"decision": {
"$ref": "#/definitions/ReviewDecision"
}
},
"required": [
"decision"
],
"title": "ExecCommandApprovalResponse",
"type": "object"
}

View File

@@ -0,0 +1,35 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"grantRoot": {
"description": "[UNSTABLE] When set, the agent is asking the user to allow writes under this root for the remainder of the session (unclear if this is honored today).",
"type": [
"string",
"null"
]
},
"itemId": {
"type": "string"
},
"reason": {
"description": "Optional explanatory reason (e.g. request for extra write access).",
"type": [
"string",
"null"
]
},
"threadId": {
"type": "string"
},
"turnId": {
"type": "string"
}
},
"required": [
"itemId",
"threadId",
"turnId"
],
"title": "FileChangeRequestApprovalParams",
"type": "object"
}

View File

@@ -0,0 +1,47 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"FileChangeApprovalDecision": {
"oneOf": [
{
"description": "User approved the file changes.",
"enum": [
"accept"
],
"type": "string"
},
{
"description": "User approved the file changes and future changes to the same files should run without prompting.",
"enum": [
"acceptForSession"
],
"type": "string"
},
{
"description": "User denied the file changes. The agent will continue the turn.",
"enum": [
"decline"
],
"type": "string"
},
{
"description": "User denied the file changes. The turn will also be immediately interrupted.",
"enum": [
"cancel"
],
"type": "string"
}
]
}
},
"properties": {
"decision": {
"$ref": "#/definitions/FileChangeApprovalDecision"
}
},
"required": [
"decision"
],
"title": "FileChangeRequestApprovalResponse",
"type": "object"
}

View File

@@ -0,0 +1,26 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"cancellationToken": {
"type": [
"string",
"null"
]
},
"query": {
"type": "string"
},
"roots": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"query",
"roots"
],
"title": "FuzzyFileSearchParams",
"type": "object"
}

View File

@@ -0,0 +1,55 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"FuzzyFileSearchResult": {
"description": "Superset of [`codex_file_search::FileMatch`]",
"properties": {
"file_name": {
"type": "string"
},
"indices": {
"items": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"type": [
"array",
"null"
]
},
"path": {
"type": "string"
},
"root": {
"type": "string"
},
"score": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
}
},
"required": [
"file_name",
"path",
"root",
"score"
],
"type": "object"
}
},
"properties": {
"files": {
"items": {
"$ref": "#/definitions/FuzzyFileSearchResult"
},
"type": "array"
}
},
"required": [
"files"
],
"title": "FuzzyFileSearchResponse",
"type": "object"
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"sessionId": {
"type": "string"
}
},
"required": [
"sessionId"
],
"title": "FuzzyFileSearchSessionCompletedNotification",
"type": "object"
}

View File

@@ -0,0 +1,63 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"FuzzyFileSearchResult": {
"description": "Superset of [`codex_file_search::FileMatch`]",
"properties": {
"file_name": {
"type": "string"
},
"indices": {
"items": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"type": [
"array",
"null"
]
},
"path": {
"type": "string"
},
"root": {
"type": "string"
},
"score": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
}
},
"required": [
"file_name",
"path",
"root",
"score"
],
"type": "object"
}
},
"properties": {
"files": {
"items": {
"$ref": "#/definitions/FuzzyFileSearchResult"
},
"type": "array"
},
"query": {
"type": "string"
},
"sessionId": {
"type": "string"
}
},
"required": [
"files",
"query",
"sessionId"
],
"title": "FuzzyFileSearchSessionUpdatedNotification",
"type": "object"
}

View File

@@ -0,0 +1,48 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"JSONRPCErrorError": {
"properties": {
"code": {
"format": "int64",
"type": "integer"
},
"data": true,
"message": {
"type": "string"
}
},
"required": [
"code",
"message"
],
"type": "object"
},
"RequestId": {
"anyOf": [
{
"type": "string"
},
{
"format": "int64",
"type": "integer"
}
]
}
},
"description": "A response to a request that indicates an error occurred.",
"properties": {
"error": {
"$ref": "#/definitions/JSONRPCErrorError"
},
"id": {
"$ref": "#/definitions/RequestId"
}
},
"required": [
"error",
"id"
],
"title": "JSONRPCError",
"type": "object"
}

View File

@@ -0,0 +1,19 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"code": {
"format": "int64",
"type": "integer"
},
"data": true,
"message": {
"type": "string"
}
},
"required": [
"code",
"message"
],
"title": "JSONRPCErrorError",
"type": "object"
}

View File

@@ -0,0 +1,109 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"anyOf": [
{
"$ref": "#/definitions/JSONRPCRequest"
},
{
"$ref": "#/definitions/JSONRPCNotification"
},
{
"$ref": "#/definitions/JSONRPCResponse"
},
{
"$ref": "#/definitions/JSONRPCError"
}
],
"definitions": {
"JSONRPCError": {
"description": "A response to a request that indicates an error occurred.",
"properties": {
"error": {
"$ref": "#/definitions/JSONRPCErrorError"
},
"id": {
"$ref": "#/definitions/RequestId"
}
},
"required": [
"error",
"id"
],
"type": "object"
},
"JSONRPCErrorError": {
"properties": {
"code": {
"format": "int64",
"type": "integer"
},
"data": true,
"message": {
"type": "string"
}
},
"required": [
"code",
"message"
],
"type": "object"
},
"JSONRPCNotification": {
"description": "A notification which does not expect a response.",
"properties": {
"method": {
"type": "string"
},
"params": true
},
"required": [
"method"
],
"type": "object"
},
"JSONRPCRequest": {
"description": "A request that expects a response.",
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"type": "string"
},
"params": true
},
"required": [
"id",
"method"
],
"type": "object"
},
"JSONRPCResponse": {
"description": "A successful (non-error) response to a request.",
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"result": true
},
"required": [
"id",
"result"
],
"type": "object"
},
"RequestId": {
"anyOf": [
{
"type": "string"
},
{
"format": "int64",
"type": "integer"
}
]
}
},
"description": "Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent.",
"title": "JSONRPCMessage"
}

View File

@@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "A notification which does not expect a response.",
"properties": {
"method": {
"type": "string"
},
"params": true
},
"required": [
"method"
],
"title": "JSONRPCNotification",
"type": "object"
}

View File

@@ -0,0 +1,32 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"RequestId": {
"anyOf": [
{
"type": "string"
},
{
"format": "int64",
"type": "integer"
}
]
}
},
"description": "A request that expects a response.",
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"type": "string"
},
"params": true
},
"required": [
"id",
"method"
],
"title": "JSONRPCRequest",
"type": "object"
}

View File

@@ -0,0 +1,29 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"RequestId": {
"anyOf": [
{
"type": "string"
},
{
"format": "int64",
"type": "integer"
}
]
}
},
"description": "A successful (non-error) response to a request.",
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"result": true
},
"required": [
"id",
"result"
],
"title": "JSONRPCResponse",
"type": "object"
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"anyOf": [
{
"type": "string"
},
{
"format": "int64",
"type": "integer"
}
],
"title": "RequestId"
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,841 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ApplyPatchApprovalParams": {
"properties": {
"callId": {
"description": "Use to correlate this with [codex_protocol::protocol::PatchApplyBeginEvent] and [codex_protocol::protocol::PatchApplyEndEvent].",
"type": "string"
},
"conversationId": {
"$ref": "#/definitions/ThreadId"
},
"fileChanges": {
"additionalProperties": {
"$ref": "#/definitions/FileChange"
},
"type": "object"
},
"grantRoot": {
"description": "When set, the agent is asking the user to allow writes under this root for the remainder of the session (unclear if this is honored today).",
"type": [
"string",
"null"
]
},
"reason": {
"description": "Optional explanatory reason (e.g. request for extra write access).",
"type": [
"string",
"null"
]
}
},
"required": [
"callId",
"conversationId",
"fileChanges"
],
"type": "object"
},
"ChatgptAuthTokensRefreshParams": {
"properties": {
"previousAccountId": {
"description": "Workspace/account identifier that Codex was previously using.\n\nClients that manage multiple accounts/workspaces can use this as a hint to refresh the token for the correct workspace.\n\nThis may be `null` when the prior auth state did not include a workspace identifier (`chatgpt_account_id`).",
"type": [
"string",
"null"
]
},
"reason": {
"$ref": "#/definitions/ChatgptAuthTokensRefreshReason"
}
},
"required": [
"reason"
],
"type": "object"
},
"ChatgptAuthTokensRefreshReason": {
"oneOf": [
{
"description": "Codex attempted a backend request and received `401 Unauthorized`.",
"enum": [
"unauthorized"
],
"type": "string"
}
]
},
"CommandAction": {
"oneOf": [
{
"properties": {
"command": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"type": {
"enum": [
"read"
],
"title": "ReadCommandActionType",
"type": "string"
}
},
"required": [
"command",
"name",
"path",
"type"
],
"title": "ReadCommandAction",
"type": "object"
},
{
"properties": {
"command": {
"type": "string"
},
"path": {
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"listFiles"
],
"title": "ListFilesCommandActionType",
"type": "string"
}
},
"required": [
"command",
"type"
],
"title": "ListFilesCommandAction",
"type": "object"
},
{
"properties": {
"command": {
"type": "string"
},
"path": {
"type": [
"string",
"null"
]
},
"query": {
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"search"
],
"title": "SearchCommandActionType",
"type": "string"
}
},
"required": [
"command",
"type"
],
"title": "SearchCommandAction",
"type": "object"
},
{
"properties": {
"command": {
"type": "string"
},
"type": {
"enum": [
"unknown"
],
"title": "UnknownCommandActionType",
"type": "string"
}
},
"required": [
"command",
"type"
],
"title": "UnknownCommandAction",
"type": "object"
}
]
},
"CommandExecutionRequestApprovalParams": {
"properties": {
"approvalId": {
"description": "Unique identifier for this specific approval callback.\n\nFor regular shell/unified_exec approvals, this is null.\n\nFor zsh-exec-bridge subcommand approvals, multiple callbacks can belong to one parent `itemId`, so `approvalId` is a distinct opaque callback id (a UUID) used to disambiguate routing.",
"type": [
"string",
"null"
]
},
"command": {
"description": "The command to be executed.",
"type": [
"string",
"null"
]
},
"commandActions": {
"description": "Best-effort parsed command actions for friendly display.",
"items": {
"$ref": "#/definitions/CommandAction"
},
"type": [
"array",
"null"
]
},
"cwd": {
"description": "The command's working directory.",
"type": [
"string",
"null"
]
},
"itemId": {
"type": "string"
},
"networkApprovalContext": {
"anyOf": [
{
"$ref": "#/definitions/NetworkApprovalContext"
},
{
"type": "null"
}
],
"description": "Optional context for managed-network approval prompts."
},
"proposedExecpolicyAmendment": {
"description": "Optional proposed execpolicy amendment to allow similar commands without prompting.",
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"reason": {
"description": "Optional explanatory reason (e.g. request for network access).",
"type": [
"string",
"null"
]
},
"threadId": {
"type": "string"
},
"turnId": {
"type": "string"
}
},
"required": [
"itemId",
"threadId",
"turnId"
],
"type": "object"
},
"DynamicToolCallParams": {
"properties": {
"arguments": true,
"callId": {
"type": "string"
},
"threadId": {
"type": "string"
},
"tool": {
"type": "string"
},
"turnId": {
"type": "string"
}
},
"required": [
"arguments",
"callId",
"threadId",
"tool",
"turnId"
],
"type": "object"
},
"ExecCommandApprovalParams": {
"properties": {
"approvalId": {
"description": "Identifier for this specific approval callback.",
"type": [
"string",
"null"
]
},
"callId": {
"description": "Use to correlate this with [codex_protocol::protocol::ExecCommandBeginEvent] and [codex_protocol::protocol::ExecCommandEndEvent].",
"type": "string"
},
"command": {
"items": {
"type": "string"
},
"type": "array"
},
"conversationId": {
"$ref": "#/definitions/ThreadId"
},
"cwd": {
"type": "string"
},
"parsedCmd": {
"items": {
"$ref": "#/definitions/ParsedCommand"
},
"type": "array"
},
"reason": {
"type": [
"string",
"null"
]
}
},
"required": [
"callId",
"command",
"conversationId",
"cwd",
"parsedCmd"
],
"type": "object"
},
"FileChange": {
"oneOf": [
{
"properties": {
"content": {
"type": "string"
},
"type": {
"enum": [
"add"
],
"title": "AddFileChangeType",
"type": "string"
}
},
"required": [
"content",
"type"
],
"title": "AddFileChange",
"type": "object"
},
{
"properties": {
"content": {
"type": "string"
},
"type": {
"enum": [
"delete"
],
"title": "DeleteFileChangeType",
"type": "string"
}
},
"required": [
"content",
"type"
],
"title": "DeleteFileChange",
"type": "object"
},
{
"properties": {
"move_path": {
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"update"
],
"title": "UpdateFileChangeType",
"type": "string"
},
"unified_diff": {
"type": "string"
}
},
"required": [
"type",
"unified_diff"
],
"title": "UpdateFileChange",
"type": "object"
}
]
},
"FileChangeRequestApprovalParams": {
"properties": {
"grantRoot": {
"description": "[UNSTABLE] When set, the agent is asking the user to allow writes under this root for the remainder of the session (unclear if this is honored today).",
"type": [
"string",
"null"
]
},
"itemId": {
"type": "string"
},
"reason": {
"description": "Optional explanatory reason (e.g. request for extra write access).",
"type": [
"string",
"null"
]
},
"threadId": {
"type": "string"
},
"turnId": {
"type": "string"
}
},
"required": [
"itemId",
"threadId",
"turnId"
],
"type": "object"
},
"NetworkApprovalContext": {
"properties": {
"host": {
"type": "string"
},
"protocol": {
"$ref": "#/definitions/NetworkApprovalProtocol"
}
},
"required": [
"host",
"protocol"
],
"type": "object"
},
"NetworkApprovalProtocol": {
"enum": [
"http",
"https",
"socks5Tcp",
"socks5Udp"
],
"type": "string"
},
"ParsedCommand": {
"oneOf": [
{
"properties": {
"cmd": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"description": "(Best effort) Path to the file being read by the command. When possible, this is an absolute path, though when relative, it should be resolved against the `cwd`` that will be used to run the command to derive the absolute path.",
"type": "string"
},
"type": {
"enum": [
"read"
],
"title": "ReadParsedCommandType",
"type": "string"
}
},
"required": [
"cmd",
"name",
"path",
"type"
],
"title": "ReadParsedCommand",
"type": "object"
},
{
"properties": {
"cmd": {
"type": "string"
},
"path": {
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"list_files"
],
"title": "ListFilesParsedCommandType",
"type": "string"
}
},
"required": [
"cmd",
"type"
],
"title": "ListFilesParsedCommand",
"type": "object"
},
{
"properties": {
"cmd": {
"type": "string"
},
"path": {
"type": [
"string",
"null"
]
},
"query": {
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"search"
],
"title": "SearchParsedCommandType",
"type": "string"
}
},
"required": [
"cmd",
"type"
],
"title": "SearchParsedCommand",
"type": "object"
},
{
"properties": {
"cmd": {
"type": "string"
},
"type": {
"enum": [
"unknown"
],
"title": "UnknownParsedCommandType",
"type": "string"
}
},
"required": [
"cmd",
"type"
],
"title": "UnknownParsedCommand",
"type": "object"
}
]
},
"RequestId": {
"anyOf": [
{
"type": "string"
},
{
"format": "int64",
"type": "integer"
}
]
},
"ThreadId": {
"type": "string"
},
"ToolRequestUserInputOption": {
"description": "EXPERIMENTAL. Defines a single selectable option for request_user_input.",
"properties": {
"description": {
"type": "string"
},
"label": {
"type": "string"
}
},
"required": [
"description",
"label"
],
"type": "object"
},
"ToolRequestUserInputParams": {
"description": "EXPERIMENTAL. Params sent with a request_user_input event.",
"properties": {
"itemId": {
"type": "string"
},
"questions": {
"items": {
"$ref": "#/definitions/ToolRequestUserInputQuestion"
},
"type": "array"
},
"threadId": {
"type": "string"
},
"turnId": {
"type": "string"
}
},
"required": [
"itemId",
"questions",
"threadId",
"turnId"
],
"type": "object"
},
"ToolRequestUserInputQuestion": {
"description": "EXPERIMENTAL. Represents one request_user_input question and its required options.",
"properties": {
"header": {
"type": "string"
},
"id": {
"type": "string"
},
"isOther": {
"default": false,
"type": "boolean"
},
"isSecret": {
"default": false,
"type": "boolean"
},
"options": {
"items": {
"$ref": "#/definitions/ToolRequestUserInputOption"
},
"type": [
"array",
"null"
]
},
"question": {
"type": "string"
}
},
"required": [
"header",
"id",
"question"
],
"type": "object"
}
},
"description": "Request initiated from the server and sent to the client.",
"oneOf": [
{
"description": "NEW APIs Sent when approval is requested for a specific command execution. This request is used for Turns started via turn/start.",
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"item/commandExecution/requestApproval"
],
"title": "Item/commandExecution/requestApprovalRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/CommandExecutionRequestApprovalParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Item/commandExecution/requestApprovalRequest",
"type": "object"
},
{
"description": "Sent when approval is requested for a specific file change. This request is used for Turns started via turn/start.",
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"item/fileChange/requestApproval"
],
"title": "Item/fileChange/requestApprovalRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/FileChangeRequestApprovalParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Item/fileChange/requestApprovalRequest",
"type": "object"
},
{
"description": "EXPERIMENTAL - Request input from the user for a tool call.",
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"item/tool/requestUserInput"
],
"title": "Item/tool/requestUserInputRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ToolRequestUserInputParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Item/tool/requestUserInputRequest",
"type": "object"
},
{
"description": "Execute a dynamic tool call on the client.",
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"item/tool/call"
],
"title": "Item/tool/callRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/DynamicToolCallParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Item/tool/callRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"account/chatgptAuthTokens/refresh"
],
"title": "Account/chatgptAuthTokens/refreshRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ChatgptAuthTokensRefreshParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Account/chatgptAuthTokens/refreshRequest",
"type": "object"
},
{
"description": "DEPRECATED APIs below Request to approve a patch. This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage).",
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"applyPatchApproval"
],
"title": "ApplyPatchApprovalRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ApplyPatchApprovalParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "ApplyPatchApprovalRequest",
"type": "object"
},
{
"description": "Request to exec a command. This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage).",
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"execCommandApproval"
],
"title": "ExecCommandApprovalRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ExecCommandApprovalParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "ExecCommandApprovalRequest",
"type": "object"
}
],
"title": "ServerRequest"
}

View File

@@ -0,0 +1,84 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ToolRequestUserInputOption": {
"description": "EXPERIMENTAL. Defines a single selectable option for request_user_input.",
"properties": {
"description": {
"type": "string"
},
"label": {
"type": "string"
}
},
"required": [
"description",
"label"
],
"type": "object"
},
"ToolRequestUserInputQuestion": {
"description": "EXPERIMENTAL. Represents one request_user_input question and its required options.",
"properties": {
"header": {
"type": "string"
},
"id": {
"type": "string"
},
"isOther": {
"default": false,
"type": "boolean"
},
"isSecret": {
"default": false,
"type": "boolean"
},
"options": {
"items": {
"$ref": "#/definitions/ToolRequestUserInputOption"
},
"type": [
"array",
"null"
]
},
"question": {
"type": "string"
}
},
"required": [
"header",
"id",
"question"
],
"type": "object"
}
},
"description": "EXPERIMENTAL. Params sent with a request_user_input event.",
"properties": {
"itemId": {
"type": "string"
},
"questions": {
"items": {
"$ref": "#/definitions/ToolRequestUserInputQuestion"
},
"type": "array"
},
"threadId": {
"type": "string"
},
"turnId": {
"type": "string"
}
},
"required": [
"itemId",
"questions",
"threadId",
"turnId"
],
"title": "ToolRequestUserInputParams",
"type": "object"
}

View File

@@ -0,0 +1,34 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ToolRequestUserInputAnswer": {
"description": "EXPERIMENTAL. Captures a user's answer to a request_user_input question.",
"properties": {
"answers": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"answers"
],
"type": "object"
}
},
"description": "EXPERIMENTAL. Response payload mapping question ids to answers.",
"properties": {
"answers": {
"additionalProperties": {
"$ref": "#/definitions/ToolRequestUserInputAnswer"
},
"type": "object"
}
},
"required": [
"answers"
],
"title": "ToolRequestUserInputResponse",
"type": "object"
}

View File

@@ -0,0 +1,67 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ClientInfo": {
"properties": {
"name": {
"type": "string"
},
"title": {
"type": [
"string",
"null"
]
},
"version": {
"type": "string"
}
},
"required": [
"name",
"version"
],
"type": "object"
},
"InitializeCapabilities": {
"description": "Client-declared capabilities negotiated during initialize.",
"properties": {
"experimentalApi": {
"default": false,
"description": "Opt into receiving experimental API methods and fields.",
"type": "boolean"
},
"optOutNotificationMethods": {
"description": "Exact notification method names that should be suppressed for this connection (for example `codex/event/session_configured`).",
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
}
},
"type": "object"
}
},
"properties": {
"capabilities": {
"anyOf": [
{
"$ref": "#/definitions/InitializeCapabilities"
},
{
"type": "null"
}
]
},
"clientInfo": {
"$ref": "#/definitions/ClientInfo"
}
},
"required": [
"clientInfo"
],
"title": "InitializeParams",
"type": "object"
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"userAgent": {
"type": "string"
}
},
"required": [
"userAgent"
],
"title": "InitializeResponse",
"type": "object"
}

View File

@@ -237,11 +237,23 @@
"type": "string"
},
"MessagePhase": {
"enum": [
"commentary",
"finalAnswer"
],
"type": "string"
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
{
"description": "Mid-turn assistant text (for example preamble/progress narration).\n\nAdditional tool calls or assistant output may follow before turn completion.",
"enum": [
"commentary"
],
"type": "string"
},
{
"description": "The assistant's terminal answer text for the current turn.",
"enum": [
"final_answer"
],
"type": "string"
}
]
},
"PatchApplyStatus": {
"enum": [

View File

@@ -237,11 +237,23 @@
"type": "string"
},
"MessagePhase": {
"enum": [
"commentary",
"finalAnswer"
],
"type": "string"
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
{
"description": "Mid-turn assistant text (for example preamble/progress narration).\n\nAdditional tool calls or assistant output may follow before turn completion.",
"enum": [
"commentary"
],
"type": "string"
},
{
"description": "The assistant's terminal answer text for the current turn.",
"enum": [
"final_answer"
],
"type": "string"
}
]
},
"PatchApplyStatus": {
"enum": [

View File

@@ -351,11 +351,23 @@
"type": "string"
},
"MessagePhase": {
"enum": [
"commentary",
"finalAnswer"
],
"type": "string"
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
{
"description": "Mid-turn assistant text (for example preamble/progress narration).\n\nAdditional tool calls or assistant output may follow before turn completion.",
"enum": [
"commentary"
],
"type": "string"
},
{
"description": "The assistant's terminal answer text for the current turn.",
"enum": [
"final_answer"
],
"type": "string"
}
]
},
"PatchApplyStatus": {
"enum": [

View File

@@ -420,11 +420,23 @@
"type": "string"
},
"MessagePhase": {
"enum": [
"commentary",
"finalAnswer"
],
"type": "string"
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
{
"description": "Mid-turn assistant text (for example preamble/progress narration).\n\nAdditional tool calls or assistant output may follow before turn completion.",
"enum": [
"commentary"
],
"type": "string"
},
{
"description": "The assistant's terminal answer text for the current turn.",
"enum": [
"final_answer"
],
"type": "string"
}
]
},
"NetworkAccess": {
"enum": [
@@ -836,6 +848,13 @@
"description": "Model provider used for this thread (for example, 'openai').",
"type": "string"
},
"name": {
"description": "Optional user-facing thread title.",
"type": [
"string",
"null"
]
},
"path": {
"description": "[UNSTABLE] Path to the thread on disk.",
"type": [

View File

@@ -374,11 +374,23 @@
"type": "string"
},
"MessagePhase": {
"enum": [
"commentary",
"finalAnswer"
],
"type": "string"
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
{
"description": "Mid-turn assistant text (for example preamble/progress narration).\n\nAdditional tool calls or assistant output may follow before turn completion.",
"enum": [
"commentary"
],
"type": "string"
},
{
"description": "The assistant's terminal answer text for the current turn.",
"enum": [
"final_answer"
],
"type": "string"
}
]
},
"PatchApplyStatus": {
"enum": [
@@ -609,6 +621,13 @@
"description": "Model provider used for this thread (for example, 'openai').",
"type": "string"
},
"name": {
"description": "Optional user-facing thread title.",
"type": [
"string",
"null"
]
},
"path": {
"description": "[UNSTABLE] Path to the thread on disk.",
"type": [

View File

@@ -374,11 +374,23 @@
"type": "string"
},
"MessagePhase": {
"enum": [
"commentary",
"finalAnswer"
],
"type": "string"
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
{
"description": "Mid-turn assistant text (for example preamble/progress narration).\n\nAdditional tool calls or assistant output may follow before turn completion.",
"enum": [
"commentary"
],
"type": "string"
},
{
"description": "The assistant's terminal answer text for the current turn.",
"enum": [
"final_answer"
],
"type": "string"
}
]
},
"PatchApplyStatus": {
"enum": [
@@ -609,6 +621,13 @@
"description": "Model provider used for this thread (for example, 'openai').",
"type": "string"
},
"name": {
"description": "Optional user-facing thread title.",
"type": [
"string",
"null"
]
},
"path": {
"description": "[UNSTABLE] Path to the thread on disk.",
"type": [

View File

@@ -420,11 +420,23 @@
"type": "string"
},
"MessagePhase": {
"enum": [
"commentary",
"finalAnswer"
],
"type": "string"
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
{
"description": "Mid-turn assistant text (for example preamble/progress narration).\n\nAdditional tool calls or assistant output may follow before turn completion.",
"enum": [
"commentary"
],
"type": "string"
},
{
"description": "The assistant's terminal answer text for the current turn.",
"enum": [
"final_answer"
],
"type": "string"
}
]
},
"NetworkAccess": {
"enum": [
@@ -836,6 +848,13 @@
"description": "Model provider used for this thread (for example, 'openai').",
"type": "string"
},
"name": {
"description": "Optional user-facing thread title.",
"type": [
"string",
"null"
]
},
"path": {
"description": "[UNSTABLE] Path to the thread on disk.",
"type": [

View File

@@ -374,11 +374,23 @@
"type": "string"
},
"MessagePhase": {
"enum": [
"commentary",
"finalAnswer"
],
"type": "string"
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
{
"description": "Mid-turn assistant text (for example preamble/progress narration).\n\nAdditional tool calls or assistant output may follow before turn completion.",
"enum": [
"commentary"
],
"type": "string"
},
{
"description": "The assistant's terminal answer text for the current turn.",
"enum": [
"final_answer"
],
"type": "string"
}
]
},
"PatchApplyStatus": {
"enum": [
@@ -609,6 +621,13 @@
"description": "Model provider used for this thread (for example, 'openai').",
"type": "string"
},
"name": {
"description": "Optional user-facing thread title.",
"type": [
"string",
"null"
]
},
"path": {
"description": "[UNSTABLE] Path to the thread on disk.",
"type": [

View File

@@ -420,11 +420,23 @@
"type": "string"
},
"MessagePhase": {
"enum": [
"commentary",
"finalAnswer"
],
"type": "string"
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
{
"description": "Mid-turn assistant text (for example preamble/progress narration).\n\nAdditional tool calls or assistant output may follow before turn completion.",
"enum": [
"commentary"
],
"type": "string"
},
{
"description": "The assistant's terminal answer text for the current turn.",
"enum": [
"final_answer"
],
"type": "string"
}
]
},
"NetworkAccess": {
"enum": [
@@ -836,6 +848,13 @@
"description": "Model provider used for this thread (for example, 'openai').",
"type": "string"
},
"name": {
"description": "Optional user-facing thread title.",
"type": [
"string",
"null"
]
},
"path": {
"description": "[UNSTABLE] Path to the thread on disk.",
"type": [

View File

@@ -374,11 +374,23 @@
"type": "string"
},
"MessagePhase": {
"enum": [
"commentary",
"finalAnswer"
],
"type": "string"
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
{
"description": "Mid-turn assistant text (for example preamble/progress narration).\n\nAdditional tool calls or assistant output may follow before turn completion.",
"enum": [
"commentary"
],
"type": "string"
},
{
"description": "The assistant's terminal answer text for the current turn.",
"enum": [
"final_answer"
],
"type": "string"
}
]
},
"PatchApplyStatus": {
"enum": [
@@ -609,6 +621,13 @@
"description": "Model provider used for this thread (for example, 'openai').",
"type": "string"
},
"name": {
"description": "Optional user-facing thread title.",
"type": [
"string",
"null"
]
},
"path": {
"description": "[UNSTABLE] Path to the thread on disk.",
"type": [

View File

@@ -374,11 +374,23 @@
"type": "string"
},
"MessagePhase": {
"enum": [
"commentary",
"finalAnswer"
],
"type": "string"
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
{
"description": "Mid-turn assistant text (for example preamble/progress narration).\n\nAdditional tool calls or assistant output may follow before turn completion.",
"enum": [
"commentary"
],
"type": "string"
},
{
"description": "The assistant's terminal answer text for the current turn.",
"enum": [
"final_answer"
],
"type": "string"
}
]
},
"PatchApplyStatus": {
"enum": [
@@ -609,6 +621,13 @@
"description": "Model provider used for this thread (for example, 'openai').",
"type": "string"
},
"name": {
"description": "Optional user-facing thread title.",
"type": [
"string",
"null"
]
},
"path": {
"description": "[UNSTABLE] Path to the thread on disk.",
"type": [

View File

@@ -351,11 +351,23 @@
"type": "string"
},
"MessagePhase": {
"enum": [
"commentary",
"finalAnswer"
],
"type": "string"
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
{
"description": "Mid-turn assistant text (for example preamble/progress narration).\n\nAdditional tool calls or assistant output may follow before turn completion.",
"enum": [
"commentary"
],
"type": "string"
},
{
"description": "The assistant's terminal answer text for the current turn.",
"enum": [
"final_answer"
],
"type": "string"
}
]
},
"PatchApplyStatus": {
"enum": [

View File

@@ -351,11 +351,23 @@
"type": "string"
},
"MessagePhase": {
"enum": [
"commentary",
"finalAnswer"
],
"type": "string"
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
{
"description": "Mid-turn assistant text (for example preamble/progress narration).\n\nAdditional tool calls or assistant output may follow before turn completion.",
"enum": [
"commentary"
],
"type": "string"
},
{
"description": "The assistant's terminal answer text for the current turn.",
"enum": [
"final_answer"
],
"type": "string"
}
]
},
"PatchApplyStatus": {
"enum": [

View File

@@ -351,11 +351,23 @@
"type": "string"
},
"MessagePhase": {
"enum": [
"commentary",
"finalAnswer"
],
"type": "string"
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
{
"description": "Mid-turn assistant text (for example preamble/progress narration).\n\nAdditional tool calls or assistant output may follow before turn completion.",
"enum": [
"commentary"
],
"type": "string"
},
{
"description": "The assistant's terminal answer text for the current turn.",
"enum": [
"final_answer"
],
"type": "string"
}
]
},
"PatchApplyStatus": {
"enum": [

View File

@@ -6,8 +6,8 @@ import type { ThreadId } from "./ThreadId";
export type ApplyPatchApprovalParams = { conversationId: ThreadId,
/**
* Use to correlate this with [codex_core::protocol::PatchApplyBeginEvent]
* and [codex_core::protocol::PatchApplyEndEvent].
* Use to correlate this with [codex_protocol::protocol::PatchApplyBeginEvent]
* and [codex_protocol::protocol::PatchApplyEndEvent].
*/
callId: string, fileChanges: { [key in string]?: FileChange },
/**

View File

@@ -47,6 +47,9 @@ import type { PatchApplyBeginEvent } from "./PatchApplyBeginEvent";
import type { PatchApplyEndEvent } from "./PatchApplyEndEvent";
import type { PlanDeltaEvent } from "./PlanDeltaEvent";
import type { RawResponseItemEvent } from "./RawResponseItemEvent";
import type { RealtimeConversationClosedEvent } from "./RealtimeConversationClosedEvent";
import type { RealtimeConversationRealtimeEvent } from "./RealtimeConversationRealtimeEvent";
import type { RealtimeConversationStartedEvent } from "./RealtimeConversationStartedEvent";
import type { ReasoningContentDeltaEvent } from "./ReasoningContentDeltaEvent";
import type { ReasoningRawContentDeltaEvent } from "./ReasoningRawContentDeltaEvent";
import type { RemoteSkillDownloadedEvent } from "./RemoteSkillDownloadedEvent";
@@ -75,4 +78,4 @@ import type { WebSearchEndEvent } from "./WebSearchEndEvent";
* Response event from the agent
* NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen.
*/
export type EventMsg = { "type": "error" } & ErrorEvent | { "type": "warning" } & WarningEvent | { "type": "model_reroute" } & ModelRerouteEvent | { "type": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "list_remote_skills_response" } & ListRemoteSkillsResponseEvent | { "type": "remote_skill_downloaded" } & RemoteSkillDownloadedEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent | { "type": "collab_resume_begin" } & CollabResumeBeginEvent | { "type": "collab_resume_end" } & CollabResumeEndEvent;
export type EventMsg = { "type": "error" } & ErrorEvent | { "type": "warning" } & WarningEvent | { "type": "realtime_conversation_started" } & RealtimeConversationStartedEvent | { "type": "realtime_conversation_realtime" } & RealtimeConversationRealtimeEvent | { "type": "realtime_conversation_closed" } & RealtimeConversationClosedEvent | { "type": "model_reroute" } & ModelRerouteEvent | { "type": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "list_remote_skills_response" } & ListRemoteSkillsResponseEvent | { "type": "remote_skill_downloaded" } & RemoteSkillDownloadedEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent | { "type": "collab_resume_begin" } & CollabResumeBeginEvent | { "type": "collab_resume_end" } & CollabResumeEndEvent;

View File

@@ -6,8 +6,8 @@ import type { ThreadId } from "./ThreadId";
export type ExecCommandApprovalParams = { conversationId: ThreadId,
/**
* Use to correlate this with [codex_core::protocol::ExecCommandBeginEvent]
* and [codex_core::protocol::ExecCommandEndEvent].
* Use to correlate this with [codex_protocol::protocol::ExecCommandBeginEvent]
* and [codex_protocol::protocol::ExecCommandEndEvent].
*/
callId: string,
/**

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type RealtimeAudioFrame = { data: string, sample_rate: number, num_channels: number, samples_per_channel: number | null, };

View File

@@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type MessagePhase = "commentary" | "finalAnswer";
export type RealtimeConversationClosedEvent = { reason: string | null, };

View File

@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { RealtimeEvent } from "./RealtimeEvent";
export type RealtimeConversationRealtimeEvent = { payload: RealtimeEvent, };

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type RealtimeConversationStartedEvent = { session_id: string | null, };

View File

@@ -0,0 +1,7 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { RealtimeAudioFrame } from "./RealtimeAudioFrame";
import type { JsonValue } from "./serde_json/JsonValue";
export type RealtimeEvent = { "SessionCreated": { session_id: string, } } | { "SessionUpdated": { backend_prompt: string | null, } } | { "AudioOut": RealtimeAudioFrame } | { "ConversationItemAdded": JsonValue } | { "Error": string };

View File

@@ -148,6 +148,11 @@ export type { RateLimitSnapshot } from "./RateLimitSnapshot";
export type { RateLimitWindow } from "./RateLimitWindow";
export type { RawResponseItemEvent } from "./RawResponseItemEvent";
export type { ReadOnlyAccess } from "./ReadOnlyAccess";
export type { RealtimeAudioFrame } from "./RealtimeAudioFrame";
export type { RealtimeConversationClosedEvent } from "./RealtimeConversationClosedEvent";
export type { RealtimeConversationRealtimeEvent } from "./RealtimeConversationRealtimeEvent";
export type { RealtimeConversationStartedEvent } from "./RealtimeConversationStartedEvent";
export type { RealtimeEvent } from "./RealtimeEvent";
export type { ReasoningContentDeltaEvent } from "./ReasoningContentDeltaEvent";
export type { ReasoningEffort } from "./ReasoningEffort";
export type { ReasoningItem } from "./ReasoningItem";

View File

@@ -55,6 +55,10 @@ agentRole: string | null,
* Optional Git metadata captured when the thread was created.
*/
gitInfo: GitInfo | null,
/**
* Optional user-facing thread title.
*/
name: string | null,
/**
* Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read`
* (when `includeTurns` is true) responses.

View File

@@ -1,6 +1,7 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { MessagePhase } from "../MessagePhase";
import type { JsonValue } from "../serde_json/JsonValue";
import type { CollabAgentState } from "./CollabAgentState";
import type { CollabAgentTool } from "./CollabAgentTool";
@@ -11,7 +12,6 @@ import type { FileUpdateChange } from "./FileUpdateChange";
import type { McpToolCallError } from "./McpToolCallError";
import type { McpToolCallResult } from "./McpToolCallResult";
import type { McpToolCallStatus } from "./McpToolCallStatus";
import type { MessagePhase } from "./MessagePhase";
import type { PatchApplyStatus } from "./PatchApplyStatus";
import type { UserInput } from "./UserInput";
import type { WebSearchAction } from "./WebSearchAction";

View File

@@ -95,7 +95,6 @@ export type { McpToolCallProgressNotification } from "./McpToolCallProgressNotif
export type { McpToolCallResult } from "./McpToolCallResult";
export type { McpToolCallStatus } from "./McpToolCallStatus";
export type { MergeStrategy } from "./MergeStrategy";
export type { MessagePhase } from "./MessagePhase";
export type { Model } from "./Model";
export type { ModelListParams } from "./ModelListParams";
export type { ModelListResponse } from "./ModelListResponse";

View File

@@ -32,15 +32,41 @@ use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::sync::LazyLock;
use ts_rs::TS;
const HEADER: &str = "// GENERATED CODE! DO NOT MODIFY BY HAND!\n\n";
const IGNORED_DEFINITIONS: &[&str] = &["Option<()>"];
static VARIANT_TITLE_COLLISION_OVERRIDES: LazyLock<HashMap<&'static str, &'static str>> =
LazyLock::new(HashMap::new);
static NUMBERED_DEFINITION_COLLISION_OVERRIDES: LazyLock<HashMap<&'static str, &'static str>> =
LazyLock::new(HashMap::new);
const JSON_V1_ALLOWLIST: &[&str] = &["InitializeParams", "InitializeResponse"];
const V1_CLIENT_REQUEST_METHODS: &[&str] = &[
"newConversation",
"getConversationSummary",
"listConversations",
"resumeConversation",
"forkConversation",
"archiveConversation",
"sendUserMessage",
"sendUserTurn",
"interruptConversation",
"addConversationListener",
"removeConversationListener",
"gitDiffToRemote",
"loginApiKey",
"loginChatGpt",
"cancelLoginChatGpt",
"logoutChatGpt",
"getAuthStatus",
"getUserSavedConfig",
"setDefaultModel",
"getUserAgent",
"userInfo",
"execOneOffCommand",
];
const EXCLUDED_SERVER_NOTIFICATION_METHODS_FOR_JSON: &[&str] = &[
"authStatusChange",
"loginChatGptComplete",
"sessionConfigured",
"rawResponseItem/completed",
];
#[derive(Clone)]
pub struct GeneratedSchema {
@@ -186,7 +212,8 @@ pub fn generate_json_with_experimental(out_dir: &Path, experimental_api: bool) -
schemas.extend(export_server_response_schemas(out_dir)?);
schemas.extend(export_client_notification_schemas(out_dir)?);
schemas.extend(export_server_notification_schemas(out_dir)?);
schemas.retain(|schema| schema.namespace() == Some("v2"));
schemas
.retain(|schema| !schema.in_v1_dir || JSON_V1_ALLOWLIST.contains(&schema.logical_name()));
let mut bundle = build_schema_bundle(schemas)?;
if !experimental_api {
@@ -955,10 +982,16 @@ where
{
let file_stem = name.trim();
let (raw_namespace, logical_name) = split_namespace(file_stem);
let include_in_json_codegen = raw_namespace == Some("v2");
let include_in_json_codegen =
raw_namespace != Some("v1") || JSON_V1_ALLOWLIST.contains(&logical_name);
let schema = schema_for!(T);
let mut schema_value = serde_json::to_value(schema)?;
if include_in_json_codegen {
if file_stem == "ClientRequest" {
strip_v1_client_request_variants_from_json_schema(&mut schema_value);
} else if file_stem == "ServerNotification" {
strip_v1_server_notification_variants_from_json_schema(&mut schema_value);
}
enforce_numbered_definition_collision_overrides(file_stem, &mut schema_value);
annotate_schema(&mut schema_value, Some(file_stem));
}
@@ -992,96 +1025,162 @@ where
fn enforce_numbered_definition_collision_overrides(schema_name: &str, schema: &mut Value) {
for defs_key in ["definitions", "$defs"] {
let renames = {
let Some(defs) = schema.get(defs_key).and_then(Value::as_object) else {
continue;
};
collect_numbered_definition_collision_renames(schema_name, defs_key, defs)
};
if renames.is_empty() {
let Some(defs) = schema.get(defs_key).and_then(Value::as_object) else {
continue;
}
#[expect(clippy::expect_used)]
let defs = schema
.get_mut(defs_key)
.and_then(Value::as_object_mut)
.expect("definition map should still be present");
for (old_name, new_name, collision_key) in &renames {
if old_name == new_name {
continue;
}
assert!(
!defs.contains_key(new_name),
"Numbered definition collision override produced duplicate definition {new_name} for {collision_key}",
);
let schema_value = defs.remove(old_name).unwrap_or_else(|| {
panic!("Missing generated numbered definition {old_name} for {collision_key}")
});
defs.insert(new_name.clone(), schema_value);
}
for (old_name, new_name, _) in renames {
if old_name != new_name {
rewrite_named_local_ref(schema, defs_key, &old_name, &new_name);
}
}
};
detect_numbered_definition_collisions(schema_name, defs_key, defs);
}
}
fn collect_numbered_definition_collision_renames(
fn strip_v1_client_request_variants_from_json_schema(schema: &mut Value) {
let v1_methods: HashSet<&str> = V1_CLIENT_REQUEST_METHODS.iter().copied().collect();
strip_method_variants_from_json_schema(schema, &v1_methods);
}
fn strip_v1_server_notification_variants_from_json_schema(schema: &mut Value) {
let methods: HashSet<&str> = EXCLUDED_SERVER_NOTIFICATION_METHODS_FOR_JSON
.iter()
.copied()
.collect();
strip_method_variants_from_json_schema(schema, &methods);
}
fn strip_method_variants_from_json_schema(schema: &mut Value, methods_to_remove: &HashSet<&str>) {
{
let Some(root) = schema.as_object_mut() else {
return;
};
let Some(Value::Array(variants)) = root.get_mut("oneOf") else {
return;
};
variants.retain(|variant| !is_method_variant_in_set(variant, methods_to_remove));
}
let reachable = reachable_local_definitions(schema, "definitions");
let Some(root) = schema.as_object_mut() else {
return;
};
if let Some(definitions) = root.get_mut("definitions").and_then(Value::as_object_mut) {
definitions.retain(|name, _| reachable.contains(name));
}
}
fn is_method_variant_in_set(value: &Value, methods: &HashSet<&str>) -> bool {
let Value::Object(map) = value else {
return false;
};
let Some(properties) = map.get("properties").and_then(Value::as_object) else {
return false;
};
let Some(method_schema) = properties.get("method") else {
return false;
};
let Some(method) = string_literal(method_schema) else {
return false;
};
methods.contains(method)
}
fn reachable_local_definitions(schema: &Value, defs_key: &str) -> HashSet<String> {
let Some(definitions) = schema.get(defs_key).and_then(Value::as_object) else {
return HashSet::new();
};
let mut queue: Vec<String> = Vec::new();
let mut reachable: HashSet<String> = HashSet::new();
collect_local_definition_refs_excluding_maps(schema, defs_key, &mut queue, &mut reachable);
while let Some(name) = queue.pop() {
if let Some(def_schema) = definitions.get(&name) {
collect_local_definition_refs(def_schema, defs_key, &mut queue, &mut reachable);
}
}
reachable
}
fn collect_local_definition_refs_excluding_maps(
value: &Value,
defs_key: &str,
queue: &mut Vec<String>,
reachable: &mut HashSet<String>,
) {
match value {
Value::Object(map) => {
for (key, child) in map {
if key == defs_key || key == "$defs" || key == "definitions" {
continue;
}
collect_local_definition_refs_excluding_maps(child, defs_key, queue, reachable);
}
}
Value::Array(items) => {
for child in items {
collect_local_definition_refs_excluding_maps(child, defs_key, queue, reachable);
}
}
_ => {}
}
collect_local_definition_ref_here(value, defs_key, queue, reachable);
}
fn collect_local_definition_refs(
value: &Value,
defs_key: &str,
queue: &mut Vec<String>,
reachable: &mut HashSet<String>,
) {
collect_local_definition_ref_here(value, defs_key, queue, reachable);
match value {
Value::Object(map) => {
for child in map.values() {
collect_local_definition_refs(child, defs_key, queue, reachable);
}
}
Value::Array(items) => {
for child in items {
collect_local_definition_refs(child, defs_key, queue, reachable);
}
}
_ => {}
}
}
fn collect_local_definition_ref_here(
value: &Value,
defs_key: &str,
queue: &mut Vec<String>,
reachable: &mut HashSet<String>,
) {
let Some(reference) = value
.as_object()
.and_then(|obj| obj.get("$ref"))
.and_then(Value::as_str)
else {
return;
};
let Some(name) = reference.strip_prefix(&format!("#/{defs_key}/")) else {
return;
};
let name = name.split('/').next().unwrap_or(name);
if reachable.insert(name.to_string()) {
queue.push(name.to_string());
}
}
fn detect_numbered_definition_collisions(
schema_name: &str,
defs_key: &str,
defs: &Map<String, Value>,
) -> Vec<(String, String, String)> {
let mut renames = Vec::new();
) {
for generated_name in defs.keys() {
let base_name = generated_name.trim_end_matches(|c: char| c.is_ascii_digit());
if base_name == generated_name || !defs.contains_key(base_name) {
continue;
}
let collision_key = format!(
"schema={schema_name}|container={defs_key}|generated={generated_name}|base={base_name}"
panic!(
"Numbered definition naming collision detected: schema={schema_name}|container={defs_key}|generated={generated_name}|base={base_name}"
);
let replacement = NUMBERED_DEFINITION_COLLISION_OVERRIDES
.get(collision_key.as_str())
.map(|value| (*value).to_string())
.unwrap_or_else(|| {
panic!("Missing numbered definition collision override for {collision_key}")
});
renames.push((generated_name.clone(), replacement, collision_key));
}
renames
}
fn rewrite_named_local_ref(value: &mut Value, defs_key: &str, old_name: &str, new_name: &str) {
let direct = format!("#/{defs_key}/{old_name}");
let prefixed = format!("{direct}/");
let replacement = format!("#/{defs_key}/{new_name}");
let replacement_prefixed = format!("{replacement}/");
match value {
Value::Object(obj) => {
if let Some(Value::String(reference)) = obj.get_mut("$ref") {
if reference == &direct {
*reference = replacement;
} else if let Some(rest) = reference.strip_prefix(&prefixed) {
*reference = format!("{replacement_prefixed}{rest}");
}
}
for child in obj.values_mut() {
rewrite_named_local_ref(child, defs_key, old_name, new_name);
}
}
Value::Array(items) => {
for child in items {
rewrite_named_local_ref(child, defs_key, old_name, new_name);
}
}
_ => {}
}
}
@@ -1189,7 +1288,11 @@ fn variant_definition_name(base: &str, variant: &Value) -> Option<String> {
if props.len() == 1
&& let Some(key) = props.keys().next()
{
let pascal = to_pascal_case(key);
let pascal = props
.get(key)
.and_then(string_literal)
.map(to_pascal_case)
.unwrap_or_else(|| to_pascal_case(key));
return Some(format!("{pascal}{base}"));
}
}
@@ -1302,20 +1405,11 @@ fn annotate_variant_list(variants: &mut [Value], base: Option<&str>) {
&& let Some(base_name) = base
&& let Some(name) = variant_definition_name(base_name, variant)
{
let mut candidate = name.clone();
let candidate = name.clone();
if seen.contains(&candidate) {
let collision_key = variant_title_collision_key(base_name, &name, variant);
candidate = VARIANT_TITLE_COLLISION_OVERRIDES
.get(collision_key.as_str())
.map(|value| (*value).to_string())
.unwrap_or_else(|| {
panic!(
"Missing variant title collision override for {collision_key} (generated name: {name})"
)
});
assert!(
!seen.contains(&candidate),
"Variant title collision override produced duplicate title {candidate} for {collision_key}",
panic!(
"Variant title naming collision detected: {collision_key} (generated name: {name})"
);
}
if let Some(obj) = variant.as_object_mut() {

View File

@@ -16,7 +16,7 @@ use crate::protocol::v2::TurnError;
use crate::protocol::v2::TurnStatus;
use crate::protocol::v2::UserInput;
use crate::protocol::v2::WebSearchAction;
use codex_protocol::models::MessagePhase as CoreMessagePhase;
use codex_protocol::models::MessagePhase;
use codex_protocol::protocol::AgentReasoningEvent;
use codex_protocol::protocol::AgentReasoningRawContentEvent;
use codex_protocol::protocol::AgentStatus;
@@ -190,17 +190,15 @@ impl ThreadHistoryBuilder {
self.current_turn = Some(turn);
}
fn handle_agent_message(&mut self, text: String, phase: Option<CoreMessagePhase>) {
fn handle_agent_message(&mut self, text: String, phase: Option<MessagePhase>) {
if text.is_empty() {
return;
}
let id = self.next_item_id();
self.ensure_turn().items.push(ThreadItem::AgentMessage {
id,
text,
phase: phase.map(Into::into),
});
self.ensure_turn()
.items
.push(ThreadItem::AgentMessage { id, text, phase });
}
fn handle_agent_reasoning(&mut self, payload: &AgentReasoningEvent) {
@@ -1196,7 +1194,7 @@ mod tests {
ThreadItem::AgentMessage {
id: "item-1".into(),
text: "Final reply".into(),
phase: Some(crate::protocol::v2::MessagePhase::FinalAnswer),
phase: Some(MessagePhase::FinalAnswer),
}
);
}

View File

@@ -234,8 +234,8 @@ pub struct GitDiffToRemoteResponse {
#[serde(rename_all = "camelCase")]
pub struct ApplyPatchApprovalParams {
pub conversation_id: ThreadId,
/// Use to correlate this with [codex_core::protocol::PatchApplyBeginEvent]
/// and [codex_core::protocol::PatchApplyEndEvent].
/// Use to correlate this with [codex_protocol::protocol::PatchApplyBeginEvent]
/// and [codex_protocol::protocol::PatchApplyEndEvent].
pub call_id: String,
pub file_changes: HashMap<PathBuf, FileChange>,
/// Optional explanatory reason (e.g. request for extra write access).
@@ -255,8 +255,8 @@ pub struct ApplyPatchApprovalResponse {
#[serde(rename_all = "camelCase")]
pub struct ExecCommandApprovalParams {
pub conversation_id: ThreadId,
/// Use to correlate this with [codex_core::protocol::ExecCommandBeginEvent]
/// and [codex_core::protocol::ExecCommandEndEvent].
/// Use to correlate this with [codex_protocol::protocol::ExecCommandBeginEvent]
/// and [codex_protocol::protocol::ExecCommandEndEvent].
pub call_id: String,
/// Identifier for this specific approval callback.
pub approval_id: Option<String>,

View File

@@ -20,7 +20,7 @@ use codex_protocol::items::TurnItem as CoreTurnItem;
use codex_protocol::mcp::Resource as McpResource;
use codex_protocol::mcp::ResourceTemplate as McpResourceTemplate;
use codex_protocol::mcp::Tool as McpTool;
use codex_protocol::models::MessagePhase as CoreMessagePhase;
use codex_protocol::models::MessagePhase;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::InputModality;
use codex_protocol::openai_models::ReasoningEffort;
@@ -2277,6 +2277,8 @@ pub struct Thread {
pub agent_role: Option<String>,
/// Optional Git metadata captured when the thread was created.
pub git_info: Option<GitInfo>,
/// Optional user-facing thread title.
pub name: Option<String>,
/// Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read`
/// (when `includeTurns` is true) responses.
/// For all other responses and notifications returning a Thread,
@@ -2664,24 +2666,6 @@ impl From<CoreUserInput> for UserInput {
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub enum MessagePhase {
Commentary,
FinalAnswer,
}
impl From<CoreMessagePhase> for MessagePhase {
fn from(value: CoreMessagePhase) -> Self {
match value {
CoreMessagePhase::Commentary => Self::Commentary,
CoreMessagePhase::FinalAnswer => Self::FinalAnswer,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "camelCase")]
#[ts(tag = "type")]
@@ -2871,7 +2855,7 @@ impl From<CoreTurnItem> for ThreadItem {
ThreadItem::AgentMessage {
id: agent.id,
text,
phase: agent.phase.map(Into::into),
phase: agent.phase,
}
}
CoreTurnItem::Plan(plan) => ThreadItem::Plan {
@@ -3690,7 +3674,6 @@ mod tests {
use codex_protocol::items::TurnItem;
use codex_protocol::items::UserMessageItem;
use codex_protocol::items::WebSearchItem;
use codex_protocol::models::MessagePhase as CoreMessagePhase;
use codex_protocol::models::WebSearchAction as CoreWebSearchAction;
use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess;
use codex_protocol::protocol::ReadOnlyAccess as CoreReadOnlyAccess;
@@ -3900,7 +3883,7 @@ mod tests {
content: vec![AgentMessageContent::Text {
text: "final".to_string(),
}],
phase: Some(CoreMessagePhase::FinalAnswer),
phase: Some(MessagePhase::FinalAnswer),
});
assert_eq!(

View File

@@ -21,6 +21,7 @@ async-trait = { workspace = true }
codex-arg0 = { workspace = true }
codex-cloud-requirements = { workspace = true }
codex-core = { workspace = true }
codex-shell-command = { workspace = true }
codex-utils-cli = { workspace = true }
codex-backend-client = { workspace = true }
codex-file-search = { workspace = true }

View File

@@ -75,35 +75,38 @@ use codex_app_server_protocol::build_turns_from_rollout_items;
use codex_app_server_protocol::convert_patch_changes;
use codex_core::CodexThread;
use codex_core::ThreadManager;
use codex_core::parse_command::shlex_join;
use codex_core::protocol::ApplyPatchApprovalRequestEvent;
use codex_core::protocol::CodexErrorInfo as CoreCodexErrorInfo;
use codex_core::protocol::Event;
use codex_core::protocol::EventMsg;
use codex_core::protocol::ExecApprovalRequestEvent;
use codex_core::protocol::ExecCommandEndEvent;
use codex_core::protocol::McpToolCallBeginEvent;
use codex_core::protocol::McpToolCallEndEvent;
use codex_core::protocol::Op;
use codex_core::protocol::ReviewDecision;
use codex_core::protocol::TokenCountEvent;
use codex_core::protocol::TurnDiffEvent;
use codex_core::find_thread_name_by_id;
use codex_core::review_format::format_review_findings_block;
use codex_core::review_prompts;
use codex_protocol::ThreadId;
use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem;
use codex_protocol::dynamic_tools::DynamicToolResponse as CoreDynamicToolResponse;
use codex_protocol::plan_tool::UpdatePlanArgs;
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ExecApprovalRequestEvent;
use codex_protocol::protocol::ExecCommandEndEvent;
use codex_protocol::protocol::McpToolCallBeginEvent;
use codex_protocol::protocol::McpToolCallEndEvent;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::ReviewOutputEvent;
use codex_protocol::protocol::TokenCountEvent;
use codex_protocol::protocol::TurnDiffEvent;
use codex_protocol::request_user_input::RequestUserInputAnswer as CoreRequestUserInputAnswer;
use codex_protocol::request_user_input::RequestUserInputResponse as CoreRequestUserInputResponse;
use codex_shell_command::parse_command::shlex_join;
use std::collections::HashMap;
use std::convert::TryFrom;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::sync::oneshot;
use tracing::error;
use tracing::warn;
type JsonValue = serde_json::Value;
@@ -129,6 +132,7 @@ pub(crate) async fn apply_bespoke_event_handling(
thread_watch_manager: ThreadWatchManager,
api_version: ApiVersion,
fallback_model_provider: String,
codex_home: &Path,
) {
let Event {
id: event_turn_id,
@@ -1224,6 +1228,16 @@ pub(crate) async fn apply_bespoke_event_handling(
thread.status = thread_watch_manager
.loaded_status_for_thread(&thread.id)
.await;
match find_thread_name_by_id(codex_home, &conversation_id).await {
Ok(name) => {
thread.name = name;
}
Err(err) => {
warn!(
"Failed to read thread name for {conversation_id}: {err}"
);
}
}
ThreadRollbackResponse { thread }
}
Err(err) => {
@@ -1928,7 +1942,7 @@ async fn on_command_execution_request_approval_response(
}
fn collab_resume_begin_item(
begin_event: codex_core::protocol::CollabResumeBeginEvent,
begin_event: codex_protocol::protocol::CollabResumeBeginEvent,
) -> ThreadItem {
ThreadItem::CollabAgentToolCall {
id: begin_event.call_id,
@@ -1941,7 +1955,7 @@ fn collab_resume_begin_item(
}
}
fn collab_resume_end_item(end_event: codex_core::protocol::CollabResumeEndEvent) -> ThreadItem {
fn collab_resume_end_item(end_event: codex_protocol::protocol::CollabResumeEndEvent) -> ThreadItem {
let status = match &end_event.status {
codex_protocol::protocol::AgentStatus::Errored(_)
| codex_protocol::protocol::AgentStatus::NotFound => V2CollabToolCallStatus::Failed,
@@ -2046,17 +2060,17 @@ mod tests {
use anyhow::anyhow;
use anyhow::bail;
use codex_app_server_protocol::TurnPlanStepStatus;
use codex_core::protocol::CollabResumeBeginEvent;
use codex_core::protocol::CollabResumeEndEvent;
use codex_core::protocol::CreditsSnapshot;
use codex_core::protocol::McpInvocation;
use codex_core::protocol::RateLimitSnapshot;
use codex_core::protocol::RateLimitWindow;
use codex_core::protocol::TokenUsage;
use codex_core::protocol::TokenUsageInfo;
use codex_protocol::mcp::CallToolResult;
use codex_protocol::plan_tool::PlanItemArg;
use codex_protocol::plan_tool::StepStatus;
use codex_protocol::protocol::CollabResumeBeginEvent;
use codex_protocol::protocol::CollabResumeEndEvent;
use codex_protocol::protocol::CreditsSnapshot;
use codex_protocol::protocol::McpInvocation;
use codex_protocol::protocol::RateLimitSnapshot;
use codex_protocol::protocol::RateLimitWindow;
use codex_protocol::protocol::TokenUsage;
use codex_protocol::protocol::TokenUsageInfo;
use pretty_assertions::assert_eq;
use rmcp::model::Content;
use serde_json::Value as JsonValue;

View File

@@ -11,6 +11,7 @@ use crate::outgoing_message::OutgoingMessageSender;
use crate::outgoing_message::OutgoingNotification;
use crate::outgoing_message::ThreadScopedOutgoingMessageSender;
use crate::thread_status::ThreadWatchManager;
use crate::thread_status::resolve_thread_status;
use chrono::DateTime;
use chrono::SecondsFormat;
use chrono::Utc;
@@ -174,7 +175,6 @@ use codex_core::AuthManager;
use codex_core::CodexAuth;
use codex_core::CodexThread;
use codex_core::Cursor as RolloutCursor;
use codex_core::InitialHistory;
use codex_core::NewThread;
use codex_core::RolloutRecorder;
use codex_core::SessionMeta;
@@ -202,17 +202,13 @@ use codex_core::features::FEATURES;
use codex_core::features::Feature;
use codex_core::features::Stage;
use codex_core::find_archived_thread_path_by_id_str;
use codex_core::find_thread_name_by_id;
use codex_core::find_thread_names_by_ids;
use codex_core::find_thread_path_by_id_str;
use codex_core::git_info::git_diff_to_remote;
use codex_core::mcp::collect_mcp_snapshot;
use codex_core::mcp::group_tools_by_server;
use codex_core::parse_cursor;
use codex_core::protocol::EventMsg;
use codex_core::protocol::Op;
use codex_core::protocol::ReviewDelivery as CoreReviewDelivery;
use codex_core::protocol::ReviewRequest;
use codex_core::protocol::ReviewTarget as CoreReviewTarget;
use codex_core::protocol::SessionConfiguredEvent;
use codex_core::read_head_for_summary;
use codex_core::read_session_meta_line;
use codex_core::rollout_date_parts;
@@ -237,13 +233,20 @@ use codex_protocol::dynamic_tools::DynamicToolSpec as CoreDynamicToolSpec;
use codex_protocol::items::TurnItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::AgentStatus;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::GitInfo as CoreGitInfo;
use codex_protocol::protocol::InitialHistory;
use codex_protocol::protocol::McpAuthStatus as CoreMcpAuthStatus;
use codex_protocol::protocol::McpServerRefreshConfig;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot;
use codex_protocol::protocol::RemoteSkillHazelnutScope;
use codex_protocol::protocol::RemoteSkillProductSurface;
use codex_protocol::protocol::ReviewDelivery as CoreReviewDelivery;
use codex_protocol::protocol::ReviewRequest;
use codex_protocol::protocol::ReviewTarget as CoreReviewTarget;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::SessionConfiguredEvent;
use codex_protocol::protocol::SessionMetaLine;
use codex_protocol::protocol::USER_MESSAGE_BEGIN;
use codex_protocol::user_input::UserInput as CoreInputItem;
@@ -2051,10 +2054,12 @@ impl CodexMessageProcessor {
.upsert_thread(thread.clone())
.await;
thread.status = self
.thread_watch_manager
.loaded_status_for_thread(&thread.id)
.await;
thread.status = resolve_thread_status(
self.thread_watch_manager
.loaded_status_for_thread(&thread.id)
.await,
false,
);
let response = ThreadStartResponse {
thread: thread.clone(),
@@ -2370,10 +2375,13 @@ impl CodexMessageProcessor {
match result {
Ok(mut thread) => {
thread.status = self
.thread_watch_manager
.loaded_status_for_thread(&thread.id)
.await;
thread.status = resolve_thread_status(
self.thread_watch_manager
.loaded_status_for_thread(&thread.id)
.await,
false,
);
self.attach_thread_name(thread_id, &mut thread).await;
let thread_id = thread.id.clone();
let response = ThreadUnarchiveResponse { thread };
self.outgoing.send_response(request_id, response).await;
@@ -2544,18 +2552,36 @@ impl CodexMessageProcessor {
return;
}
};
let mut threads = Vec::with_capacity(summaries.len());
let mut thread_ids = HashSet::with_capacity(summaries.len());
let mut status_ids = Vec::with_capacity(summaries.len());
for summary in summaries {
let conversation_id = summary.conversation_id;
thread_ids.insert(conversation_id);
let thread = summary_to_thread(summary);
status_ids.push(thread.id.clone());
threads.push((conversation_id, thread));
}
let names = match find_thread_names_by_ids(&self.config.codex_home, &thread_ids).await {
Ok(names) => names,
Err(err) => {
warn!("Failed to read thread names: {err}");
HashMap::new()
}
};
let data = summaries
.into_iter()
.map(summary_to_thread)
.collect::<Vec<_>>();
let statuses = self
.thread_watch_manager
.loaded_statuses_for_threads(data.iter().map(|thread| thread.id.clone()).collect())
.loaded_statuses_for_threads(status_ids)
.await;
let data = data
let data = threads
.into_iter()
.map(|mut thread| {
.map(|(conversation_id, mut thread)| {
thread.name = names.get(&conversation_id).cloned();
if let Some(status) = statuses.get(&thread.id) {
thread.status = status.clone();
}
@@ -2717,6 +2743,7 @@ impl CodexMessageProcessor {
}
build_thread_from_snapshot(thread_uuid, &config_snapshot, loaded_rollout_path)
};
self.attach_thread_name(thread_uuid, &mut thread).await;
if include_turns && let Some(rollout_path) = rollout_path.as_ref() {
match read_rollout_items_from_rollout(rollout_path).await {
@@ -2747,10 +2774,12 @@ impl CodexMessageProcessor {
}
}
thread.status = self
.thread_watch_manager
.loaded_status_for_thread(&thread.id)
.await;
thread.status = resolve_thread_status(
self.thread_watch_manager
.loaded_status_for_thread(&thread.id)
.await,
false,
);
let response = ThreadReadResponse { thread };
self.outgoing.send_response(request_id, response).await;
}
@@ -2927,10 +2956,12 @@ impl CodexMessageProcessor {
.upsert_thread(thread.clone())
.await;
thread.status = self
.thread_watch_manager
.loaded_status_for_thread(&thread.id)
.await;
thread.status = resolve_thread_status(
self.thread_watch_manager
.loaded_status_for_thread(&thread.id)
.await,
false,
);
let response = ThreadResumeResponse {
thread,
@@ -3206,6 +3237,7 @@ impl CodexMessageProcessor {
match read_rollout_items_from_rollout(rollout_path).await {
Ok(items) => {
thread.turns = build_turns_from_rollout_items(&items);
self.attach_thread_name(thread_id, &mut thread).await;
Some(thread)
}
Err(err) => {
@@ -3222,6 +3254,17 @@ impl CodexMessageProcessor {
}
}
async fn attach_thread_name(&self, thread_id: ThreadId, thread: &mut Thread) {
match find_thread_name_by_id(&self.config.codex_home, &thread_id).await {
Ok(name) => {
thread.name = name;
}
Err(err) => {
warn!("Failed to read thread name for {thread_id}: {err}");
}
}
}
async fn thread_fork(&mut self, request_id: ConnectionRequestId, params: ThreadForkParams) {
let ThreadForkParams {
thread_id,
@@ -3419,6 +3462,7 @@ impl CodexMessageProcessor {
return;
}
};
// forked thread names do not inherit the source thread name
match read_rollout_items_from_rollout(rollout_path.as_path()).await {
Ok(items) => {
thread.turns = build_turns_from_rollout_items(&items);
@@ -3440,10 +3484,12 @@ impl CodexMessageProcessor {
.upsert_thread(thread.clone())
.await;
thread.status = self
.thread_watch_manager
.loaded_status_for_thread(&thread.id)
.await;
thread.status = resolve_thread_status(
self.thread_watch_manager
.loaded_status_for_thread(&thread.id)
.await,
false,
);
let response = ThreadForkResponse {
thread: thread.clone(),
@@ -5581,10 +5627,12 @@ impl CodexMessageProcessor {
self.thread_watch_manager
.upsert_thread(thread.clone())
.await;
thread.status = self
.thread_watch_manager
.loaded_status_for_thread(&thread.id)
.await;
thread.status = resolve_thread_status(
self.thread_watch_manager
.loaded_status_for_thread(&thread.id)
.await,
false,
);
let notif = ThreadStartedNotification { thread };
self.outgoing
.send_server_notification(ServerNotification::ThreadStarted(notif))
@@ -5823,11 +5871,12 @@ impl CodexMessageProcessor {
let thread_watch_manager = self.thread_watch_manager.clone();
let fallback_model_provider = self.config.model_provider_id.clone();
let single_client_mode = self.single_client_mode;
let codex_home = self.config.codex_home.clone();
tokio::spawn(async move {
loop {
tokio::select! {
_ = &mut cancel_rx => {
// User has unsubscribed, so exit this task.
// Listener was superseded or the thread is being torn down.
break;
}
event = conversation.next_event() => {
@@ -5903,6 +5952,7 @@ impl CodexMessageProcessor {
thread_watch_manager.clone(),
api_version,
fallback_model_provider.clone(),
codex_home.as_path(),
)
.await;
}
@@ -5916,6 +5966,7 @@ impl CodexMessageProcessor {
) => {
handle_pending_thread_resume_request(
conversation_id,
codex_home.as_path(),
&thread_state,
&thread_watch_manager,
&outgoing_for_task,
@@ -6235,6 +6286,7 @@ impl CodexMessageProcessor {
async fn handle_pending_thread_resume_request(
conversation_id: ThreadId,
codex_home: &Path,
thread_state: &Arc<Mutex<ThreadState>>,
thread_watch_manager: &ThreadWatchManager,
outgoing: &Arc<OutgoingMessageSender>,
@@ -6244,6 +6296,17 @@ async fn handle_pending_thread_resume_request(
let state = thread_state.lock().await;
state.active_turn_snapshot()
};
tracing::debug!(
thread_id = %conversation_id,
request_id = ?pending.request_id,
active_turn_present = active_turn.is_some(),
active_turn_id = ?active_turn.as_ref().map(|turn| turn.id.as_str()),
active_turn_status = ?active_turn.as_ref().map(|turn| &turn.status),
"composing running thread resume response"
);
let mut has_in_progress_turn = active_turn
.as_ref()
.is_some_and(|turn| matches!(turn.status, TurnStatus::InProgress));
let request_id = pending.request_id;
let connection_id = request_id.connection_id;
@@ -6270,9 +6333,20 @@ async fn handle_pending_thread_resume_request(
return;
}
};
thread.status = thread_watch_manager
.loaded_status_for_thread(&thread.id)
.await;
has_in_progress_turn = has_in_progress_turn || latest_turn_is_in_progress(&thread.turns);
let status = resolve_thread_status(
thread_watch_manager
.loaded_status_for_thread(&thread.id)
.await,
has_in_progress_turn,
);
thread.status = status;
match find_thread_name_by_id(codex_home, &conversation_id).await {
Ok(thread_name) => thread.name = thread_name,
Err(err) => warn!("Failed to read thread name for {conversation_id}: {err}"),
}
let ThreadConfigSnapshot {
model,
@@ -6333,6 +6407,12 @@ fn merge_turn_history_with_active_turn(turns: &mut Vec<Turn>, active_turn: Turn)
turns.push(active_turn);
}
fn latest_turn_is_in_progress(turns: &[Turn]) -> bool {
turns
.last()
.is_some_and(|turn| matches!(turn.status, TurnStatus::InProgress))
}
fn collect_resume_override_mismatches(
request: &ThreadResumeParams,
config_snapshot: &ThreadConfigSnapshot,
@@ -6994,6 +7074,7 @@ fn build_thread_from_snapshot(
agent_role: config_snapshot.session_source.get_agent_role(),
source: config_snapshot.session_source.clone().into(),
git_info: None,
name: None,
turns: Vec::new(),
}
}
@@ -7034,6 +7115,7 @@ pub(crate) fn summary_to_thread(summary: ConversationSummary) -> Thread {
agent_role: source.get_agent_role(),
source: source.into(),
git_info,
name: None,
turns: Vec::new(),
}
}
@@ -7270,9 +7352,31 @@ mod tests {
Ok(())
}
#[test]
fn latest_turn_is_in_progress_ignores_stale_historical_in_progress_turns() {
let turn = |id: &str, status: TurnStatus| Turn {
id: id.to_string(),
items: Vec::new(),
status,
error: None,
};
let turns = vec![
turn("old", TurnStatus::InProgress),
turn("new", TurnStatus::Completed),
];
assert!(!latest_turn_is_in_progress(&turns));
let turns = vec![
turn("old", TurnStatus::Completed),
turn("new", TurnStatus::InProgress),
];
assert!(latest_turn_is_in_progress(&turns));
}
#[tokio::test]
async fn removing_one_listener_does_not_cancel_other_subscriptions_for_same_thread()
-> Result<()> {
async fn removing_listeners_retains_thread_listener_when_last_subscriber_leaves() -> Result<()>
{
let mut manager = ThreadStateManager::new();
let thread_id = ThreadId::from_string("ad7f0408-99b8-4f6e-a46f-bd0eec433370")?;
let listener_a = Uuid::new_v4();
@@ -7299,7 +7403,13 @@ mod tests {
.is_err()
);
assert_eq!(manager.remove_listener(listener_b).await, Some(thread_id));
assert_eq!(cancel_rx.await, Ok(()));
assert!(
tokio::time::timeout(Duration::from_millis(20), &mut cancel_rx)
.await
.is_err()
);
let state = manager.thread_state(thread_id);
assert!(state.lock().await.subscribed_connection_ids().is_empty());
Ok(())
}
@@ -7351,28 +7461,79 @@ mod tests {
}
#[tokio::test]
async fn removing_connection_clears_subscription_and_listener_when_last_subscriber()
async fn removing_connection_retains_listener_and_active_turn_when_last_subscriber_disconnects()
-> Result<()> {
let mut manager = ThreadStateManager::new();
let thread_id = ThreadId::from_string("ad7f0408-99b8-4f6e-a46f-bd0eec433370")?;
let listener = Uuid::new_v4();
let connection = ConnectionId(1);
let (cancel_tx, cancel_rx) = oneshot::channel();
let (cancel_tx, mut cancel_rx) = oneshot::channel();
manager
.set_listener(listener, thread_id, connection, false)
.await;
{
let state = manager.thread_state(thread_id);
state.lock().await.cancel_tx = Some(cancel_tx);
let mut state = state.lock().await;
state.cancel_tx = Some(cancel_tx);
state.track_current_turn_event(&EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: "turn-1".to_string(),
model_context_window: None,
collaboration_mode_kind: Default::default(),
},
));
}
manager.remove_connection(connection).await;
assert_eq!(cancel_rx.await, Ok(()));
assert!(
tokio::time::timeout(Duration::from_millis(20), &mut cancel_rx)
.await
.is_err()
);
assert_eq!(manager.remove_listener(listener).await, None);
let state = manager.thread_state(thread_id);
assert!(state.lock().await.subscribed_connection_ids().is_empty());
let state = state.lock().await;
assert!(state.subscribed_connection_ids().is_empty());
assert!(state.cancel_tx.is_some());
let active_turn = state.active_turn_snapshot().expect("active turn snapshot");
assert_eq!(active_turn.id, "turn-1");
assert_eq!(active_turn.status, TurnStatus::InProgress);
Ok(())
}
#[tokio::test]
async fn removing_thread_state_clears_listener_and_active_turn_history() -> Result<()> {
let mut manager = ThreadStateManager::new();
let thread_id = ThreadId::from_string("ad7f0408-99b8-4f6e-a46f-bd0eec433370")?;
let connection = ConnectionId(1);
let (cancel_tx, cancel_rx) = oneshot::channel();
manager
.ensure_connection_subscribed(thread_id, connection, false)
.await;
{
let state = manager.thread_state(thread_id);
let mut state = state.lock().await;
state.cancel_tx = Some(cancel_tx);
state.track_current_turn_event(&EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: "turn-1".to_string(),
model_context_window: None,
collaboration_mode_kind: Default::default(),
},
));
}
manager.remove_thread_state(thread_id).await;
assert_eq!(cancel_rx.await, Ok(()));
let state = manager.thread_state(thread_id);
let state = state.lock().await;
assert!(state.subscribed_connection_ids().is_empty());
assert!(state.cancel_tx.is_none());
assert!(state.active_turn_snapshot().is_none());
Ok(())
}

View File

@@ -23,10 +23,12 @@ struct AppServerArgs {
}
fn main() -> anyhow::Result<()> {
if codex_core::maybe_run_zsh_exec_wrapper_mode()? {
return Ok(());
}
arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move {
// Run wrapper mode only after arg0 dispatch so `codex-linux-sandbox`
// invocations don't get misclassified as zsh exec-wrapper calls.
if codex_core::maybe_run_zsh_exec_wrapper_mode()? {
return Ok(());
}
let args = AppServerArgs::parse();
let managed_config_path = managed_config_path_from_debug_env();
let loader_overrides = LoaderOverrides {

View File

@@ -175,7 +175,13 @@ impl ThreadStateManager {
thread_state.remove_connection(subscription_state.connection_id);
}
if thread_state.subscribed_connection_ids().is_empty() {
thread_state.clear_listener();
tracing::debug!(
thread_id = %thread_id,
subscription_id = %subscription_id,
connection_id = ?subscription_state.connection_id,
listener_generation = thread_state.listener_generation,
"retaining thread listener after last subscription removed"
);
}
}
Some(thread_id)
@@ -183,7 +189,15 @@ impl ThreadStateManager {
pub(crate) async fn remove_thread_state(&mut self, thread_id: ThreadId) {
if let Some(thread_state) = self.thread_states.remove(&thread_id) {
thread_state.lock().await.clear_listener();
let mut thread_state = thread_state.lock().await;
tracing::debug!(
thread_id = %thread_id,
listener_generation = thread_state.listener_generation,
had_listener = thread_state.cancel_tx.is_some(),
had_active_turn = thread_state.active_turn_snapshot().is_some(),
"clearing thread listener during thread-state teardown"
);
thread_state.clear_listener();
}
self.subscription_state_by_id
.retain(|_, state| state.thread_id != thread_id);
@@ -254,7 +268,11 @@ impl ThreadStateManager {
let mut thread_state = thread_state.lock().await;
thread_state.remove_connection(connection_id);
if thread_state.subscribed_connection_ids().is_empty() {
thread_state.clear_listener();
tracing::debug!(
connection_id = ?connection_id,
listener_generation = thread_state.listener_generation,
"retaining thread listener after connection disconnect left zero subscribers"
);
}
}
return;
@@ -265,7 +283,12 @@ impl ThreadStateManager {
let mut thread_state = thread_state.lock().await;
thread_state.remove_connection(connection_id);
if thread_state.subscribed_connection_ids().is_empty() {
thread_state.clear_listener();
tracing::debug!(
thread_id = %thread_id,
connection_id = ?connection_id,
listener_generation = thread_state.listener_generation,
"retaining thread listener after connection disconnect left zero subscribers"
);
}
}
}

View File

@@ -239,6 +239,22 @@ impl ThreadWatchManager {
}
}
pub(crate) fn resolve_thread_status(
status: ThreadStatus,
has_in_progress_turn: bool,
) -> ThreadStatus {
// Running-turn events can arrive before the watch runtime state is observed by
// the listener loop. In that window we prefer to reflect a real active turn as
// `Active` instead of `Idle`/`NotLoaded`.
if has_in_progress_turn && matches!(status, ThreadStatus::Idle | ThreadStatus::NotLoaded) {
return ThreadStatus::Active {
active_flags: Vec::new(),
};
}
status
}
#[derive(Default)]
struct ThreadWatchState {
runtime_by_thread_id: HashMap<String, RuntimeFacts>,
@@ -459,6 +475,37 @@ mod tests {
);
}
#[test]
fn resolves_in_progress_turn_to_active_status() {
let status = resolve_thread_status(ThreadStatus::Idle, true);
assert_eq!(
status,
ThreadStatus::Active {
active_flags: Vec::new(),
}
);
let status = resolve_thread_status(ThreadStatus::NotLoaded, true);
assert_eq!(
status,
ThreadStatus::Active {
active_flags: Vec::new(),
}
);
}
#[test]
fn keeps_status_when_no_in_progress_turn() {
assert_eq!(
resolve_thread_status(ThreadStatus::Idle, false),
ThreadStatus::Idle
);
assert_eq!(
resolve_thread_status(ThreadStatus::SystemError, false),
ThreadStatus::SystemError
);
}
#[tokio::test]
async fn system_error_sets_idle_flag_until_next_turn() {
let manager = ThreadWatchManager::new();
@@ -626,6 +673,7 @@ mod tests {
agent_role: None,
source,
git_info: None,
name: None,
turns: Vec::new(),
}
}

View File

@@ -59,6 +59,7 @@ use codex_app_server_protocol::ThreadLoadedListParams;
use codex_app_server_protocol::ThreadReadParams;
use codex_app_server_protocol::ThreadResumeParams;
use codex_app_server_protocol::ThreadRollbackParams;
use codex_app_server_protocol::ThreadSetNameParams;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadUnarchiveParams;
use codex_app_server_protocol::TurnCompletedNotification;
@@ -418,6 +419,15 @@ impl McpProcess {
self.send_request("thread/archive", params).await
}
/// Send a `thread/name/set` JSON-RPC request.
pub async fn send_thread_set_name_request(
&mut self,
params: ThreadSetNameParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("thread/name/set", params).await
}
/// Send a `thread/unarchive` JSON-RPC request.
pub async fn send_thread_unarchive_request(
&mut self,

View File

@@ -21,15 +21,15 @@ use codex_app_server_protocol::SendUserMessageResponse;
use codex_app_server_protocol::SendUserTurnParams;
use codex_app_server_protocol::SendUserTurnResponse;
use codex_app_server_protocol::ServerRequest;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol_config_types::ReasoningSummary;
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::SandboxPolicy;
use pretty_assertions::assert_eq;
use std::env;
use std::path::Path;
@@ -312,7 +312,7 @@ async fn test_send_user_turn_changes_approval_policy_behavior() -> Result<()> {
// Approve so the first turn can complete
mcp.send_response(
request_id,
serde_json::json!({ "decision": codex_core::protocol::ReviewDecision::Approved }),
serde_json::json!({ "decision": codex_protocol::protocol::ReviewDecision::Approved }),
)
.await?;

View File

@@ -9,12 +9,12 @@ use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::SandboxSettings;
use codex_app_server_protocol::Tools;
use codex_app_server_protocol::UserSavedConfig;
use codex_core::protocol::AskForApproval;
use codex_protocol::config_types::ForcedLoginMethod;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::config_types::Verbosity;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
use std::path::Path;

View File

@@ -10,7 +10,7 @@ use codex_app_server_protocol::NewConversationParams; // reused for overrides sh
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::SessionConfiguredNotification;
use codex_core::protocol::EventMsg;
use codex_protocol::protocol::EventMsg;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
use tokio::time::timeout;

View File

@@ -12,7 +12,7 @@ use codex_app_server_protocol::NewConversationResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::SendUserMessageParams;
use codex_app_server_protocol::SendUserMessageResponse;
use codex_core::protocol::TurnAbortReason;
use codex_protocol::protocol::TurnAbortReason;
use core_test_support::skip_if_no_network;
use tempfile::TempDir;
use tokio::time::timeout;

View File

@@ -12,9 +12,9 @@ use codex_app_server_protocol::ResumeConversationParams;
use codex_app_server_protocol::ResumeConversationResponse;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::SessionConfiguredNotification;
use codex_core::protocol::EventMsg;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::EventMsg;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
use tokio::time::timeout;

View File

@@ -9,10 +9,10 @@ use codex_app_server_protocol::NewConversationResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::SendUserTurnParams;
use codex_app_server_protocol::SendUserTurnResponse;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::SandboxPolicy;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
use core_test_support::responses;
use core_test_support::skip_if_no_network;
use pretty_assertions::assert_eq;

View File

@@ -209,7 +209,7 @@ async fn review_start_exec_approval_item_id_matches_command_execution_item() ->
mcp.send_response(
request_id,
serde_json::json!({ "decision": codex_core::protocol::ReviewDecision::Approved }),
serde_json::json!({ "decision": codex_protocol::protocol::ReviewDecision::Approved }),
)
.await?;
timeout(

View File

@@ -18,6 +18,7 @@ use codex_app_server_protocol::ThreadStatus;
use codex_app_server_protocol::TurnStatus;
use codex_app_server_protocol::UserInput;
use pretty_assertions::assert_eq;
use serde_json::Value;
use std::path::Path;
use tempfile::TempDir;
use tokio::time::timeout;
@@ -70,8 +71,20 @@ async fn thread_fork_creates_new_thread_and_emits_started() -> Result<()> {
mcp.read_stream_until_response_message(RequestId::Integer(fork_id)),
)
.await??;
let fork_result = fork_resp.result.clone();
let ThreadForkResponse { thread, .. } = to_response::<ThreadForkResponse>(fork_resp)?;
// Wire contract: thread title field is `name`, serialized as null when unset.
let thread_json = fork_result
.get("thread")
.and_then(Value::as_object)
.expect("thread/fork result.thread must be an object");
assert_eq!(
thread_json.get("name"),
Some(&Value::Null),
"forked threads do not inherit a name; expected `name: null`"
);
let after_contents = std::fs::read_to_string(&original_path)?;
assert_eq!(
after_contents, original_contents,
@@ -87,6 +100,7 @@ async fn thread_fork_creates_new_thread_and_emits_started() -> Result<()> {
assert_ne!(thread_path, original_path);
assert!(thread.cwd.is_absolute());
assert_eq!(thread.source, SessionSource::VsCode);
assert_eq!(thread.name, None);
assert_eq!(
thread.turns.len(),
@@ -115,6 +129,16 @@ async fn thread_fork_creates_new_thread_and_emits_started() -> Result<()> {
mcp.read_stream_until_notification_message("thread/started"),
)
.await??;
let started_params = notif.params.clone().expect("params must be present");
let started_thread_json = started_params
.get("thread")
.and_then(Value::as_object)
.expect("thread/started params.thread must be an object");
assert_eq!(
started_thread_json.get("name"),
Some(&Value::Null),
"thread/started must serialize `name: null` when unset"
);
let started: ThreadStartedNotification =
serde_json::from_value(notif.params.expect("params must be present"))?;
assert_eq!(started.thread, thread);

View File

@@ -8,8 +8,14 @@ use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::SessionSource;
use codex_app_server_protocol::ThreadItem;
use codex_app_server_protocol::ThreadListParams;
use codex_app_server_protocol::ThreadListResponse;
use codex_app_server_protocol::ThreadReadParams;
use codex_app_server_protocol::ThreadReadResponse;
use codex_app_server_protocol::ThreadResumeParams;
use codex_app_server_protocol::ThreadResumeResponse;
use codex_app_server_protocol::ThreadSetNameParams;
use codex_app_server_protocol::ThreadSetNameResponse;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
use codex_app_server_protocol::ThreadStatus;
@@ -21,6 +27,7 @@ use codex_protocol::user_input::ByteRange;
use codex_protocol::user_input::TextElement;
use core_test_support::responses;
use pretty_assertions::assert_eq;
use serde_json::Value;
use std::path::Path;
use std::path::PathBuf;
use tempfile::TempDir;
@@ -192,6 +199,155 @@ async fn thread_read_loaded_thread_returns_precomputed_path_before_materializati
Ok(())
}
#[tokio::test]
async fn thread_name_set_is_reflected_in_read_list_and_resume() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
let preview = "Saved user message";
let conversation_id = create_fake_rollout_with_text_elements(
codex_home.path(),
"2025-01-05T12-00-00",
"2025-01-05T12:00:00Z",
preview,
vec![],
Some("mock_provider"),
None,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
// `thread/name/set` operates on loaded threads (via ThreadManager). A rollout existing on disk
// is not enough; we must `thread/resume` first to load it into the running server.
let pre_resume_id = mcp
.send_thread_resume_request(ThreadResumeParams {
thread_id: conversation_id.clone(),
..Default::default()
})
.await?;
let pre_resume_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(pre_resume_id)),
)
.await??;
let ThreadResumeResponse {
thread: pre_resumed,
..
} = to_response::<ThreadResumeResponse>(pre_resume_resp)?;
assert_eq!(pre_resumed.id, conversation_id);
// Set a user-facing thread title.
let new_name = "My renamed thread";
let set_id = mcp
.send_thread_set_name_request(ThreadSetNameParams {
thread_id: conversation_id.clone(),
name: new_name.to_string(),
})
.await?;
let set_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(set_id)),
)
.await??;
let _: ThreadSetNameResponse = to_response::<ThreadSetNameResponse>(set_resp)?;
// Read should now surface `thread.name`, and the wire payload must include `name`.
let read_id = mcp
.send_thread_read_request(ThreadReadParams {
thread_id: conversation_id.clone(),
include_turns: false,
})
.await?;
let read_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(read_id)),
)
.await??;
let read_result = read_resp.result.clone();
let ThreadReadResponse { thread } = to_response::<ThreadReadResponse>(read_resp)?;
assert_eq!(thread.id, conversation_id);
assert_eq!(thread.name.as_deref(), Some(new_name));
let thread_json = read_result
.get("thread")
.and_then(Value::as_object)
.expect("thread/read result.thread must be an object");
assert_eq!(
thread_json.get("name").and_then(Value::as_str),
Some(new_name),
"thread/read must serialize `thread.name` on the wire"
);
// List should also surface the name.
let list_id = mcp
.send_thread_list_request(ThreadListParams {
cursor: None,
limit: Some(50),
sort_key: None,
model_providers: Some(vec!["mock_provider".to_string()]),
source_kinds: None,
archived: None,
cwd: None,
})
.await?;
let list_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(list_id)),
)
.await??;
let list_result = list_resp.result.clone();
let ThreadListResponse { data, .. } = to_response::<ThreadListResponse>(list_resp)?;
let listed = data
.iter()
.find(|t| t.id == conversation_id)
.expect("thread/list should include the created thread");
assert_eq!(listed.name.as_deref(), Some(new_name));
let listed_json = list_result
.get("data")
.and_then(Value::as_array)
.expect("thread/list result.data must be an array")
.iter()
.find(|t| t.get("id").and_then(Value::as_str) == Some(&conversation_id))
.and_then(Value::as_object)
.expect("thread/list should include the created thread as an object");
assert_eq!(
listed_json.get("name").and_then(Value::as_str),
Some(new_name),
"thread/list must serialize `thread.name` on the wire"
);
// Resume should also surface the name.
let resume_id = mcp
.send_thread_resume_request(ThreadResumeParams {
thread_id: conversation_id.clone(),
..Default::default()
})
.await?;
let resume_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(resume_id)),
)
.await??;
let resume_result = resume_resp.result.clone();
let ThreadResumeResponse {
thread: resumed, ..
} = to_response::<ThreadResumeResponse>(resume_resp)?;
assert_eq!(resumed.id, conversation_id);
assert_eq!(resumed.name.as_deref(), Some(new_name));
let resumed_json = resume_result
.get("thread")
.and_then(Value::as_object)
.expect("thread/resume result.thread must be an object");
assert_eq!(
resumed_json.get("name").and_then(Value::as_str),
Some(new_name),
"thread/resume must serialize `thread.name` on the wire"
);
Ok(())
}
#[tokio::test]
async fn thread_read_include_turns_rejects_unmaterialized_loaded_thread() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;

View File

@@ -16,6 +16,7 @@ use codex_app_server_protocol::ThreadStatus;
use codex_app_server_protocol::TurnStartParams;
use codex_app_server_protocol::UserInput as V2UserInput;
use pretty_assertions::assert_eq;
use serde_json::Value;
use tempfile::TempDir;
use tokio::time::timeout;
@@ -107,10 +108,23 @@ async fn thread_rollback_drops_last_turns_and_persists_to_rollout() -> Result<()
mcp.read_stream_until_response_message(RequestId::Integer(rollback_id)),
)
.await??;
let rollback_result = rollback_resp.result.clone();
let ThreadRollbackResponse {
thread: rolled_back_thread,
} = to_response::<ThreadRollbackResponse>(rollback_resp)?;
// Wire contract: thread title field is `name`, serialized as null when unset.
let thread_json = rollback_result
.get("thread")
.and_then(Value::as_object)
.expect("thread/rollback result.thread must be an object");
assert_eq!(rolled_back_thread.name, None);
assert_eq!(
thread_json.get("name"),
Some(&Value::Null),
"thread/rollback must serialize `name: null` when unset"
);
assert_eq!(rolled_back_thread.turns.len(), 1);
assert_eq!(rolled_back_thread.status, ThreadStatus::Idle);
assert_eq!(rolled_back_thread.turns[0].items.len(), 2);

View File

@@ -13,6 +13,7 @@ use codex_app_server_protocol::ThreadStatus;
use codex_core::config::set_project_trust_level;
use codex_protocol::config_types::TrustLevel;
use codex_protocol::openai_models::ReasoningEffort;
use serde_json::Value;
use std::path::Path;
use tempfile::TempDir;
use tokio::time::timeout;
@@ -45,6 +46,7 @@ async fn thread_start_creates_thread_and_emits_started() -> Result<()> {
mcp.read_stream_until_response_message(RequestId::Integer(req_id)),
)
.await??;
let resp_result = resp.result.clone();
let ThreadStartResponse {
thread,
model_provider,
@@ -68,12 +70,34 @@ async fn thread_start_creates_thread_and_emits_started() -> Result<()> {
"fresh thread rollout should not be materialized until first user message"
);
// Wire contract: thread title field is `name`, serialized as null when unset.
let thread_json = resp_result
.get("thread")
.and_then(Value::as_object)
.expect("thread/start result.thread must be an object");
assert_eq!(
thread_json.get("name"),
Some(&Value::Null),
"new threads should serialize `name: null`"
);
assert_eq!(thread.name, None);
// A corresponding thread/started notification should arrive.
let notif: JSONRPCNotification = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("thread/started"),
)
.await??;
let started_params = notif.params.clone().expect("params must be present");
let started_thread_json = started_params
.get("thread")
.and_then(Value::as_object)
.expect("thread/started params.thread must be an object");
assert_eq!(
started_thread_json.get("name"),
Some(&Value::Null),
"thread/started should serialize `name: null` for new threads"
);
let started: ThreadStartedNotification =
serde_json::from_value(notif.params.expect("params must be present"))?;
assert_eq!(started.thread, thread);

View File

@@ -18,6 +18,7 @@ use codex_app_server_protocol::UserInput;
use codex_core::find_archived_thread_path_by_id_str;
use codex_core::find_thread_path_by_id_str;
use pretty_assertions::assert_eq;
use serde_json::Value;
use std::fs::FileTimes;
use std::fs::OpenOptions;
use std::path::Path;
@@ -120,6 +121,7 @@ async fn thread_unarchive_moves_rollout_back_into_sessions_directory() -> Result
mcp.read_stream_until_response_message(RequestId::Integer(unarchive_id)),
)
.await??;
let unarchive_result = unarchive_resp.result.clone();
let ThreadUnarchiveResponse {
thread: unarchived_thread,
} = to_response::<ThreadUnarchiveResponse>(unarchive_resp)?;
@@ -140,6 +142,18 @@ async fn thread_unarchive_moves_rollout_back_into_sessions_directory() -> Result
);
assert_eq!(unarchived_thread.status, ThreadStatus::NotLoaded);
// Wire contract: thread title field is `name`, serialized as null when unset.
let thread_json = unarchive_result
.get("thread")
.and_then(Value::as_object)
.expect("thread/unarchive result.thread must be an object");
assert_eq!(unarchived_thread.name, None);
assert_eq!(
thread_json.get("name"),
Some(&Value::Null),
"thread/unarchive must serialize `name: null` when unset"
);
let rollout_path_display = rollout_path.display();
assert!(
rollout_path.exists(),

View File

@@ -39,10 +39,10 @@ use codex_core::config::ConfigToml;
use codex_core::features::FEATURES;
use codex_core::features::Feature;
use codex_core::personality_migration::PERSONALITY_MIGRATION_FILENAME;
use codex_core::protocol_config_types::ReasoningSummary;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::Settings;
use codex_protocol::openai_models::ReasoningEffort;
use core_test_support::responses;
@@ -847,7 +847,9 @@ async fn turn_start_exec_approval_toggle_v2() -> Result<()> {
// Approve and wait for task completion
mcp.send_response(
request_id,
serde_json::json!({ "decision": codex_core::protocol::ReviewDecision::Approved }),
serde_json::to_value(CommandExecutionRequestApprovalResponse {
decision: CommandExecutionApprovalDecision::Accept,
})?,
)
.await?;
timeout(
@@ -1035,7 +1037,7 @@ async fn turn_start_exec_approval_decline_v2() -> Result<()> {
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("codex/event/task_complete"),
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??;
@@ -1129,7 +1131,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
.await??;
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("codex/event/task_complete"),
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??;
mcp.clear_message_buffer();

View File

@@ -2,18 +2,15 @@
//
// Running these tests with the patched zsh fork:
//
// The suite uses `CODEX_TEST_ZSH_PATH` when set. Example:
// CODEX_TEST_ZSH_PATH="$HOME/.local/codex-zsh-77045ef/bin/zsh" \
// cargo test -p codex-app-server turn_start_zsh_fork -- --nocapture
//
// For a single test:
// CODEX_TEST_ZSH_PATH="$HOME/.local/codex-zsh-77045ef/bin/zsh" \
// cargo test -p codex-app-server turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2 -- --nocapture
// The suite resolves the shared test-only zsh DotSlash file at
// `exec-server/tests/suite/zsh` via DotSlash on first use, so `dotslash` and
// network access are required the first time the artifact is fetched.
use anyhow::Result;
use app_test_support::McpProcess;
use app_test_support::create_final_assistant_message_sse_response;
use app_test_support::create_mock_responses_server_sequence;
use app_test_support::create_mock_responses_server_sequence_unchecked;
use app_test_support::create_shell_command_sse_response;
use app_test_support::to_response;
use codex_app_server_protocol::CommandExecutionApprovalDecision;
@@ -38,6 +35,7 @@ use core_test_support::responses;
use core_test_support::skip_if_no_network;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use tempfile::TempDir;
use tokio::time::timeout;
@@ -57,7 +55,7 @@ async fn turn_start_shell_zsh_fork_executes_command_v2() -> Result<()> {
let workspace = tmp.path().join("workspace");
std::fs::create_dir(&workspace)?;
let Some(zsh_path) = find_test_zsh_path() else {
let Some(zsh_path) = find_test_zsh_path()? else {
eprintln!("skipping zsh fork test: no zsh executable found");
return Ok(());
};
@@ -82,7 +80,7 @@ async fn turn_start_shell_zsh_fork_executes_command_v2() -> Result<()> {
&zsh_path,
)?;
let mut mcp = McpProcess::new(&codex_home).await?;
let mut mcp = create_zsh_test_mcp_process(&codex_home, &workspace).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let start_id = mcp
@@ -111,7 +109,7 @@ async fn turn_start_shell_zsh_fork_executes_command_v2() -> Result<()> {
sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::DangerFullAccess),
model: Some("mock-model".to_string()),
effort: Some(codex_protocol::openai_models::ReasoningEffort::Medium),
summary: Some(codex_core::protocol_config_types::ReasoningSummary::Auto),
summary: Some(codex_protocol::config_types::ReasoningSummary::Auto),
..Default::default()
})
.await?;
@@ -167,7 +165,7 @@ async fn turn_start_shell_zsh_fork_exec_approval_decline_v2() -> Result<()> {
let workspace = tmp.path().join("workspace");
std::fs::create_dir(&workspace)?;
let Some(zsh_path) = find_test_zsh_path() else {
let Some(zsh_path) = find_test_zsh_path()? else {
eprintln!("skipping zsh fork decline test: no zsh executable found");
return Ok(());
};
@@ -199,7 +197,7 @@ async fn turn_start_shell_zsh_fork_exec_approval_decline_v2() -> Result<()> {
&zsh_path,
)?;
let mut mcp = McpProcess::new(&codex_home).await?;
let mut mcp = create_zsh_test_mcp_process(&codex_home, &workspace).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let start_id = mcp
@@ -303,7 +301,7 @@ async fn turn_start_shell_zsh_fork_exec_approval_cancel_v2() -> Result<()> {
let workspace = tmp.path().join("workspace");
std::fs::create_dir(&workspace)?;
let Some(zsh_path) = find_test_zsh_path() else {
let Some(zsh_path) = find_test_zsh_path()? else {
eprintln!("skipping zsh fork cancel test: no zsh executable found");
return Ok(());
};
@@ -332,7 +330,7 @@ async fn turn_start_shell_zsh_fork_exec_approval_cancel_v2() -> Result<()> {
&zsh_path,
)?;
let mut mcp = McpProcess::new(&codex_home).await?;
let mut mcp = create_zsh_test_mcp_process(&codex_home, &workspace).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let start_id = mcp
@@ -434,7 +432,7 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2()
let workspace = tmp.path().join("workspace");
std::fs::create_dir(&workspace)?;
let Some(zsh_path) = find_test_zsh_path() else {
let Some(zsh_path) = find_test_zsh_path()? else {
eprintln!("skipping zsh fork subcommand decline test: no zsh executable found");
return Ok(());
};
@@ -446,6 +444,29 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2()
return Ok(());
}
eprintln!("using zsh path for zsh-fork test: {}", zsh_path.display());
let zsh_path_for_config = {
// App-server config accepts only a zsh path, not extra argv. Use a
// wrapper so this test can force `-df` and downgrade `-lc` to `-c`
// to avoid rc/login-shell startup noise.
let path = workspace.join("zsh-no-rc");
std::fs::write(
&path,
format!(
r#"#!/bin/sh
if [ "$1" = "-lc" ]; then
shift
set -- -c "$@"
fi
exec "{}" -df "$@"
"#,
zsh_path.display()
),
)?;
let mut permissions = std::fs::metadata(&path)?.permissions();
permissions.set_mode(0o755);
std::fs::set_permissions(&path, permissions)?;
path
};
let tool_call_arguments = serde_json::to_string(&serde_json::json!({
"command": "/usr/bin/true && /usr/bin/true",
@@ -461,7 +482,16 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2()
),
responses::ev_completed("resp-1"),
]);
let server = create_mock_responses_server_sequence(vec![response]).await;
let no_op_response = responses::sse(vec![
responses::ev_response_created("resp-2"),
responses::ev_completed("resp-2"),
]);
// Linux CI has occasionally issued a second `/responses` POST after the
// subcommand-decline flow. This test is about approval/decline behavior in
// the zsh fork, not exact model request count, so allow an extra request
// and return a harmless no-op response if it arrives.
let server =
create_mock_responses_server_sequence_unchecked(vec![response, no_op_response]).await;
create_config_toml(
&codex_home,
&server.uri(),
@@ -471,10 +501,10 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2()
(Feature::UnifiedExec, false),
(Feature::ShellSnapshot, false),
]),
&zsh_path,
&zsh_path_for_config,
)?;
let mut mcp = McpProcess::new(&codex_home).await?;
let mut mcp = create_zsh_test_mcp_process(&codex_home, &workspace).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let start_id = mcp
@@ -500,12 +530,20 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2()
}],
cwd: Some(workspace.clone()),
approval_policy: Some(codex_app_server_protocol::AskForApproval::OnRequest),
sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::ReadOnly {
access: codex_app_server_protocol::ReadOnlyAccess::FullAccess,
sandbox_policy: Some(if cfg!(target_os = "linux") {
// The zsh exec-bridge wrapper uses a Unix socket back to the parent
// process. Linux restricted sandbox seccomp denies connect(2), so use
// full access here; this test is validating zsh approval/decline
// behavior, not Linux sandboxing.
codex_app_server_protocol::SandboxPolicy::DangerFullAccess
} else {
codex_app_server_protocol::SandboxPolicy::ReadOnly {
access: codex_app_server_protocol::ReadOnlyAccess::FullAccess,
}
}),
model: Some("mock-model".to_string()),
effort: Some(codex_protocol::openai_models::ReasoningEffort::Medium),
summary: Some(codex_core::protocol_config_types::ReasoningSummary::Auto),
summary: Some(codex_protocol::config_types::ReasoningSummary::Auto),
..Default::default()
})
.await?;
@@ -517,10 +555,13 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2()
let TurnStartResponse { turn } = to_response::<TurnStartResponse>(turn_resp)?;
let mut approval_ids = Vec::new();
for decision in [
let mut saw_parent_approval = false;
let target_decisions = [
CommandExecutionApprovalDecision::Accept,
CommandExecutionApprovalDecision::Cancel,
] {
];
let mut target_decision_index = 0;
while target_decision_index < target_decisions.len() {
let server_req = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_request_message(),
@@ -531,13 +572,40 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2()
panic!("expected CommandExecutionRequestApproval request");
};
assert_eq!(params.item_id, "call-zsh-fork-subcommand-decline");
approval_ids.push(
params
.approval_id
.clone()
.expect("approval_id must be present for zsh subcommand approvals"),
);
assert_eq!(params.thread_id, thread.id);
let is_target_subcommand = params.command.as_deref() == Some("/usr/bin/true");
if is_target_subcommand {
approval_ids.push(
params
.approval_id
.clone()
.expect("approval_id must be present for zsh subcommand approvals"),
);
}
let decision = if is_target_subcommand {
let decision = target_decisions[target_decision_index].clone();
target_decision_index += 1;
decision
} else {
let command = params
.command
.as_deref()
.expect("approval command should be present");
assert!(
!saw_parent_approval,
"unexpected extra non-target approval: {command}"
);
assert!(
command.contains("zsh-no-rc"),
"expected parent zsh wrapper approval, got: {command}"
);
assert!(
command.contains("/usr/bin/true && /usr/bin/true"),
"expected tool command in parent approval, got: {command}"
);
saw_parent_approval = true;
CommandExecutionApprovalDecision::Accept
};
mcp.send_response(
request_id,
serde_json::to_value(CommandExecutionRequestApprovalResponse { decision })?,
@@ -545,6 +613,8 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2()
.await?;
}
assert_eq!(approval_ids.len(), 2);
assert_ne!(approval_ids[0], approval_ids[1]);
let parent_completed_command_execution = timeout(DEFAULT_READ_TIMEOUT, async {
loop {
let completed_notif = mcp
@@ -563,32 +633,61 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2()
}
}
})
.await??;
.await;
let ThreadItem::CommandExecution {
id,
status,
aggregated_output,
..
} = parent_completed_command_execution
else {
unreachable!("loop ensures we break on parent command execution item");
};
assert_eq!(id, "call-zsh-fork-subcommand-decline");
assert_eq!(status, CommandExecutionStatus::Declined);
assert!(
aggregated_output.is_none()
|| aggregated_output == Some("exec command rejected by user".to_string())
);
assert_eq!(approval_ids.len(), 2);
assert_ne!(approval_ids[0], approval_ids[1]);
match parent_completed_command_execution {
Ok(Ok(parent_completed_command_execution)) => {
let ThreadItem::CommandExecution {
id,
status,
aggregated_output,
..
} = parent_completed_command_execution
else {
unreachable!("loop ensures we break on parent command execution item");
};
assert_eq!(id, "call-zsh-fork-subcommand-decline");
assert_eq!(status, CommandExecutionStatus::Declined);
assert!(
aggregated_output.is_none()
|| aggregated_output == Some("exec command rejected by user".to_string())
);
mcp.interrupt_turn_and_wait_for_aborted(thread.id, turn.id, DEFAULT_READ_TIMEOUT)
.await?;
mcp.interrupt_turn_and_wait_for_aborted(
thread.id.clone(),
turn.id.clone(),
DEFAULT_READ_TIMEOUT,
)
.await?;
}
Ok(Err(error)) => return Err(error),
Err(_) => {
// Some zsh builds abort the turn immediately after the rejected
// subcommand without emitting a parent `item/completed`.
let completed_notif = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??;
let completed: TurnCompletedNotification = serde_json::from_value(
completed_notif
.params
.expect("turn/completed params must be present"),
)?;
assert_eq!(completed.thread_id, thread.id);
assert_eq!(completed.turn.id, turn.id);
assert_eq!(completed.turn.status, TurnStatus::Interrupted);
}
}
Ok(())
}
async fn create_zsh_test_mcp_process(codex_home: &Path, zdotdir: &Path) -> Result<McpProcess> {
let zdotdir = zdotdir.to_string_lossy().into_owned();
McpProcess::new_with_env(codex_home, &[("ZDOTDIR", Some(zdotdir.as_str()))]).await
}
fn create_config_toml(
codex_home: &Path,
server_uri: &str,
@@ -640,36 +739,24 @@ stream_max_retries = 0
)
}
fn find_test_zsh_path() -> Option<std::path::PathBuf> {
if let Some(path) = std::env::var_os("CODEX_TEST_ZSH_PATH") {
let path = std::path::PathBuf::from(path);
if path.is_file() {
return Some(path);
}
panic!(
"CODEX_TEST_ZSH_PATH is set but is not a file: {}",
path.display()
fn find_test_zsh_path() -> Result<Option<std::path::PathBuf>> {
let repo_root = codex_utils_cargo_bin::repo_root()?;
let dotslash_zsh = repo_root.join("codex-rs/exec-server/tests/suite/zsh");
if !dotslash_zsh.is_file() {
eprintln!(
"skipping zsh fork test: shared zsh DotSlash file not found at {}",
dotslash_zsh.display()
);
return Ok(None);
}
for candidate in ["/bin/zsh", "/usr/bin/zsh"] {
let path = Path::new(candidate);
if path.is_file() {
return Some(path.to_path_buf());
match core_test_support::fetch_dotslash_file(&dotslash_zsh, None) {
Ok(path) => return Ok(Some(path)),
Err(error) => {
eprintln!("failed to fetch vendored zsh via dotslash: {error:#}");
}
}
let shell = std::env::var_os("SHELL")?;
let shell_path = std::path::PathBuf::from(shell);
if shell_path
.file_name()
.is_some_and(|file_name| file_name == "zsh")
&& shell_path.is_file()
{
return Some(shell_path);
}
None
Ok(None)
}
fn supports_exec_wrapper_intercept(zsh_path: &Path) -> bool {

View File

@@ -25,6 +25,15 @@ use crate::invocation::ExtractHeredocError;
/// Detailed instructions for gpt-4.1 on how to use the `apply_patch` tool.
pub const APPLY_PATCH_TOOL_INSTRUCTIONS: &str = include_str!("../apply_patch_tool_instructions.md");
/// Special argv[1] flag used when the Codex executable self-invokes to run the
/// internal `apply_patch` path.
///
/// Although this constant lives in `codex-apply-patch` (to avoid forcing
/// `codex-arg0` to depend on `codex-core`), it is part of the "codex core"
/// process-invocation contract between the apply-patch runtime and the arg0
/// dispatcher.
pub const CODEX_CORE_APPLY_PATCH_ARG1: &str = "--codex-run-as-apply-patch";
#[derive(Debug, Error, PartialEq)]
pub enum ApplyPatchError {
#[error(transparent)]

View File

@@ -14,8 +14,8 @@ workspace = true
[dependencies]
anyhow = { workspace = true }
codex-apply-patch = { workspace = true }
codex-core = { workspace = true }
codex-linux-sandbox = { workspace = true }
codex-utils-home-dir = { workspace = true }
dotenvy = { workspace = true }
tempfile = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread"] }

View File

@@ -3,7 +3,8 @@ use std::future::Future;
use std::path::Path;
use std::path::PathBuf;
use codex_core::CODEX_APPLY_PATCH_ARG1;
use codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1;
use codex_utils_home_dir::find_codex_home;
#[cfg(unix)]
use std::os::unix::fs::symlink;
use tempfile::TempDir;
@@ -46,7 +47,7 @@ pub fn arg0_dispatch() -> Option<Arg0PathEntryGuard> {
}
let argv1 = args.next().unwrap_or_default();
if argv1 == CODEX_APPLY_PATCH_ARG1 {
if argv1 == CODEX_CORE_APPLY_PATCH_ARG1 {
let patch_arg = args.next().and_then(|s| s.to_str().map(str::to_owned));
let exit_code = match patch_arg {
Some(patch_arg) => {
@@ -58,7 +59,7 @@ pub fn arg0_dispatch() -> Option<Arg0PathEntryGuard> {
}
}
None => {
eprintln!("Error: {CODEX_APPLY_PATCH_ARG1} requires a UTF-8 PATCH argument.");
eprintln!("Error: {CODEX_CORE_APPLY_PATCH_ARG1} requires a UTF-8 PATCH argument.");
1
}
};
@@ -139,7 +140,7 @@ const ILLEGAL_ENV_VAR_PREFIX: &str = "CODEX_";
/// Security: Do not allow `.env` files to create or modify any variables
/// with names starting with `CODEX_`.
fn load_dotenv() {
if let Ok(codex_home) = codex_core::config::find_codex_home()
if let Ok(codex_home) = find_codex_home()
&& let Ok(iter) = dotenvy::from_path_iter(codex_home.join(".env"))
{
set_filtered(iter);
@@ -175,7 +176,7 @@ where
/// IMPORTANT: This function modifies the PATH environment variable, so it MUST
/// be called before multiple threads are spawned.
pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result<Arg0PathEntryGuard> {
let codex_home = codex_core::config::find_codex_home()?;
let codex_home = find_codex_home()?;
#[cfg(not(debug_assertions))]
{
// Guard against placing helpers in system temp directories outside debug builds.
@@ -242,7 +243,7 @@ pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result<Arg0PathEntryGu
&batch_script,
format!(
r#"@echo off
"{}" {CODEX_APPLY_PATCH_ARG1} %*
"{}" {CODEX_CORE_APPLY_PATCH_ARG1} %*
"#,
exe.display()
),

View File

@@ -26,6 +26,7 @@ codex-arg0 = { workspace = true }
codex-chatgpt = { workspace = true }
codex-cloud-tasks = { path = "../cloud-tasks" }
codex-utils-cli = { workspace = true }
codex-config = { workspace = true }
codex-core = { workspace = true }
codex-exec = { workspace = true }
codex-execpolicy = { workspace = true }

View File

@@ -390,7 +390,7 @@ fn format_exit_messages(exit_info: AppExitInfo, color_enabled: bool) -> Vec<Stri
let mut lines = vec![format!(
"{}",
codex_core::protocol::FinalOutput::from(token_usage)
codex_protocol::protocol::FinalOutput::from(token_usage)
)];
if let Some(resume_cmd) =
@@ -543,10 +543,12 @@ fn stage_str(stage: codex_core::features::Stage) -> &'static str {
}
fn main() -> anyhow::Result<()> {
if codex_core::maybe_run_zsh_exec_wrapper_mode()? {
return Ok(());
}
arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move {
// Run wrapper mode only after arg0 dispatch so `codex-linux-sandbox`
// invocations don't get misclassified as zsh exec-wrapper calls.
if codex_core::maybe_run_zsh_exec_wrapper_mode()? {
return Ok(());
}
cli_main(codex_linux_sandbox_exe).await?;
Ok(())
})
@@ -870,7 +872,7 @@ fn maybe_print_under_development_feature_warning(
return;
}
let config_path = codex_home.join(codex_core::config::CONFIG_TOML_FILE);
let config_path = codex_home.join(codex_config::CONFIG_TOML_FILE);
eprintln!(
"Under-development features enabled: {feature}. Under-development features are incomplete and may behave unpredictably. To suppress this warning, set `suppress_unstable_features_warning = true` in {}.",
config_path.display()
@@ -1037,8 +1039,8 @@ fn print_completion(cmd: CompletionCommand) {
mod tests {
use super::*;
use assert_matches::assert_matches;
use codex_core::protocol::TokenUsage;
use codex_protocol::ThreadId;
use codex_protocol::protocol::TokenUsage;
use pretty_assertions::assert_eq;
fn finalize_resume_from_args(args: &[&str]) -> TuiCli {

View File

@@ -14,7 +14,7 @@ use codex_core::config::types::McpServerTransportConfig;
use codex_core::mcp::auth::McpOAuthLoginSupport;
use codex_core::mcp::auth::compute_auth_statuses;
use codex_core::mcp::auth::oauth_login_support;
use codex_core::protocol::McpAuthStatus;
use codex_protocol::protocol::McpAuthStatus;
use codex_rmcp_client::delete_oauth_tokens;
use codex_rmcp_client::perform_oauth_login;
use codex_utils_cli::CliConfigOverrides;

View File

Some files were not shown because too many files have changed in this diff Show More