Compare commits

..

25 Commits

Author SHA1 Message Date
starr-openai
077a3970d7 Use Dev Drive for Windows CI
Configure Windows Rust CI jobs and the shared Bazel CI setup to put temp, repository-cache, and output-root paths on the runner's fast work drive when available. Fall back to C: if no secondary drive or Dev Drive provisioning path is available.

Co-authored-by: Codex <noreply@openai.com>
2026-05-07 15:20:40 -07:00
starr-openai
5815dd6a4b Give Windows arm64 tests enough CI time
Let the Windows arm64 test matrix use a longer timeout after CI showed the lane spending most of the default 45 minutes compiling before nextest could finish.

Also pin nextest through taiki-e/install-action's supported tool version syntax so the requested version is not ignored.

Co-authored-by: Codex <noreply@openai.com>
2026-05-07 15:20:39 -07:00
starr-openai
296fa6df0c Serialize Windows process-heavy nextest cases
Windows rust-ci-full repeatedly times out in subprocess-heavy tests even when the global nextest thread count is capped. Isolate the recurring Windows-only families with nextest overrides so the rest of the suite can keep normal parallelism.

Co-authored-by: Codex <noreply@openai.com>
2026-05-07 15:20:39 -07:00
starr-openai
64c684bd57 Add Windows nextest thread override for rust-ci-full
Co-authored-by: Codex <noreply@openai.com>
2026-05-07 15:20:39 -07:00
starr-openai
ce5d84e43a Make pending sideband close test deterministic
Replace the realtime websocket accept-delay race with an explicit test-server gate so close is issued while the sideband connection is pending, then prove the closed conversation does not emit stale events or send sideband websocket requests.

Co-authored-by: Codex <noreply@openai.com>
2026-05-07 15:20:35 -07:00
starr-openai
926b8d77cd Tolerate transient Windows metadata denial in memory startup test
Keep polling when Windows temporarily denies metadata reads while the phase 2 memory workspace is being cleaned up, so the test still verifies the file is removed and the baseline becomes clean.

Co-authored-by: Codex <noreply@openai.com>
2026-05-07 14:48:09 -07:00
starr-openai
7cd5127421 Wait for agent shutdown before resume tests reopen IDs
Subscribe before test shutdown and close operations, then wait for the Shutdown status before resuming the same thread IDs. This removes the Windows live-writer race exposed by the full nextest run.

Co-authored-by: Codex <noreply@openai.com>
2026-05-07 14:48:09 -07:00
starr-openai
6a2ce743f1 Make Windows realtime shell test use successful cmd echo
Use a Windows command form that exits successfully in constrained CI shells and trim the expected newline in the delegated realtime shell-tool assertion.

Co-authored-by: Codex <noreply@openai.com>
2026-05-07 14:48:08 -07:00
starr-openai
32deb67fc6 Harden Windows realtime and agent resume tests
Avoid PowerShell command forms that depend on method invocation for the delegated realtime shell-tool test, and wait for a shutdown status before resuming the same subagent thread in the nickname/role restore test.

Co-authored-by: Codex <noreply@openai.com>
2026-05-07 14:48:08 -07:00
starr-openai
59d9e96d66 Use PowerShell literal output in sandbox tests
The legacy sandbox runs PowerShell in constrained language mode, so method calls fail and module-backed cmdlets may not autoload. Use literal string expressions for the PowerShell I/O smoke tests so they exercise process output without depending on cmdlets or method invocation.

Co-authored-by: Codex <noreply@openai.com>
2026-05-07 14:48:08 -07:00
starr-openai
097e3ef949 Avoid PowerShell module autoload in sandbox tests
Windows arm64 can launch pwsh in the legacy sandbox while still failing Write-Output because Microsoft.PowerShell.Utility cannot autoload. Use Console output in the legacy PowerShell smoke tests so they continue to verify sandbox process I/O without depending on module autoload.

Co-authored-by: Codex <noreply@openai.com>
2026-05-07 14:48:07 -07:00
starr-openai
f3afa1132d Fix rollout cwd fixture import
Import the Windows-aware test_path_buf helper from core_test_support where it is defined.

Co-authored-by: Codex <noreply@openai.com>
2026-05-07 14:48:07 -07:00
starr-openai
a666109389 Make rollout cwd fixtures drive-stable on Windows
Dev Drive setup can put temporary Codex homes on D:, which exposed test fixtures that wrote root-relative '/' rollout cwd values while assertions expected the Windows-aware C:\ root helper. Use the same test_path_buf helper when creating and expecting fake rollout cwd values so the tests remain independent of the process temp drive.

Co-authored-by: Codex <noreply@openai.com>
2026-05-07 14:48:07 -07:00
starr-openai
16648c8d1c Make realtime sideband failure test deterministic
Use the existing mock server as the sideband failure endpoint instead of relying on an OS-level connection refusal from 127.0.0.1:1. Disable retries in this failure-path test so Windows CI does not spend the default retry budget before emitting the expected error/close events.

Co-authored-by: Codex <noreply@openai.com>
2026-05-07 14:48:06 -07:00
starr-openai
7d2c8dbec4 Fix agent job worker assignment race
Claim job items before spawning workers and allow reports to complete unassigned running items, so fast workers cannot lose stop=true reports before the parent records their thread id.

Co-authored-by: Codex <noreply@openai.com>
2026-05-07 14:48:06 -07:00
starr-openai
bfe33e5a7a Make agent job stop cancellation atomic
A worker stop request used to record the item result and job cancellation in separate updates, so the job runner could observe the item completion first and continue spawning pending work. Commit both state updates together and prevent completion from overwriting a final cancellation.

Co-authored-by: Codex <noreply@openai.com>
2026-05-07 14:48:05 -07:00
William Woodruff
8abcc5357d [codex] Fully qualify hash-pins in GitHub Actions (#21436)
This builds on top of https://github.com/openai/codex/pull/15828 by
ensuring that hash-pinned actions with version comments are fully
qualified, rather than referencing floating/mutable comments like "v7".
This makes actions management tools behave more consistently.

This shouldn't break anything, since it's comment only. But if it does,
ping ww@ 🙂
2026-05-07 14:31:20 -07:00
Zanie Blue
27ec488ad5 Add a Cargo build profile for benchmarking (#21574)
A clean release build takes ~18m and an incremental build takes ~12m.
This is far too slow to iterate on performance related changes and the
build time is dominated by LTO.

This pull request adds a `profiling` profile for Cargo which takes ~13m
clean and ~6m incremental, the primary change is that LTO is disabled.
This matches a profile used in uv and follows the great work at
https://github.com/astral-sh/uv/pull/5955 — there's a bit of commentary
there about the trade-offs this implies.

We've found that this does not inhibit the ability to accurately
benchmark as measurements with LTO disabled are generally consistent
with the results with LTO enabled and it makes it much faster (~2x) to
rebuild after making a change.

This is motivated by my interest in improving Codex TUI performance,
which is blocked by the tragically builds right now.

I tested incremental build times by making a no-op change to the
`codex-cli` crate.
2026-05-07 14:30:35 -07:00
Zanie Blue
8367ef4522 Use descriptive names for Cargo profile options (#21582)
These are equivalent and their intent is clearer, e.g., I was confused
if `debug = 1` meant the same thing as `debug = true` (it does not).
2026-05-07 14:19:32 -07:00
iceweasel-oai
163eac9306 Grant sandbox users access to desktop runtime bin (#21564)
## Why

Codex desktop copies bundled Windows binaries out of `WindowsApps` into
a LocalAppData runtime cache before launching `codex.exe`. Sandboxed
commands can then need to execute helpers from that cache, but the
sandbox user group may not have read/execute access to the runtime bin
directory.

This makes the Windows sandbox refresh path repair that access directly
so the packaged desktop runtime remains usable from sandboxed sessions.

## What changed

- Added `setup_runtime_bin` to locate `%LOCALAPPDATA%\OpenAI\Codex\bin`,
matching the desktop bundled-binaries destination path, with the same
`USERPROFILE\AppData\Local` fallback shape.
- During refresh setup, check whether `CodexSandboxUsers` already has
read/execute access to the runtime bin directory.
- If access is missing, grant `CodexSandboxUsers` `OI/CI/RX` inheritance
on that directory.
- If the runtime bin directory does not exist, no-op cleanly.

## Verification

- `cargo build -p codex-windows-sandbox --bin
codex-windows-sandbox-setup`
- `cargo test -p codex-windows-sandbox --bin
codex-windows-sandbox-setup`
- Manual Windows ACL exercise against the installed packaged runtime
bin:
- existing inherited `CodexSandboxUsers:(I)(OI)(CI)(RX)` no-ops without
changing SDDL
- after disabling inheritance and removing the group ACE, setup adds
`CodexSandboxUsers:(OI)(CI)(RX)`
- with `LOCALAPPDATA` pointed at a fake location without
`OpenAI\Codex\bin`, setup exits successfully and does not create the
directory
- restored the real runtime bin with inherited ACLs and confirmed the
final SDDL matched the baseline exactly
2026-05-07 11:38:10 -07:00
Tom
4242bba2eb Route ThreadManager rollout path reads through thread store (#21265)
- Route ThreadManager rollout-path resume/fork through ThreadStore
history reads.
- Add in-memory store coverage proving path-addressed reads are used.

This isn't strictly necessary for the ThreadStore migration, since these
ThreadManager methods _only_ work for path-based lookups, but I'm trying
to migrate all the rollout recorder callsites to use the threadstore
were possible for consistency.
2026-05-07 11:25:25 -07:00
Tom
0274398901 [codex] Fix pathless thread summaries (#21266)
## Summary

Fix `getConversationSummary` so thread-id summaries work for stored
threads that do not have a local rollout path, such as remote thread
stores.

The root cause was that `summary_from_stored_thread` returned `None`
when `StoredThread.rollout_path` was absent, and
`get_thread_summary_response_inner` treated that as an internal error.
This made conversation-id lookups depend on a local-only field even
though the thread store can address the thread by id.
2026-05-07 11:18:16 -07:00
Tom
56823ec46b Move thread name edits to ThreadStore (#21264)
- Route live thread renames through `ThreadStore` metadata updates.
- Read resumed thread names from store metadata with legacy local
fallback preserved in the store.
2026-05-07 11:12:22 -07:00
Charlie Marsh
0dc1885a5c Upgrade cargo-shear to 1.11.2 (#21547)
## Summary

Catches a few additional dependencies (`sha2`, `url`) that should be in
`dev-dependencies`.
2026-05-07 11:07:18 -07:00
pakrym-oai
566f2cb612 [codex] Move tool specs onto handlers (#21461)
## Why

This is the next stacked step after deleting the tool-handler kind
indirection. Specs should come from the registered handlers themselves
so registry construction has a single source of truth for handler
behavior and exposed tool definitions.

## What changed

- Added `ToolHandler::spec()` plus handler-provided parallel/code-mode
metadata, and made `ToolRegistryBuilder::register_handler` automatically
collect specs from registered handlers.
- Moved builtin tool spec construction into the corresponding handlers
and their adjacent `_spec` modules, including shell, unified exec, apply
patch, view image, request plugin install, tool search, MCP resource,
goals, planning, permissions, agent jobs, and multi-agent tools.
- Reworked configurable handlers to receive their tool-building options
through constructors, with non-optional handler options where the
handler is always spec-backed. Shell fallback handlers keep an explicit
no-spec mode because they are also registered as hidden dispatch
aliases.
- Kept `CodeModeExecuteHandler` on the explicit configured wrapper so
the code-mode exec spec can still be built from the nested registry.

## Verification

- `cargo check -p codex-core`
- `cargo test -p codex-core tools::spec_plan::tests`
- `cargo test -p codex-core tools::spec::tests`
- `cargo test -p codex-core tools::handlers::multi_agents_spec::tests`
- `RUST_MIN_STACK=16777216 cargo test -p codex-core
tools::handlers::multi_agents::tests`
- `cargo test -p codex-core tools::handlers::apply_patch::tests`
- `cargo test -p codex-core tools::handlers::unified_exec::tests`
- `just fix -p codex-core`
- `git diff --check`
2026-05-07 10:48:36 -07:00
104 changed files with 1988 additions and 685 deletions

View File

@@ -50,7 +50,7 @@ runs:
- name: Restore bazel repository cache
id: cache_bazel_repository_restore
continue-on-error: true
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ${{ steps.setup_bazel.outputs.repository-cache-path }}
key: ${{ steps.cache_bazel_repository_key.outputs.repository-cache-key }}

View File

@@ -35,6 +35,11 @@ runs:
- name: Set up Bazel
uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0
- name: Configure Dev Drive (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: ./.github/scripts/setup-dev-drive.ps1
- name: Configure Bazel repository cache
id: configure_bazel_repository_cache
shell: pwsh
@@ -42,7 +47,12 @@ runs:
# Keep the repository cache under HOME on all runners. Windows `D:\a`
# cache paths match `.bazelrc`, but `actions/cache/restore` currently
# returns HTTP 400 for that path in the Windows clippy job.
$repositoryCachePath = Join-Path $HOME '.cache/bazel-repo-cache'
$cacheRoot = if ($env:RUNNER_OS -eq 'Windows' -and $env:DEV_DRIVE) {
$env:DEV_DRIVE
} else {
$HOME
}
$repositoryCachePath = Join-Path $cacheRoot '.cache/bazel-repo-cache'
"repository-cache-path=$repositoryCachePath" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
"BAZEL_REPOSITORY_CACHE=$repositoryCachePath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
@@ -50,11 +60,10 @@ runs:
if: runner.os == 'Windows'
shell: pwsh
run: |
# Use the shortest available drive to reduce argv/path length issues,
# but avoid the drive root because some Windows test launchers mis-handle
# MANIFEST paths there.
$hasDDrive = Test-Path 'D:\'
$bazelOutputUserRoot = if ($hasDDrive) { 'D:\b' } else { 'C:\b' }
# Keep Bazel on the fast Windows work drive, but avoid the drive root
# because some Windows test launchers mis-handle MANIFEST paths there.
$driveRoot = if ($env:DEV_DRIVE) { $env:DEV_DRIVE } elseif (Test-Path 'D:\') { 'D:' } else { 'C:' }
$bazelOutputUserRoot = Join-Path $driveRoot 'b'
$repoContentsCache = Join-Path $env:RUNNER_TEMP "bazel-repo-contents-cache-$env:GITHUB_RUN_ID-$env:GITHUB_JOB"
"BAZEL_OUTPUT_USER_ROOT=$bazelOutputUserRoot" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
"BAZEL_REPO_CONTENTS_CACHE=$repoContentsCache" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append

View File

@@ -30,7 +30,7 @@ runs:
using: composite
steps:
- name: Azure login for Trusted Signing (OIDC)
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ inputs.client-id }}
tenant-id: ${{ inputs.tenant-id }}
@@ -54,7 +54,7 @@ runs:
} >> "$GITHUB_OUTPUT"
- name: Sign Windows binaries with Azure Trusted Signing
uses: azure/trusted-signing-action@1d365fec12862c4aa68fcac418143d73f0cea293 # v0
uses: azure/trusted-signing-action@1d365fec12862c4aa68fcac418143d73f0cea293 # v0.5.11
with:
endpoint: ${{ inputs.endpoint }}
trusted-signing-account-name: ${{ inputs.account-name }}

62
.github/scripts/setup-dev-drive.ps1 vendored Normal file
View File

@@ -0,0 +1,62 @@
# Configure a fast drive for Windows CI jobs.
#
# GitHub-hosted Windows runners do not always expose a secondary D: volume. When
# they do not, try to create a Dev Drive VHD and fall back to C: if the runner
# image does not allow that provisioning path.
function Use-FallbackDrive {
param([string]$Reason)
Write-Warning "$Reason Falling back to C:"
return "C:"
}
function Invoke-BestEffort {
param([scriptblock]$Script, [string]$Description)
try {
& $Script
} catch {
Write-Warning "$Description failed: $($_.Exception.Message)"
}
}
if (Test-Path "D:\") {
Write-Output "Using existing drive at D:"
$Drive = "D:"
} else {
try {
$VhdPath = Join-Path $env:RUNNER_TEMP "codex-dev-drive.vhdx"
$SizeBytes = 64GB
if (Test-Path $VhdPath) {
Remove-Item -Path $VhdPath -Force
}
New-VHD -Path $VhdPath -SizeBytes $SizeBytes -Dynamic -ErrorAction Stop | Out-Null
$Mounted = Mount-VHD -Path $VhdPath -Passthru -ErrorAction Stop
$Disk = $Mounted | Get-Disk -ErrorAction Stop
$Disk | Initialize-Disk -PartitionStyle GPT -ErrorAction Stop
$Partition = $Disk | New-Partition -AssignDriveLetter -UseMaximumSize -ErrorAction Stop
$Volume = $Partition | Format-Volume -FileSystem ReFS -NewFileSystemLabel "CodexDevDrive" -DevDrive -Confirm:$false -Force -ErrorAction Stop
$Drive = "$($Volume.DriveLetter):"
Invoke-BestEffort { fsutil devdrv trust $Drive } "Trusting Dev Drive $Drive"
Invoke-BestEffort { fsutil devdrv enable /disallowAv } "Disabling AV filter attachment for Dev Drives"
Invoke-BestEffort { fsutil devdrv query $Drive } "Querying Dev Drive $Drive"
Write-Output "Using Dev Drive at $Drive"
} catch {
$Drive = Use-FallbackDrive "Failed to create Dev Drive: $($_.Exception.Message)"
}
}
$Tmp = "$Drive\codex-tmp"
New-Item -Path $Tmp -ItemType Directory -Force | Out-Null
@(
"DEV_DRIVE=$Drive"
"TMP=$Tmp"
"TEMP=$Tmp"
) | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append

View File

@@ -56,7 +56,7 @@ jobs:
name: Bazel test on ${{ matrix.os }} for ${{ matrix.target }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check rusty_v8 MODULE.bazel checksums
if: matrix.os == 'ubuntu-24.04' && matrix.target == 'x86_64-unknown-linux-gnu'
@@ -122,7 +122,7 @@ jobs:
- name: Upload Bazel execution logs
if: always() && !cancelled()
continue-on-error: true
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: bazel-execution-logs-test-${{ matrix.target }}
path: ${{ runner.temp }}/bazel-execution-logs
@@ -133,7 +133,7 @@ jobs:
- name: Save bazel repository cache
if: always() && !cancelled() && steps.prepare_bazel.outputs.repository-cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ${{ steps.prepare_bazel.outputs.repository-cache-path }}
key: ${{ steps.prepare_bazel.outputs.repository-cache-key }}
@@ -148,7 +148,7 @@ jobs:
name: Bazel test on windows-latest for x86_64-pc-windows-gnullvm (native main)
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Prepare Bazel CI
id: prepare_bazel
@@ -195,7 +195,7 @@ jobs:
- name: Upload Bazel execution logs
if: always() && !cancelled()
continue-on-error: true
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: bazel-execution-logs-test-windows-native-x86_64-pc-windows-gnullvm
path: ${{ runner.temp }}/bazel-execution-logs
@@ -206,7 +206,7 @@ jobs:
- name: Save bazel repository cache
if: always() && !cancelled() && steps.prepare_bazel.outputs.repository-cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ${{ steps.prepare_bazel.outputs.repository-cache-path }}
key: ${{ steps.prepare_bazel.outputs.repository-cache-key }}
@@ -231,7 +231,7 @@ jobs:
name: Bazel clippy on ${{ matrix.os }} for ${{ matrix.target }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Prepare Bazel CI
id: prepare_bazel
@@ -286,7 +286,7 @@ jobs:
- name: Upload Bazel execution logs
if: always() && !cancelled()
continue-on-error: true
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: bazel-execution-logs-clippy-${{ matrix.target }}
path: ${{ runner.temp }}/bazel-execution-logs
@@ -297,7 +297,7 @@ jobs:
- name: Save bazel repository cache
if: always() && !cancelled() && steps.prepare_bazel.outputs.repository-cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ${{ steps.prepare_bazel.outputs.repository-cache-path }}
key: ${{ steps.prepare_bazel.outputs.repository-cache-key }}
@@ -318,7 +318,7 @@ jobs:
name: Verify release build on ${{ matrix.os }} for ${{ matrix.target }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Prepare Bazel CI
id: prepare_bazel
@@ -390,7 +390,7 @@ jobs:
- name: Upload Bazel execution logs
if: always() && !cancelled()
continue-on-error: true
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: bazel-execution-logs-verify-release-build-${{ matrix.target }}
path: ${{ runner.temp }}/bazel-execution-logs
@@ -401,7 +401,7 @@ jobs:
- name: Save bazel repository cache
if: always() && !cancelled() && steps.prepare_bazel.outputs.repository-cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ${{ steps.prepare_bazel.outputs.repository-cache-path }}
key: ${{ steps.prepare_bazel.outputs.repository-cache-key }}

View File

@@ -8,7 +8,7 @@ jobs:
name: Blob size policy
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0

View File

@@ -14,7 +14,7 @@ jobs:
working-directory: ./codex-rs
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0

View File

@@ -12,7 +12,7 @@ jobs:
NODE_OPTIONS: --max-old-space-size=4096
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Verify codex-rs Cargo manifests inherit workspace settings
run: python3 .github/scripts/verify_cargo_workspace_manifests.py
@@ -29,7 +29,7 @@ jobs:
run_install: false
- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
@@ -63,7 +63,7 @@ jobs:
echo "pack_output=$PACK_OUTPUT" >> "$GITHUB_OUTPUT"
- name: Upload staged npm package artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: codex-npm-staging
path: ${{ steps.stage_npm_package.outputs.pack_output }}

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Close inactive PRs from contributors
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |

View File

@@ -18,9 +18,9 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Annotate locations with typos
uses: codespell-project/codespell-problem-matcher@b80729f885d32f78a716c2f107b4db1025001c42 # v1
uses: codespell-project/codespell-problem-matcher@b80729f885d32f78a716c2f107b4db1025001c42 # v1.1.0
- name: Codespell
uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2
with:

View File

@@ -19,7 +19,7 @@ jobs:
reason: ${{ steps.normalize-all.outputs.reason }}
has_matches: ${{ steps.normalize-all.outputs.has_matches }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Prepare Codex inputs
env:
@@ -155,7 +155,7 @@ jobs:
reason: ${{ steps.normalize-open.outputs.reason }}
has_matches: ${{ steps.normalize-open.outputs.has_matches }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Prepare Codex inputs
env:
@@ -342,7 +342,7 @@ jobs:
issues: write
steps:
- name: Comment on issue
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
CODEX_OUTPUT: ${{ needs.select-final.outputs.codex_output }}
with:

View File

@@ -17,7 +17,7 @@ jobs:
outputs:
codex_output: ${{ steps.codex.outputs.final-message }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- id: codex
uses: openai/codex-action@5c3f4ccdb2b8790f73d6b21751ac00e602aa0c02 # v1.7

View File

@@ -1,10 +1,21 @@
name: rust-ci-full
run-name: >-
rust-ci-full${{
github.event_name == 'workflow_dispatch' &&
format(' windows-nextest-{0}', inputs.windows_nextest_threads) ||
''
}}
on:
push:
branches:
- main
- "**full-ci**"
workflow_dispatch:
inputs:
windows_nextest_threads:
description: "Optional nextest --test-threads override for Windows test jobs"
required: false
type: string
# CI builds in debug (dev) for faster signal.
@@ -17,7 +28,7 @@ jobs:
run:
working-directory: codex-rs
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
with:
components: rustfmt
@@ -31,12 +42,12 @@ jobs:
run:
working-directory: codex-rs
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: cargo-shear
version: 1.5.1
version: 1.11.2
- name: cargo shear
run: cargo shear
@@ -47,14 +58,14 @@ jobs:
CARGO_DYLINT_VERSION: 5.0.0
DYLINT_LINK_VERSION: 5.0.0
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
with:
toolchain: nightly-2025-09-18
components: llvm-tools-preview, rustc-dev, rust-src
- name: Cache cargo-dylint tooling
id: cargo_dylint_cache
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.cargo/bin/cargo-dylint
@@ -97,7 +108,7 @@ jobs:
group: codex-runners
labels: codex-windows-x64
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/setup-bazel-ci
with:
target: ${{ runner.os }}
@@ -233,7 +244,11 @@ jobs:
labels: codex-windows-arm64
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Configure Dev Drive (Windows)
if: ${{ runner.os == 'Windows' }}
shell: pwsh
run: ../.github/scripts/setup-dev-drive.ps1
- name: Install Linux build dependencies
if: ${{ runner.os == 'Linux' }}
shell: bash
@@ -276,7 +291,7 @@ jobs:
# avoid caching the large target dir on the gnu-dev job.
- name: Restore cargo home cache
id: cache_cargo_home_restore
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.cargo/bin/
@@ -294,7 +309,7 @@ jobs:
# Install and restore sccache cache
- name: Install sccache
if: ${{ env.USE_SCCACHE == 'true' }}
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: sccache
version: 0.7.5
@@ -321,7 +336,7 @@ jobs:
- name: Restore sccache cache (fallback)
if: ${{ env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true' }}
id: cache_sccache_restore
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ${{ github.workspace }}/.sccache/
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
@@ -348,7 +363,7 @@ jobs:
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Restore APT cache (musl)
id: cache_apt_restore
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
/var/cache/apt
@@ -356,7 +371,7 @@ jobs:
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Install Zig
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1
with:
version: 0.14.0
@@ -430,7 +445,7 @@ jobs:
- name: Install cargo-chef
if: ${{ matrix.profile == 'release' }}
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: cargo-chef
version: 0.1.71
@@ -449,7 +464,7 @@ jobs:
- name: Upload Cargo timings (clippy)
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: cargo-timings-rust-ci-clippy-${{ matrix.target }}-${{ matrix.profile }}
path: codex-rs/target/**/cargo-timings/cargo-timing.html
@@ -460,7 +475,7 @@ jobs:
- name: Save cargo home cache
if: always() && !cancelled() && steps.cache_cargo_home_restore.outputs.cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.cargo/bin/
@@ -476,7 +491,7 @@ jobs:
- name: Save sccache cache (fallback)
if: always() && !cancelled() && env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true'
continue-on-error: true
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ${{ github.workspace }}/.sccache/
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
@@ -501,7 +516,7 @@ jobs:
- name: Save APT cache (musl)
if: always() && !cancelled() && (matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl') && steps.cache_apt_restore.outputs.cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
/var/cache/apt
@@ -510,10 +525,10 @@ jobs:
tests:
name: Tests — ${{ matrix.runner }} - ${{ matrix.target }}${{ matrix.remote_env == 'true' && ' (remote)' || '' }}
runs-on: ${{ matrix.runs_on || matrix.runner }}
# Perhaps we can bring this back down to 30m once we finish the cutover
# from tui_app_server/ to tui/. Incidentally, windows-arm64 was the main
# offender for exceeding the timeout.
timeout-minutes: 45
# Perhaps we can bring this back down once we finish the cutover from
# tui_app_server/ to tui/. Incidentally, windows-arm64 was the main offender
# for exceeding the timeout.
timeout-minutes: ${{ matrix.timeout_minutes || 45 }}
defaults:
run:
working-directory: codex-rs
@@ -524,6 +539,7 @@ jobs:
USE_SCCACHE: ${{ (startsWith(matrix.runner, 'windows') || (matrix.runner == 'macos-15-xlarge' && matrix.target == 'x86_64-apple-darwin')) && 'false' || 'true' }}
CARGO_INCREMENTAL: "0"
SCCACHE_CACHE_SIZE: 10G
WINDOWS_NEXTEST_THREADS: ${{ github.event_name == 'workflow_dispatch' && inputs.windows_nextest_threads || '' }}
strategy:
fail-fast: false
@@ -554,12 +570,17 @@ jobs:
- runner: windows-arm64
target: aarch64-pc-windows-msvc
profile: dev
timeout_minutes: 75
runs_on:
group: codex-runners
labels: codex-windows-arm64
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Configure Dev Drive (Windows)
if: ${{ runner.os == 'Windows' }}
shell: pwsh
run: ../.github/scripts/setup-dev-drive.ps1
- name: Install Linux build dependencies
if: ${{ runner.os == 'Linux' }}
shell: bash
@@ -590,7 +611,7 @@ jobs:
- name: Restore cargo home cache
id: cache_cargo_home_restore
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.cargo/bin/
@@ -603,7 +624,7 @@ jobs:
- name: Install sccache
if: ${{ env.USE_SCCACHE == 'true' }}
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: sccache
version: 0.7.5
@@ -630,7 +651,7 @@ jobs:
- name: Restore sccache cache (fallback)
if: ${{ env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true' }}
id: cache_sccache_restore
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ${{ github.workspace }}/.sccache/
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
@@ -638,10 +659,9 @@ jobs:
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: nextest
version: 0.9.103
tool: nextest@0.9.103
- name: Enable unprivileged user namespaces (Linux)
if: runner.os == 'Linux'
@@ -666,7 +686,19 @@ jobs:
- name: tests
id: test
run: cargo nextest run --no-fail-fast --target ${{ matrix.target }} --cargo-profile ci-test --timings
shell: bash
run: |
set -euo pipefail
nextest_args=(
--no-fail-fast
--target "${{ matrix.target }}"
--cargo-profile ci-test
--timings
)
if [[ "${{ runner.os }}" == "Windows" && -n "${WINDOWS_NEXTEST_THREADS}" ]]; then
nextest_args+=(--test-threads "${WINDOWS_NEXTEST_THREADS}")
fi
cargo nextest run "${nextest_args[@]}"
env:
RUST_BACKTRACE: 1
RUST_MIN_STACK: "8388608" # 8 MiB
@@ -674,7 +706,7 @@ jobs:
- name: Upload Cargo timings (nextest)
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: cargo-timings-rust-ci-nextest-${{ matrix.target }}-${{ matrix.profile }}
path: codex-rs/target/**/cargo-timings/cargo-timing.html
@@ -683,7 +715,7 @@ jobs:
- name: Save cargo home cache
if: always() && !cancelled() && steps.cache_cargo_home_restore.outputs.cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.cargo/bin/
@@ -695,7 +727,7 @@ jobs:
- name: Save sccache cache (fallback)
if: always() && !cancelled() && env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true'
continue-on-error: true
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ${{ github.workspace }}/.sccache/
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}

View File

@@ -14,7 +14,7 @@ jobs:
codex: ${{ steps.detect.outputs.codex }}
workflows: ${{ steps.detect.outputs.workflows }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Detect changed paths (no external action)
@@ -61,7 +61,7 @@ jobs:
run:
working-directory: codex-rs
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
with:
components: rustfmt
@@ -77,12 +77,12 @@ jobs:
run:
working-directory: codex-rs
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: cargo-shear
version: 1.5.1
version: 1.11.2
- name: cargo shear
run: cargo shear
@@ -95,7 +95,7 @@ jobs:
CARGO_DYLINT_VERSION: 5.0.0
DYLINT_LINK_VERSION: 5.0.0
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
- name: Install nightly argument-comment-lint toolchain
shell: bash
@@ -109,7 +109,7 @@ jobs:
rustup default nightly-2025-09-18
- name: Cache cargo-dylint tooling
id: cargo_dylint_cache
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.cargo/bin/cargo-dylint
@@ -170,7 +170,7 @@ jobs:
echo "No argument-comment-lint relevant changes."
echo "run=false" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
if: ${{ steps.argument_comment_lint_gate.outputs.run == 'true' }}
- name: Run argument comment lint on codex-rs via Bazel
if: ${{ steps.argument_comment_lint_gate.outputs.run == 'true' }}

View File

@@ -56,7 +56,7 @@ jobs:
labels: codex-windows-x64
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
with:
@@ -100,7 +100,7 @@ jobs:
(cd "${RUNNER_TEMP}" && tar -czf "$GITHUB_WORKSPACE/$archive_path" argument-comment-lint)
fi
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: argument-comment-lint-${{ matrix.target }}
path: dist/argument-comment-lint/${{ matrix.target }}/*

View File

@@ -18,7 +18,7 @@ jobs:
if: github.repository == 'openai/codex'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: main
fetch-depth: 0
@@ -43,7 +43,7 @@ jobs:
curl --http1.1 --fail --show-error --location "${headers[@]}" "${url}" | jq '.' > codex-rs/models-manager/models.json
- name: Open pull request (if changed)
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
commit-message: "Update models.json"
title: "Update models.json"

View File

@@ -83,7 +83,7 @@ jobs:
labels: codex-windows-arm64
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Print runner specs (Windows)
shell: powershell
run: |
@@ -112,7 +112,7 @@ jobs:
cargo build --target ${{ matrix.target }} --release --timings "${build_args[@]}"
- name: Upload Cargo timings
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: cargo-timings-rust-release-windows-${{ matrix.target }}-${{ matrix.bundle }}
path: codex-rs/target/**/cargo-timings/cargo-timing.html
@@ -128,7 +128,7 @@ jobs:
done
- name: Upload Windows binaries
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: windows-binaries-${{ matrix.target }}-${{ matrix.bundle }}
path: |
@@ -165,22 +165,22 @@ jobs:
labels: codex-windows-arm64
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Download prebuilt Windows primary binaries
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: windows-binaries-${{ matrix.target }}-primary
path: codex-rs/target/${{ matrix.target }}/release
- name: Download prebuilt Windows helper binaries
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: windows-binaries-${{ matrix.target }}-helpers
path: codex-rs/target/${{ matrix.target }}/release
- name: Download prebuilt Windows app-server binary
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: windows-binaries-${{ matrix.target }}-app-server
path: codex-rs/target/${{ matrix.target }}/release
@@ -281,7 +281,7 @@ jobs:
"${GITHUB_WORKSPACE}/.github/workflows/zstd" -T0 -19 "$dest/$base"
done
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: ${{ matrix.target }}
path: |

View File

@@ -45,7 +45,7 @@ jobs:
git \
libncursesw5-dev
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Build, smoke-test, and stage zsh artifact
shell: bash
@@ -53,7 +53,7 @@ jobs:
"${GITHUB_WORKSPACE}/.github/scripts/build-zsh-release-artifact.sh" \
"dist/zsh/${{ matrix.target }}/${{ matrix.archive_name }}"
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: codex-zsh-${{ matrix.target }}
path: dist/zsh/${{ matrix.target }}/*
@@ -81,7 +81,7 @@ jobs:
brew install autoconf
fi
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Build, smoke-test, and stage zsh artifact
shell: bash
@@ -89,7 +89,7 @@ jobs:
"${GITHUB_WORKSPACE}/.github/scripts/build-zsh-release-artifact.sh" \
"dist/zsh/${{ matrix.target }}/${{ matrix.archive_name }}"
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: codex-zsh-${{ matrix.target }}
path: dist/zsh/${{ matrix.target }}/*

View File

@@ -19,7 +19,7 @@ jobs:
tag-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
- name: Validate tag matches Cargo.toml version
shell: bash
@@ -118,7 +118,7 @@ jobs:
build_dmg: "false"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Print runner specs (Linux)
if: ${{ runner.os == 'Linux' }}
shell: bash
@@ -181,7 +181,7 @@ jobs:
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Install Zig
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1
with:
version: 0.14.0
@@ -284,7 +284,7 @@ jobs:
cargo build --target ${{ matrix.target }} --release --timings "${build_args[@]}"
- name: Upload Cargo timings
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: cargo-timings-rust-release-${{ matrix.target }}-${{ matrix.bundle }}
path: codex-rs/target/**/cargo-timings/cargo-timing.html
@@ -430,7 +430,7 @@ jobs:
zstd -T0 -19 --rm "$dest/$base"
done
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: ${{ matrix.artifact_name }}
# Upload the per-binary .zst files, .tar.gz equivalents, and any
@@ -476,7 +476,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Generate release notes from tag commit message
id: release_notes
@@ -498,7 +498,7 @@ jobs:
echo "path=${notes_path}" >> "${GITHUB_OUTPUT}"
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: dist
@@ -553,7 +553,7 @@ jobs:
run_install: false
- name: Setup Node.js for npm packaging
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
@@ -579,7 +579,7 @@ jobs:
cp scripts/install/install.ps1 dist/install.ps1
- name: Create GitHub Release
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
with:
name: ${{ steps.release_name.outputs.name }}
tag_name: ${{ github.ref_name }}
@@ -638,7 +638,7 @@ jobs:
steps:
- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
# Node 24 bundles npm >= 11.5.1, which trusted publishing requires.
node-version: 24

View File

@@ -17,10 +17,10 @@ jobs:
v8_version: ${{ steps.v8_version.outputs.version }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
@@ -69,7 +69,7 @@ jobs:
target: aarch64-unknown-linux-musl
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Bazel
uses: ./.github/actions/setup-bazel-ci
@@ -77,7 +77,7 @@ jobs:
target: ${{ matrix.target }}
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
@@ -133,7 +133,7 @@ jobs:
--output-dir "dist/${TARGET}"
- name: Upload staged musl artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: rusty-v8-${{ needs.metadata.outputs.v8_version }}-${{ matrix.target }}
path: dist/${{ matrix.target }}/*
@@ -161,12 +161,12 @@ jobs:
exit 1
fi
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: dist
- name: Create GitHub Release
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
with:
tag_name: ${{ needs.metadata.outputs.release_tag }}
name: ${{ needs.metadata.outputs.release_tag }}

View File

@@ -13,7 +13,7 @@ jobs:
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Linux bwrap build dependencies
shell: bash
@@ -28,7 +28,7 @@ jobs:
run_install: false
- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: pnpm
@@ -115,7 +115,7 @@ jobs:
- name: Save bazel repository cache
if: always() && !cancelled() && steps.setup_bazel.outputs.cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.cache/bazel-repo-cache

View File

@@ -40,10 +40,10 @@ jobs:
v8_version: ${{ steps.v8_version.outputs.version }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
@@ -74,7 +74,7 @@ jobs:
target: aarch64-unknown-linux-musl
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Bazel
uses: ./.github/actions/setup-bazel-ci
@@ -82,7 +82,7 @@ jobs:
target: ${{ matrix.target }}
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
@@ -132,7 +132,7 @@ jobs:
--output-dir "dist/${TARGET}"
- name: Upload staged musl artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: v8-canary-${{ needs.metadata.outputs.v8_version }}-${{ matrix.target }}
path: dist/${{ matrix.target }}/*

View File

@@ -14,6 +14,9 @@ max-threads = 1
[test-groups.windows_sandbox_legacy_sessions]
max-threads = 1
[test-groups.windows_process_heavy]
max-threads = 1
[[profile.default.overrides]]
# Do not add new tests here
filter = 'test(rmcp_client) | test(humanlike_typing_1000_chars_appears_live_no_placeholder)'
@@ -27,6 +30,41 @@ slow-timeout = { period = "30s", terminate-after = 2 }
filter = 'package(codex-app-server-protocol) & (test(typescript_schema_fixtures_match_generated) | test(json_schema_fixtures_match_generated) | test(generate_ts_with_experimental_api_retains_experimental_entries) | test(generated_ts_optional_nullable_fields_only_in_params) | test(generate_json_filters_experimental_fields_and_methods))'
test-group = 'app_server_protocol_codegen'
[[profile.default.overrides]]
# These Windows CI tests launch full Codex/app-server process trees. They have
# repeatedly timed out when nextest schedules them alongside similar tests.
platform = 'cfg(windows)'
filter = 'package(codex-core) & kind(test) & (test(cli_stream) | test(realtime_conversation))'
test-group = 'windows_process_heavy'
threads-required = "num-test-threads"
slow-timeout = { period = "1m", terminate-after = 4 }
[[profile.default.overrides]]
# The exec resume tests spawn the CLI and touch shared session state on disk.
platform = 'cfg(windows)'
filter = 'package(codex-exec) & kind(test) & test(exec_resume)'
test-group = 'windows_process_heavy'
threads-required = "num-test-threads"
slow-timeout = { period = "1m", terminate-after = 4 }
[[profile.default.overrides]]
# Keep the specific app-server subprocess-heavy cases isolated on Windows. This
# must stay before the broader codex-app-server override below.
platform = 'cfg(windows)'
filter = 'package(codex-app-server) & kind(test) & (test(thread_fork_can_exclude_turns_and_skip_restored_token_usage) | test(turn_start_resolves_sticky_thread_environments_and_turn_overrides) | test(message_processor_tracing_tests))'
test-group = 'windows_process_heavy'
threads-required = "num-test-threads"
slow-timeout = { period = "1m", terminate-after = 4 }
[[profile.default.overrides]]
# These tests create restricted-token Windows child processes and private
# desktops. Running them alone avoids contention with other subprocess tests.
platform = 'cfg(windows)'
filter = 'package(codex-windows-sandbox) & kind(test) & test(legacy_)'
test-group = 'windows_process_heavy'
threads-required = "num-test-threads"
slow-timeout = { period = "1m", terminate-after = 4 }
[[profile.default.overrides]]
# These integration tests spawn a fresh app-server subprocess per case.
# Keep the library unit tests parallel.

View File

@@ -475,13 +475,13 @@ ignored = [
[profile.dev]
# Keep line tables/backtraces while avoiding expensive full variable debug info
# across local dev builds.
debug = 1
debug = "limited"
[profile.dev-small]
inherits = "dev"
opt-level = 0
debug = 0
strip = true
debug = "none"
strip = "symbols"
[profile.release]
lto = "fat"
@@ -493,8 +493,15 @@ strip = "symbols"
# See https://github.com/openai/codex/issues/1411 for details.
codegen-units = 1
[profile.profiling]
inherits = "release"
debug = "full"
lto = false
strip = false
[profile.ci-test]
debug = 1 # Reduce debug symbol size
# Reduce binary size to reduce disk pressure.
debug = "limited"
inherits = "test"
opt-level = 0

View File

@@ -71,7 +71,6 @@ clap = { workspace = true, features = ["derive"] }
futures = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sha2 = { workspace = true }
tempfile = { workspace = true }
thiserror = { workspace = true }
time = { workspace = true }
@@ -87,20 +86,19 @@ tokio = { workspace = true, features = [
tokio-util = { workspace = true }
tracing = { workspace = true, features = ["log"] }
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt", "json"] }
url = { workspace = true }
uuid = { workspace = true, features = ["serde", "v7"] }
[dev-dependencies]
app_test_support = { workspace = true }
base64 = { workspace = true }
axum = { workspace = true, default-features = false, features = [
"http1",
"json",
"tokio",
] }
core_test_support = { workspace = true }
base64 = { workspace = true }
codex-model-provider-info = { workspace = true }
codex-utils-cargo-bin = { workspace = true }
core_test_support = { workspace = true }
flate2 = { workspace = true }
hmac = { workspace = true }
opentelemetry = { workspace = true }
@@ -113,8 +111,10 @@ rmcp = { workspace = true, default-features = false, features = [
"transport-streamable-http-server",
] }
serial_test = { workspace = true }
sha2 = { workspace = true }
shlex = { workspace = true }
tar = { workspace = true }
tokio-tungstenite = { workspace = true }
tracing-opentelemetry = { workspace = true }
url = { workspace = true }
wiremock = { workspace = true }
shlex = { workspace = true }

View File

@@ -13,10 +13,8 @@ use crate::outgoing_message::RequestContext;
use crate::outgoing_message::ThreadScopedOutgoingMessageSender;
use crate::thread_status::ThreadWatchManager;
use crate::thread_status::resolve_thread_status;
use chrono::DateTime;
use chrono::Duration as ChronoDuration;
use chrono::SecondsFormat;
use chrono::Utc;
use codex_analytics::AnalyticsEventsClient;
use codex_analytics::AnalyticsJsonRpcError;
use codex_analytics::InputError;
@@ -274,7 +272,6 @@ use codex_core::exec::ExecCapturePolicy;
use codex_core::exec::ExecExpiration;
use codex_core::exec::ExecParams;
use codex_core::exec_env::create_env;
use codex_core::find_thread_name_by_id;
use codex_core::find_thread_path_by_id_str;
use codex_core::path_utils;
#[cfg(test)]
@@ -496,6 +493,7 @@ pub(crate) use self::thread_lifecycle::populate_thread_turns_from_history;
pub(crate) use self::thread_processor::thread_from_stored_thread;
#[cfg(test)]
pub(crate) use self::thread_summary::read_summary_from_rollout;
#[cfg(test)]
pub(crate) use self::thread_summary::summary_to_thread;
pub(crate) fn build_api_turns_from_rollout_items(items: &[RolloutItem]) -> Vec<Turn> {

View File

@@ -1605,11 +1605,8 @@ impl ThreadRequestProcessor {
.unarchive_thread(StoreArchiveThreadParams { thread_id })
.await
.map_err(|err| thread_store_archive_error("unarchive", err))?;
let summary = summary_from_stored_thread(stored_thread, fallback_provider.as_str())
.ok_or_else(|| {
internal_error(format!("failed to read unarchived thread {thread_id}"))
})?;
let mut thread = summary_to_thread(summary, &self.config.cwd);
let (mut thread, _) =
thread_from_stored_thread(stored_thread, fallback_provider.as_str(), &self.config.cwd);
thread.status = resolve_thread_status(
self.thread_watch_manager
@@ -2914,10 +2911,19 @@ impl ThreadRequestProcessor {
}
async fn attach_thread_name(&self, thread_id: ThreadId, thread: &mut Thread) {
if let Some(title) =
title_from_state_db(&self.config, self.state_db.as_ref(), thread_id).await
if let Ok(stored_thread) = self
.thread_store
.read_thread(StoreReadThreadParams {
thread_id,
include_archived: true,
include_history: false,
})
.await
&& let Some(title) = stored_thread.name.as_deref().map(str::trim)
&& !title.is_empty()
&& stored_thread.preview.trim() != title
{
set_thread_name_from_title(thread, title);
set_thread_name_from_title(thread, title.to_string());
}
}
@@ -3206,12 +3212,7 @@ impl ThreadRequestProcessor {
};
let stored_thread = read_result?;
let summary =
summary_from_stored_thread(stored_thread, fallback_provider).ok_or_else(|| {
internal_error(
"failed to load conversation summary: thread is missing rollout path",
)
})?;
let summary = summary_from_stored_thread(stored_thread, fallback_provider);
Ok(GetConversationSummaryResponse { summary })
}
@@ -3625,37 +3626,6 @@ fn thread_store_archive_error(operation: &str, err: ThreadStoreError) -> JSONRPC
}
}
async fn title_from_state_db(
config: &Config,
state_db_ctx: Option<&StateDbHandle>,
thread_id: ThreadId,
) -> Option<String> {
if let Some(state_db_ctx) = state_db_ctx
&& let Some(metadata) = state_db_ctx.get_thread(thread_id).await.ok().flatten()
&& let Some(title) = distinct_title(&metadata)
{
return Some(title);
}
find_thread_name_by_id(&config.codex_home, &thread_id)
.await
.ok()
.flatten()
}
fn non_empty_title(metadata: &ThreadMetadata) -> Option<String> {
let title = metadata.title.trim();
(!title.is_empty()).then(|| title.to_string())
}
fn distinct_title(metadata: &ThreadMetadata) -> Option<String> {
let title = non_empty_title(metadata)?;
if metadata.first_user_message.as_deref().map(str::trim) == Some(title.as_str()) {
None
} else {
Some(title)
}
}
fn set_thread_name_from_title(thread: &mut Thread, title: String) {
if title.trim().is_empty() || thread.preview.trim() == title.trim() {
return;
@@ -3719,8 +3689,8 @@ pub(crate) fn thread_from_stored_thread(
fn summary_from_stored_thread(
thread: StoredThread,
fallback_provider: &str,
) -> Option<ConversationSummary> {
let path = thread.rollout_path?;
) -> ConversationSummary {
let path = thread.rollout_path.unwrap_or_default();
let source = with_thread_spawn_agent_metadata(
thread.source,
thread.agent_nickname.clone(),
@@ -3731,7 +3701,7 @@ fn summary_from_stored_thread(
branch: git.branch,
origin_url: git.repository_url,
});
Some(ConversationSummary {
ConversationSummary {
conversation_id: thread.thread_id,
path,
preview: thread.first_user_message.unwrap_or(thread.preview),
@@ -3756,7 +3726,7 @@ fn summary_from_stored_thread(
cli_version: thread.cli_version,
source,
git_info,
})
}
}
#[allow(clippy::too_many_arguments)]

View File

@@ -409,8 +409,7 @@ mod thread_processor_behavior_tests {
history: None,
};
let summary =
summary_from_stored_thread(stored_thread, "fallback").expect("summary should exist");
let summary = summary_from_stored_thread(stored_thread, "fallback");
assert_eq!(
summary.timestamp.as_deref(),

View File

@@ -1,5 +1,10 @@
use super::*;
#[cfg(test)]
use chrono::DateTime;
#[cfg(test)]
use chrono::Utc;
#[cfg(test)]
pub(crate) async fn read_summary_from_rollout(
path: &Path,
@@ -203,6 +208,7 @@ pub(super) fn thread_response_sandbox_policy(
sandbox_policy.into()
}
#[cfg(test)]
fn parse_datetime(timestamp: Option<&str>) -> Option<DateTime<Utc>> {
timestamp.and_then(|ts| {
chrono::DateTime::parse_from_rfc3339(ts)
@@ -229,6 +235,7 @@ pub(super) fn thread_started_notification(mut thread: Thread) -> ThreadStartedNo
ThreadStartedNotification { thread }
}
#[cfg(test)]
pub(crate) fn summary_to_thread(
summary: ConversationSummary,
fallback_cwd: &AbsolutePathBuf,
@@ -257,6 +264,7 @@ pub(crate) fn summary_to_thread(
AbsolutePathBuf::relative_to_current_dir(path_utils::normalize_for_native_workdir(cwd))
.unwrap_or_else(|err| {
warn!(
conversation_id = %conversation_id,
path = %path.display(),
"failed to normalize thread cwd while summarizing thread: {err}"
);
@@ -274,7 +282,7 @@ pub(crate) fn summary_to_thread(
created_at: created_at.map(|dt| dt.timestamp()).unwrap_or(0),
updated_at: updated_at.map(|dt| dt.timestamp()).unwrap_or(0),
status: ThreadStatus::NotLoaded,
path: Some(path),
path: (!path.as_os_str().is_empty()).then_some(path),
cwd,
cli_version,
agent_nickname: source.get_nickname(),

View File

@@ -8,6 +8,7 @@ use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::TokenCountEvent;
use codex_protocol::protocol::TokenUsage;
use codex_protocol::protocol::TokenUsageInfo;
use core_test_support::test_path_buf;
use serde_json::json;
use std::fs;
use std::fs::FileTimes;
@@ -134,7 +135,7 @@ pub fn create_fake_rollout_with_source(
id: conversation_id,
forked_from_id: None,
timestamp: meta_rfc3339.to_string(),
cwd: PathBuf::from("/"),
cwd: test_path_buf("/"),
originator: "codex".to_string(),
cli_version: "0.0.0".to_string(),
source,
@@ -218,7 +219,7 @@ pub fn create_fake_rollout_with_text_elements(
id: conversation_id,
forked_from_id: None,
timestamp: meta_rfc3339.to_string(),
cwd: PathBuf::from("/"),
cwd: test_path_buf("/"),
originator: "codex".to_string(),
cli_version: "0.0.0".to_string(),
source: SessionSource::Cli,

View File

@@ -3,20 +3,42 @@ use app_test_support::McpProcess;
use app_test_support::create_fake_rollout;
use app_test_support::rollout_path;
use app_test_support::to_response;
use codex_app_server::in_process;
use codex_app_server::in_process::InProcessStartArgs;
use codex_app_server_protocol::ClientInfo;
use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::ConversationSummary;
use codex_app_server_protocol::GetConversationSummaryParams;
use codex_app_server_protocol::GetConversationSummaryResponse;
use codex_app_server_protocol::InitializeCapabilities;
use codex_app_server_protocol::InitializeParams;
use codex_app_server_protocol::JSONRPCError;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_arg0::Arg0DispatchPaths;
use codex_config::CloudRequirementsLoader;
use codex_config::LoaderOverrides;
use codex_core::config::ConfigBuilder;
use codex_exec_server::EnvironmentManager;
use codex_feedback::CodexFeedback;
use codex_protocol::ThreadId;
use codex_protocol::models::BaseInstructions;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::ThreadMemoryMode;
use codex_thread_store::CreateThreadParams;
use codex_thread_store::InMemoryThreadStore;
use codex_thread_store::ThreadEventPersistenceMode;
use codex_thread_store::ThreadPersistenceMetadata;
use codex_thread_store::ThreadStore;
use codex_utils_absolute_path::AbsolutePathBuf;
use core_test_support::test_path_buf;
use pretty_assertions::assert_eq;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use tempfile::TempDir;
use tokio::time::timeout;
use uuid::Uuid;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
const FILENAME_TS: &str = "2025-01-02T12-00-00";
@@ -35,7 +57,7 @@ fn expected_summary(conversation_id: ThreadId, path: PathBuf) -> ConversationSum
timestamp: Some(CREATED_AT_RFC3339.to_string()),
updated_at: Some(UPDATED_AT_RFC3339.to_string()),
model_provider: MODEL_PROVIDER.to_string(),
cwd: PathBuf::from("/"),
cwd: test_path_buf("/"),
cli_version: "0.0.0".to_string(),
source: SessionSource::Cli,
git_info: None,
@@ -47,7 +69,9 @@ fn normalized_canonical_path(path: impl AsRef<Path>) -> Result<PathBuf> {
}
fn normalized_summary_path(mut summary: ConversationSummary) -> Result<ConversationSummary> {
summary.path = normalized_canonical_path(&summary.path)?;
if !summary.path.as_os_str().is_empty() {
summary.path = normalized_canonical_path(summary.path)?;
}
Ok(summary)
}
@@ -122,6 +146,87 @@ async fn get_conversation_summary_by_rollout_path_rejects_remote_thread_store()
Ok(())
}
#[tokio::test]
async fn get_conversation_summary_by_thread_id_reads_pathless_store_thread() -> Result<()> {
let codex_home = TempDir::new()?;
let store_id = Uuid::new_v4().to_string();
create_config_toml_with_in_memory_thread_store(codex_home.path(), &store_id)?;
let store = InMemoryThreadStore::for_id(store_id.clone());
let _in_memory_store = InMemoryThreadStoreId { store_id };
let thread_id = ThreadId::from_string("00000000-0000-4000-8000-000000000125")?;
store
.create_thread(CreateThreadParams {
thread_id,
forked_from_id: None,
source: SessionSource::Cli,
thread_source: None,
base_instructions: BaseInstructions::default(),
dynamic_tools: Vec::new(),
metadata: ThreadPersistenceMetadata {
cwd: None,
model_provider: "test-provider".to_string(),
memory_mode: ThreadMemoryMode::Disabled,
},
event_persistence_mode: ThreadEventPersistenceMode::default(),
})
.await?;
let loader_overrides = LoaderOverrides::without_managed_config_for_tests();
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.loader_overrides(loader_overrides.clone())
.build()
.await?;
let client = in_process::start(InProcessStartArgs {
arg0_paths: Arg0DispatchPaths::default(),
config: Arc::new(config),
cli_overrides: Vec::new(),
loader_overrides,
cloud_requirements: CloudRequirementsLoader::default(),
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
feedback: CodexFeedback::new(),
log_db: None,
state_db: None,
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
config_warnings: Vec::new(),
session_source: SessionSource::Cli,
enable_codex_api_key_env: false,
initialize: InitializeParams {
client_info: ClientInfo {
name: "codex-app-server-tests".to_string(),
title: None,
version: "0.1.0".to_string(),
},
capabilities: Some(InitializeCapabilities {
experimental_api: true,
..Default::default()
}),
},
channel_capacity: in_process::DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
})
.await?;
let result = client
.request(ClientRequest::GetConversationSummary {
request_id: RequestId::Integer(1),
params: GetConversationSummaryParams::ThreadId {
conversation_id: thread_id,
},
})
.await?
.expect("getConversationSummary should succeed");
let GetConversationSummaryResponse { summary } = serde_json::from_value(result)?;
assert_eq!(summary.conversation_id, thread_id);
assert_eq!(summary.path, PathBuf::new());
assert_eq!(summary.cwd, PathBuf::new());
assert_eq!(summary.model_provider, "test");
client.shutdown().await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn get_conversation_summary_by_relative_rollout_path_resolves_from_codex_home() -> Result<()>
{
@@ -157,3 +262,39 @@ async fn get_conversation_summary_by_relative_rollout_path_resolves_from_codex_h
assert_eq!(normalized_summary_path(received.summary)?, expected);
Ok(())
}
struct InMemoryThreadStoreId {
store_id: String,
}
impl Drop for InMemoryThreadStoreId {
fn drop(&mut self) {
InMemoryThreadStore::remove_id(&self.store_id);
}
}
fn create_config_toml_with_in_memory_thread_store(
codex_home: &Path,
store_id: &str,
) -> std::io::Result<()> {
std::fs::write(
codex_home.join("config.toml"),
format!(
r#"
model = "mock-model"
approval_policy = "never"
sandbox_mode = "read-only"
experimental_thread_store = {{ type = "in_memory", id = "{store_id}" }}
model_provider = "mock_provider"
[model_providers.mock_provider]
name = "Mock provider for test"
base_url = "http://127.0.0.1:1/v1"
wire_api = "responses"
request_max_retries = 0
stream_max_retries = 0
"#
),
)
}

View File

@@ -426,6 +426,8 @@ fn realtime_sideband_connection(
WebSocketConnectionConfig {
requests: realtime_server_events,
response_headers: Vec::new(),
accept_started: None,
accept_release: None,
accept_delay: None,
close_after_requests: true,
}
@@ -1044,6 +1046,8 @@ async fn realtime_webrtc_start_emits_sdp_notification() -> Result<()> {
"session": { "id": "sess_webrtc", "instructions": "backend prompt" }
})]],
response_headers: Vec::new(),
accept_started: None,
accept_release: None,
accept_delay: None,
close_after_requests: false,
}])
@@ -1836,7 +1840,10 @@ async fn webrtc_v2_tool_call_delegated_turn_can_execute_shell_tool() -> Result<(
};
assert_eq!(id.as_str(), "shell_call");
assert_eq!(status, CommandExecutionStatus::Completed);
assert_eq!(aggregated_output.as_deref(), Some("realtime-tool-ok"));
assert_eq!(
aggregated_output.as_deref().map(str::trim),
Some("realtime-tool-ok")
);
// Phase 3: verify the shell output reached Responses and the final delegated answer returned
// to realtime as a single function-call-output item.
@@ -2154,10 +2161,10 @@ fn realtime_tool_ok_command() -> Vec<String> {
#[cfg(windows)]
{
vec![
"powershell.exe".to_string(),
"-NoProfile".to_string(),
"-Command".to_string(),
"[Console]::Write('realtime-tool-ok')".to_string(),
"cmd.exe".to_string(),
"/D".to_string(),
"/C".to_string(),
"echo realtime-tool-ok".to_string(),
]
}

View File

@@ -2,6 +2,12 @@ use anyhow::Result;
use app_test_support::McpProcess;
use app_test_support::create_mock_responses_server_repeating_assistant;
use app_test_support::to_response;
use codex_app_server::in_process;
use codex_app_server::in_process::InProcessStartArgs;
use codex_app_server_protocol::ClientInfo;
use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::InitializeCapabilities;
use codex_app_server_protocol::InitializeParams;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ThreadArchiveParams;
@@ -15,17 +21,36 @@ use codex_app_server_protocol::ThreadUnarchivedNotification;
use codex_app_server_protocol::TurnStartParams;
use codex_app_server_protocol::TurnStartResponse;
use codex_app_server_protocol::UserInput;
use codex_arg0::Arg0DispatchPaths;
use codex_config::CloudRequirementsLoader;
use codex_config::LoaderOverrides;
use codex_core::config::ConfigBuilder;
use codex_core::find_archived_thread_path_by_id_str;
use codex_core::find_thread_path_by_id_str;
use codex_exec_server::EnvironmentManager;
use codex_feedback::CodexFeedback;
use codex_protocol::ThreadId;
use codex_protocol::models::BaseInstructions;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::ThreadMemoryMode;
use codex_thread_store::CreateThreadParams;
use codex_thread_store::InMemoryThreadStore;
use codex_thread_store::ThreadEventPersistenceMode;
use codex_thread_store::ThreadMetadataPatch;
use codex_thread_store::ThreadPersistenceMetadata;
use codex_thread_store::ThreadStore;
use codex_thread_store::UpdateThreadMetadataParams;
use pretty_assertions::assert_eq;
use serde_json::Value;
use std::fs::FileTimes;
use std::fs::OpenOptions;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use std::time::SystemTime;
use tempfile::TempDir;
use tokio::time::timeout;
use uuid::Uuid;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
@@ -172,11 +197,139 @@ async fn thread_unarchive_moves_rollout_back_into_sessions_directory() -> Result
Ok(())
}
#[tokio::test]
async fn thread_unarchive_preserves_pathless_store_metadata() -> Result<()> {
let codex_home = TempDir::new()?;
let store_id = Uuid::new_v4().to_string();
create_config_toml_with_in_memory_thread_store(codex_home.path(), &store_id)?;
let store = InMemoryThreadStore::for_id(store_id.clone());
let _in_memory_store = InMemoryThreadStoreId { store_id };
let thread_id = ThreadId::from_string("00000000-0000-4000-8000-000000000126")?;
let parent_thread_id = ThreadId::from_string("00000000-0000-4000-8000-000000000127")?;
store
.create_thread(CreateThreadParams {
thread_id,
forked_from_id: Some(parent_thread_id),
source: SessionSource::Cli,
thread_source: None,
base_instructions: BaseInstructions::default(),
dynamic_tools: Vec::new(),
metadata: ThreadPersistenceMetadata {
cwd: None,
model_provider: "test-provider".to_string(),
memory_mode: ThreadMemoryMode::Disabled,
},
event_persistence_mode: ThreadEventPersistenceMode::default(),
})
.await?;
store
.update_thread_metadata(UpdateThreadMetadataParams {
thread_id,
patch: ThreadMetadataPatch {
name: Some("named pathless thread".to_string()),
..Default::default()
},
include_archived: true,
})
.await?;
let loader_overrides = LoaderOverrides::without_managed_config_for_tests();
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.loader_overrides(loader_overrides.clone())
.build()
.await?;
let client = in_process::start(InProcessStartArgs {
arg0_paths: Arg0DispatchPaths::default(),
config: Arc::new(config),
cli_overrides: Vec::new(),
loader_overrides,
cloud_requirements: CloudRequirementsLoader::default(),
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
feedback: CodexFeedback::new(),
log_db: None,
state_db: None,
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
config_warnings: Vec::new(),
session_source: SessionSource::Cli,
enable_codex_api_key_env: false,
initialize: InitializeParams {
client_info: ClientInfo {
name: "codex-app-server-tests".to_string(),
title: None,
version: "0.1.0".to_string(),
},
capabilities: Some(InitializeCapabilities {
experimental_api: true,
..Default::default()
}),
},
channel_capacity: in_process::DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
})
.await?;
let result = client
.request(ClientRequest::ThreadUnarchive {
request_id: RequestId::Integer(1),
params: ThreadUnarchiveParams {
thread_id: thread_id.to_string(),
},
})
.await?
.expect("thread/unarchive should succeed");
let ThreadUnarchiveResponse { thread } = serde_json::from_value(result)?;
assert_eq!(thread.id, thread_id.to_string());
assert_eq!(thread.path, None);
assert_eq!(thread.forked_from_id, Some(parent_thread_id.to_string()));
assert_eq!(thread.name, Some("named pathless thread".to_string()));
client.shutdown().await?;
Ok(())
}
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> {
let config_toml = codex_home.join("config.toml");
std::fs::write(config_toml, config_contents(server_uri))
}
struct InMemoryThreadStoreId {
store_id: String,
}
impl Drop for InMemoryThreadStoreId {
fn drop(&mut self) {
InMemoryThreadStore::remove_id(&self.store_id);
}
}
fn create_config_toml_with_in_memory_thread_store(
codex_home: &Path,
store_id: &str,
) -> std::io::Result<()> {
std::fs::write(
codex_home.join("config.toml"),
format!(
r#"
model = "mock-model"
approval_policy = "never"
sandbox_mode = "read-only"
experimental_thread_store = {{ type = "in_memory", id = "{store_id}" }}
model_provider = "mock_provider"
[model_providers.mock_provider]
name = "Mock provider for test"
base_url = "http://127.0.0.1:1/v1"
wire_api = "responses"
request_max_retries = 0
stream_max_retries = 0
"#
),
)
}
fn config_contents(server_uri: &str) -> String {
format!(
r#"model = "mock-model"

View File

@@ -239,6 +239,65 @@ async fn wait_for_live_thread_spawn_children(
.expect("expected persisted child tree");
}
async fn wait_for_agent_shutdown(
thread_id: ThreadId,
mut status_rx: tokio::sync::watch::Receiver<AgentStatus>,
) {
if matches!(status_rx.borrow().clone(), AgentStatus::Shutdown) {
return;
}
timeout(Duration::from_secs(5), async {
loop {
status_rx
.changed()
.await
.unwrap_or_else(|_| panic!("thread {thread_id} status should reach shutdown"));
if matches!(status_rx.borrow().clone(), AgentStatus::Shutdown) {
break;
}
}
})
.await
.unwrap_or_else(|_| panic!("thread {thread_id} should shut down before resume"));
}
async fn shutdown_live_agent_and_wait(control: &AgentControl, thread_id: ThreadId) {
let status_rx = control
.subscribe_status(thread_id)
.await
.expect("status subscription should succeed before shutdown");
let _ = control
.shutdown_live_agent(thread_id)
.await
.expect("thread shutdown should submit");
wait_for_agent_shutdown(thread_id, status_rx).await;
}
async fn close_agent_and_wait(
control: &AgentControl,
agent_id: ThreadId,
shutdown_ids: &[ThreadId],
) {
let mut status_rxs = Vec::with_capacity(shutdown_ids.len());
for thread_id in shutdown_ids {
status_rxs.push((
*thread_id,
control
.subscribe_status(*thread_id)
.await
.expect("status subscription should succeed before close"),
));
}
let _ = control
.close_agent(agent_id)
.await
.expect("agent close should succeed");
for (thread_id, status_rx) in status_rxs {
wait_for_agent_shutdown(thread_id, status_rx).await;
}
}
#[tokio::test]
async fn send_input_errors_when_manager_dropped() {
let control = AgentControl::default();
@@ -1626,11 +1685,9 @@ async fn resume_thread_subagent_restores_stored_nickname_and_role() {
.await
.expect("child thread metadata should be persisted to sqlite before shutdown");
let _ = harness
.control
.shutdown_live_agent(child_thread_id)
.await
.expect("child shutdown should submit");
drop(status_rx);
shutdown_live_agent_and_wait(&harness.control, child_thread_id).await;
drop(child_thread);
let resumed_thread_id = harness
.control
@@ -1699,11 +1756,8 @@ async fn resume_agent_from_rollout_reads_archived_rollout_path() {
.await
.expect("child thread should exist");
persist_thread_for_tree_resume(&child_thread, "persist before archiving").await;
let _ = harness
.control
.shutdown_live_agent(child_thread_id)
.await
.expect("child shutdown should succeed");
shutdown_live_agent_and_wait(&harness.control, child_thread_id).await;
drop(child_thread);
let store = LocalThreadStore::new(
LocalThreadStoreConfig::from_config(&harness.config),
harness.state_db.clone(),
@@ -1993,11 +2047,12 @@ async fn shutdown_agent_tree_closes_descendants_when_started_at_child() {
wait_for_live_thread_spawn_children(&harness.control, child_thread_id, &[grandchild_thread_id])
.await;
let _ = harness
.control
.close_agent(child_thread_id)
.await
.expect("child close should succeed");
close_agent_and_wait(
&harness.control,
child_thread_id,
&[child_thread_id, grandchild_thread_id],
)
.await;
let _ = harness
.control
@@ -2085,16 +2140,14 @@ async fn resume_agent_from_rollout_does_not_reopen_closed_descendants() {
wait_for_live_thread_spawn_children(&harness.control, child_thread_id, &[grandchild_thread_id])
.await;
let _ = harness
.control
.close_agent(child_thread_id)
.await
.expect("child close should succeed");
let _ = harness
.control
.shutdown_live_agent(parent_thread_id)
.await
.expect("parent shutdown should succeed");
close_agent_and_wait(
&harness.control,
child_thread_id,
&[child_thread_id, grandchild_thread_id],
)
.await;
shutdown_live_agent_and_wait(&harness.control, parent_thread_id).await;
drop(parent_thread);
let resumed_parent_thread_id = harness
.control
@@ -2180,11 +2233,12 @@ async fn resume_closed_child_reopens_open_descendants() {
wait_for_live_thread_spawn_children(&harness.control, child_thread_id, &[grandchild_thread_id])
.await;
let _ = harness
.control
.close_agent(child_thread_id)
.await
.expect("child close should succeed");
close_agent_and_wait(
&harness.control,
child_thread_id,
&[child_thread_id, grandchild_thread_id],
)
.await;
let resumed_child_thread_id = harness
.control

View File

@@ -35,7 +35,6 @@ use crate::exec_policy::ExecPolicyManager;
use crate::parse_turn_item;
use crate::path_utils::normalize_for_native_workdir;
use crate::realtime_conversation::RealtimeConversationManager;
use crate::rollout::find_thread_name_by_id;
use crate::session_prefix::format_subagent_notification_message;
use crate::skills::SkillRenderSideEffects;
use crate::skills_load_input_from_config;
@@ -133,6 +132,7 @@ use codex_thread_store::CreateThreadParams;
use codex_thread_store::LiveThread;
use codex_thread_store::LiveThreadInitGuard;
use codex_thread_store::LocalThreadStore;
use codex_thread_store::ReadThreadParams;
use codex_thread_store::ResumeThreadParams;
use codex_thread_store::ThreadEventPersistenceMode;
use codex_thread_store::ThreadPersistenceMetadata;
@@ -829,24 +829,33 @@ pub(crate) fn session_loop_termination_from_handle(
.shared()
}
async fn thread_title_from_state_db(
state_db: Option<&state_db::StateDbHandle>,
codex_home: &AbsolutePathBuf,
async fn thread_title_from_thread_store(
live_thread: Option<&LiveThread>,
thread_store: &Arc<dyn ThreadStore>,
conversation_id: ThreadId,
) -> Option<String> {
if let Some(metadata) = state_db
&& let Some(metadata) = metadata.get_thread(conversation_id).await.ok().flatten()
{
let title = metadata.title.trim();
if !title.is_empty() && metadata.first_user_message.as_deref().map(str::trim) != Some(title)
{
return Some(title.to_string());
let thread = match live_thread {
Some(live_thread) => {
live_thread
.read_thread(
/*include_archived*/ true, /*include_history*/ false,
)
.await
}
None => {
thread_store
.read_thread(ReadThreadParams {
thread_id: conversation_id,
include_archived: true,
include_history: false,
})
.await
}
}
find_thread_name_by_id(codex_home, &conversation_id)
.await
.ok()
.flatten()
.ok()?;
let title = thread.name.as_deref()?.trim();
(!title.is_empty() && thread.preview.trim() != title).then(|| title.to_string())
}
impl Session {

View File

@@ -717,7 +717,7 @@ impl Session {
tx
};
let thread_name =
thread_title_from_state_db(state_db_ctx.as_ref(), &config.codex_home, thread_id)
thread_title_from_thread_store(live_thread_init.as_ref(), &thread_store, thread_id)
.instrument(info_span!(
"session_init.thread_name_lookup",
otel.name = "session_init.thread_name_lookup",

View File

@@ -8908,7 +8908,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
let tool_name = "shell";
let call_id = "test-call".to_string();
let handler = ShellHandler;
let handler = ShellHandler::default();
let resp = handler
.handle(ToolInvocation {
session: Arc::clone(&session),
@@ -8983,7 +8983,7 @@ async fn unified_exec_rejects_escalated_permissions_when_policy_not_on_request()
let turn_context = Arc::new(turn_context_raw);
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
let handler = ExecCommandHandler;
let handler = ExecCommandHandler::default();
let resp = handler
.handle(ToolInvocation {
session: Arc::clone(&session),

View File

@@ -323,7 +323,7 @@ async fn guardian_allows_shell_additional_permissions_requests_past_policy_valid
arg0: None,
};
let handler = ShellHandler;
let handler = ShellHandler::default();
let resp = handler
.handle(ToolInvocation {
session: Arc::clone(&session),
@@ -437,7 +437,7 @@ async fn strict_auto_review_turn_grant_forces_guardian_for_shell_policy_skip() {
let session = Arc::new(session);
let turn_context = Arc::new(turn_context_raw);
let handler = ShellHandler;
let handler = ShellHandler::default();
let command = if cfg!(windows) {
vec![
"cmd.exe".to_string(),
@@ -498,7 +498,7 @@ async fn guardian_allows_unified_exec_additional_permissions_requests_past_polic
let turn_context = Arc::new(turn_context_raw);
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
let handler = ExecCommandHandler;
let handler = ExecCommandHandler::default();
let resp = handler
.handle(ToolInvocation {
session: Arc::clone(&session),
@@ -615,7 +615,7 @@ async fn shell_handler_allows_sticky_turn_permissions_without_inline_request_per
let session = Arc::new(session);
let turn_context = Arc::new(turn_context_raw);
let handler = ShellHandler;
let handler = ShellHandler::default();
let resp = handler
.handle(ToolInvocation {
session: Arc::clone(&session),

View File

@@ -7,7 +7,6 @@ use crate::environment_selection::default_thread_environment_selections;
use crate::environment_selection::resolve_environment_selections;
use crate::file_watcher::FileWatcher;
use crate::mcp::McpManager;
use crate::rollout::RolloutRecorder;
use crate::rollout::truncation;
use crate::session::Codex;
use crate::session::CodexSpawnArgs;
@@ -41,6 +40,7 @@ use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::InitialHistory;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::ResumedHistory;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::SessionConfiguredEvent;
use codex_protocol::protocol::SessionSource;
@@ -55,6 +55,7 @@ use codex_state::DirectionalThreadSpawnEdgeStatus;
use codex_thread_store::InMemoryThreadStore;
use codex_thread_store::LocalThreadStore;
use codex_thread_store::LocalThreadStoreConfig;
use codex_thread_store::ReadThreadByRolloutPathParams;
use codex_thread_store::ReadThreadParams;
use codex_thread_store::RemoteThreadStore;
use codex_thread_store::StoredThread;
@@ -615,7 +616,7 @@ impl ThreadManager {
auth_manager: Arc<AuthManager>,
parent_trace: Option<W3cTraceContext>,
) -> CodexResult<NewThread> {
let initial_history = RolloutRecorder::get_rollout_history(&rollout_path).await?;
let initial_history = self.initial_history_from_rollout_path(rollout_path).await?;
Box::pin(self.resume_thread_with_history(
config,
initial_history,
@@ -687,7 +688,7 @@ impl ThreadManager {
auth_manager: Arc<AuthManager>,
user_shell_override: crate::shell::Shell,
) -> CodexResult<NewThread> {
let initial_history = RolloutRecorder::get_rollout_history(&rollout_path).await?;
let initial_history = self.initial_history_from_rollout_path(rollout_path).await?;
let environments = default_thread_environment_selections(
self.state.environment_manager.as_ref(),
&config.cwd,
@@ -784,7 +785,7 @@ impl ThreadManager {
S: Into<ForkSnapshot>,
{
let snapshot = snapshot.into();
let history = RolloutRecorder::get_rollout_history(&path).await?;
let history = self.initial_history_from_rollout_path(path).await?;
self.fork_thread_from_history(
snapshot,
config,
@@ -796,6 +797,24 @@ impl ThreadManager {
.await
}
async fn initial_history_from_rollout_path(
&self,
rollout_path: PathBuf,
) -> CodexResult<InitialHistory> {
let requested_rollout_path = rollout_path.clone();
let stored_thread = self
.state
.thread_store
.read_thread_by_rollout_path(ReadThreadByRolloutPathParams {
rollout_path,
include_archived: true,
include_history: true,
})
.await
.map_err(thread_store_rollout_read_error)?;
stored_thread_to_initial_history(stored_thread, Some(requested_rollout_path))
}
/// Fork an existing thread from already-loaded store history.
pub async fn fork_thread_from_history<S>(
&self,
@@ -1280,6 +1299,31 @@ impl ThreadManagerState {
}
}
fn stored_thread_to_initial_history(
stored_thread: StoredThread,
rollout_path: Option<PathBuf>,
) -> CodexResult<InitialHistory> {
let thread_id = stored_thread.thread_id;
let history = stored_thread.history.ok_or_else(|| {
CodexErr::Fatal(format!(
"thread {thread_id} did not include persisted history"
))
})?;
Ok(InitialHistory::Resumed(ResumedHistory {
conversation_id: thread_id,
history: history.items,
rollout_path: rollout_path.or(stored_thread.rollout_path),
}))
}
fn thread_store_rollout_read_error(err: ThreadStoreError) -> CodexErr {
match err {
ThreadStoreError::ThreadNotFound { thread_id } => CodexErr::ThreadNotFound(thread_id),
ThreadStoreError::InvalidRequest { message } => CodexErr::InvalidRequest(message),
err => CodexErr::Fatal(format!("failed to read thread by rollout path: {err}")),
}
}
/// Return a fork snapshot cut strictly before the nth user message (0-based).
///
/// Out-of-range values keep the full committed history at a turn boundary, but

View File

@@ -16,6 +16,7 @@ use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::protocol::AgentMessageEvent;
use codex_protocol::protocol::InitialHistory;
use codex_protocol::protocol::InternalSessionSource;
use codex_protocol::protocol::ResumedHistory;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::ThreadSource;
use codex_protocol::protocol::TurnStartedEvent;
@@ -730,6 +731,111 @@ async fn resume_stopped_thread_from_rollout_preserves_thread_source() {
.expect("shutdown resumed thread");
}
#[tokio::test]
async fn rollout_path_resume_and_fork_read_history_through_thread_store() {
let temp_dir = tempdir().expect("tempdir");
let mut config = test_config().await;
config.codex_home = temp_dir.path().join("codex-home").abs();
config.cwd = config.codex_home.abs();
config.experimental_thread_store = ThreadStoreConfig::InMemory {
id: format!("thread-manager-{}", uuid::Uuid::new_v4()),
};
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let state_db = init_state_db(&config).await;
let thread_store = thread_store_from_config(&config, state_db.clone());
let in_memory_store = thread_store
.as_any()
.downcast_ref::<InMemoryThreadStore>()
.expect("configured in-memory store");
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
thread_store.clone(),
state_db,
TEST_INSTALLATION_ID.to_string(),
);
let source = manager
.start_thread(config.clone())
.await
.expect("start source thread");
source
.thread
.shutdown_and_wait()
.await
.expect("shutdown source thread");
let _ = manager.remove_thread(&source.thread_id).await;
let rollout_path = config
.codex_home
.join("rollouts/source.jsonl")
.to_path_buf();
let resumed = manager
.resume_thread_with_history(
config.clone(),
InitialHistory::Resumed(ResumedHistory {
conversation_id: source.thread_id,
history: vec![RolloutItem::ResponseItem(user_msg("hello"))],
rollout_path: Some(rollout_path.clone()),
}),
auth_manager.clone(),
/*persist_extended_history*/ false,
/*parent_trace*/ None,
)
.await
.expect("seed rollout path in store");
resumed
.thread
.shutdown_and_wait()
.await
.expect("shutdown seeded resumed thread");
let _ = manager.remove_thread(&resumed.thread_id).await;
let resumed_from_path = manager
.resume_thread_from_rollout(
config.clone(),
rollout_path.clone(),
auth_manager,
/*parent_trace*/ None,
)
.await
.expect("resume from rollout path");
assert_eq!(resumed_from_path.thread_id, resumed.thread_id);
let forked = manager
.fork_thread(
ForkSnapshot::Interrupted,
config,
rollout_path,
/*thread_source*/ None,
/*persist_extended_history*/ false,
/*parent_trace*/ None,
)
.await
.expect("fork from rollout path");
assert_ne!(forked.thread_id, resumed.thread_id);
let calls = in_memory_store.calls().await;
assert_eq!(calls.read_thread_by_rollout_path, 2);
resumed_from_path
.thread
.shutdown_and_wait()
.await
.expect("shutdown path-resumed thread");
forked
.thread
.shutdown_and_wait()
.await
.expect("shutdown forked thread");
}
#[tokio::test]
async fn new_uses_active_provider_for_model_refresh() {
let server = MockServer::start().await;

View File

@@ -5,6 +5,7 @@ use crate::tools::context::ToolPayload;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use super::ExecContext;
use super::PUBLIC_TOOL_NAME;
@@ -12,9 +13,15 @@ use super::build_enabled_tools;
use super::handle_runtime_response;
use super::is_exec_tool_name;
pub struct CodeModeExecuteHandler;
pub struct CodeModeExecuteHandler {
spec: ToolSpec,
}
impl CodeModeExecuteHandler {
pub(crate) fn new(spec: ToolSpec) -> Self {
Self { spec }
}
async fn execute(
&self,
session: std::sync::Arc<crate::session::session::Session>,
@@ -83,6 +90,10 @@ impl ToolHandler for CodeModeExecuteHandler {
ToolName::plain(PUBLIC_TOOL_NAME)
}
fn spec(&self) -> Option<ToolSpec> {
Some(self.spec.clone())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -7,11 +7,13 @@ use crate::tools::context::ToolPayload;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use super::DEFAULT_WAIT_YIELD_TIME_MS;
use super::ExecContext;
use super::WAIT_TOOL_NAME;
use super::handle_runtime_response;
use super::wait_spec::create_wait_tool;
pub struct CodeModeWaitHandler;
@@ -46,6 +48,10 @@ impl ToolHandler for CodeModeWaitHandler {
ToolName::plain(WAIT_TOOL_NAME)
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_wait_tool())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -196,6 +196,12 @@ async fn run_agent_job_loop(
)
.await?;
for item in pending_items {
let claimed = db
.mark_agent_job_item_running(job_id.as_str(), item.item_id.as_str())
.await?;
if !claimed {
continue;
}
let prompt = build_worker_prompt(&job, &item)?;
let items = vec![UserInput::Text {
text: prompt,
@@ -240,7 +246,7 @@ async fn run_agent_job_loop(
}
};
let assigned = db
.mark_agent_job_item_running_with_thread(
.set_agent_job_item_thread(
job_id.as_str(),
item.item_id.as_str(),
thread_id.to_string().as_str(),

View File

@@ -2,9 +2,11 @@ use crate::function_tool::FunctionCallError;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::agent_jobs_spec::create_report_agent_job_result_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use super::*;
@@ -17,6 +19,10 @@ impl ToolHandler for ReportAgentJobResultHandler {
ToolName::plain("report_agent_job_result")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_report_agent_job_result_tool())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
@@ -55,27 +61,31 @@ pub async fn handle(
}
let db = required_state_db(&session)?;
let reporting_thread_id = session.conversation_id.to_string();
let accepted = db
.report_agent_job_item_result(
let accepted = if args.stop.unwrap_or(false) {
db.report_agent_job_item_result_and_cancel_job(
args.job_id.as_str(),
args.item_id.as_str(),
reporting_thread_id.as_str(),
&args.result,
"cancelled by worker request",
)
.await
} else {
db.report_agent_job_item_result(
args.job_id.as_str(),
args.item_id.as_str(),
reporting_thread_id.as_str(),
&args.result,
)
.await
.map_err(|err| {
let job_id = args.job_id.as_str();
let item_id = args.item_id.as_str();
FunctionCallError::RespondToModel(format!(
"failed to record agent job result for {job_id} / {item_id}: {err}"
))
})?;
if accepted && args.stop.unwrap_or(false) {
let message = "cancelled by worker request";
let _ = db
.mark_agent_job_cancelled(args.job_id.as_str(), message)
.await;
}
.map_err(|err| {
let job_id = args.job_id.as_str();
let item_id = args.item_id.as_str();
FunctionCallError::RespondToModel(format!(
"failed to record agent job result for {job_id} / {item_id}: {err}"
))
})?;
let content =
serde_json::to_string(&ReportAgentJobResultToolResult { accepted }).map_err(|err| {
FunctionCallError::Fatal(format!(

View File

@@ -2,9 +2,11 @@ use crate::function_tool::FunctionCallError;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::agent_jobs_spec::create_spawn_agents_on_csv_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use super::*;
@@ -17,6 +19,10 @@ impl ToolHandler for SpawnAgentsOnCsvHandler {
ToolName::plain("spawn_agents_on_csv")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_spawn_agents_on_csv_tool())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -22,6 +22,8 @@ use crate::tools::events::ToolEmitter;
use crate::tools::events::ToolEventCtx;
use crate::tools::handlers::apply_granted_turn_permissions;
use crate::tools::handlers::apply_patch_spec::ApplyPatchToolArgs;
use crate::tools::handlers::apply_patch_spec::create_apply_patch_freeform_tool;
use crate::tools::handlers::apply_patch_spec::create_apply_patch_json_tool;
use crate::tools::handlers::parse_arguments;
use crate::tools::hook_names::HookToolName;
use crate::tools::orchestrator::ToolOrchestrator;
@@ -41,6 +43,7 @@ use codex_exec_server::ExecutorFileSystem;
use codex_features::Feature;
use codex_protocol::models::AdditionalPermissionProfile;
use codex_protocol::models::FileSystemPermissions;
use codex_protocol::openai_models::ApplyPatchToolType;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::FileChange;
use codex_protocol::protocol::PatchApplyUpdatedEvent;
@@ -48,11 +51,30 @@ use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy;
use codex_sandboxing::policy_transforms::merge_permission_profiles;
use codex_sandboxing::policy_transforms::normalize_additional_permissions;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use codex_utils_absolute_path::AbsolutePathBuf;
const APPLY_PATCH_ARGUMENT_DIFF_BUFFER_INTERVAL: Duration = Duration::from_millis(500);
pub struct ApplyPatchHandler;
pub struct ApplyPatchHandler {
options: ApplyPatchToolType,
}
impl Default for ApplyPatchHandler {
fn default() -> Self {
Self {
options: ApplyPatchToolType::Freeform,
}
}
}
impl ApplyPatchHandler {
pub(crate) fn new(apply_patch_tool_type: ApplyPatchToolType) -> Self {
Self {
options: apply_patch_tool_type,
}
}
}
#[derive(Default)]
struct ApplyPatchArgumentDiffConsumer {
@@ -297,6 +319,13 @@ impl ToolHandler for ApplyPatchHandler {
ToolName::plain("apply_patch")
}
fn spec(&self) -> Option<ToolSpec> {
Some(match self.options {
ApplyPatchToolType::Freeform => create_apply_patch_freeform_tool(),
ApplyPatchToolType::Function => create_apply_patch_json_tool(),
})
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -49,7 +49,7 @@ async fn pre_tool_use_payload_uses_json_patch_input() {
arguments: json!({ "input": patch }).to_string(),
};
let invocation = invocation_for_payload(payload).await;
let handler = ApplyPatchHandler;
let handler = ApplyPatchHandler::default();
assert_eq!(
handler.pre_tool_use_payload(&invocation),
@@ -67,7 +67,7 @@ async fn pre_tool_use_payload_uses_freeform_patch_input() {
input: patch.to_string(),
};
let invocation = invocation_for_payload(payload).await;
let handler = ApplyPatchHandler;
let handler = ApplyPatchHandler::default();
assert_eq!(
handler.pre_tool_use_payload(&invocation),
@@ -86,7 +86,7 @@ async fn post_tool_use_payload_uses_patch_input_and_tool_output() {
};
let invocation = invocation_for_payload(payload).await;
let output = ApplyPatchToolOutput::from_text("Success. Updated files.".to_string());
let handler = ApplyPatchHandler;
let handler = ApplyPatchHandler::default();
assert_eq!(
handler.post_tool_use_payload(&invocation, &output),

View File

@@ -4,10 +4,12 @@ use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::goal_spec::CREATE_GOAL_TOOL_NAME;
use crate::tools::handlers::goal_spec::create_create_goal_tool;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use super::CompletionBudgetReport;
use super::CreateGoalArgs;
@@ -23,6 +25,10 @@ impl ToolHandler for CreateGoalHandler {
ToolName::plain(CREATE_GOAL_TOOL_NAME)
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_create_goal_tool())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -3,9 +3,11 @@ use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::goal_spec::GET_GOAL_TOOL_NAME;
use crate::tools::handlers::goal_spec::create_get_goal_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use super::CompletionBudgetReport;
use super::format_goal_error;
@@ -20,6 +22,10 @@ impl ToolHandler for GetGoalHandler {
ToolName::plain(GET_GOAL_TOOL_NAME)
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_get_goal_tool())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -5,11 +5,13 @@ use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::goal_spec::UPDATE_GOAL_TOOL_NAME;
use crate::tools::handlers::goal_spec::create_update_goal_tool;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_protocol::protocol::ThreadGoalStatus;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use super::CompletionBudgetReport;
use super::UpdateGoalArgs;
@@ -25,6 +27,10 @@ impl ToolHandler for UpdateGoalHandler {
ToolName::plain(UPDATE_GOAL_TOOL_NAME)
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_update_goal_tool())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -4,11 +4,13 @@ use crate::function_tool::FunctionCallError;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::mcp_resource_spec::create_list_mcp_resource_templates_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_protocol::models::function_call_output_content_items_to_text;
use codex_protocol::protocol::McpInvocation;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use rmcp::model::PaginatedRequestParams;
@@ -31,6 +33,14 @@ impl ToolHandler for ListMcpResourceTemplatesHandler {
ToolName::plain("list_mcp_resource_templates")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_list_mcp_resource_templates_tool())
}
fn supports_parallel_tool_calls(&self) -> bool {
true
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -4,11 +4,13 @@ use crate::function_tool::FunctionCallError;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::mcp_resource_spec::create_list_mcp_resources_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_protocol::models::function_call_output_content_items_to_text;
use codex_protocol::protocol::McpInvocation;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use rmcp::model::PaginatedRequestParams;
@@ -31,6 +33,14 @@ impl ToolHandler for ListMcpResourcesHandler {
ToolName::plain("list_mcp_resources")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_list_mcp_resources_tool())
}
fn supports_parallel_tool_calls(&self) -> bool {
true
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -4,11 +4,13 @@ use crate::function_tool::FunctionCallError;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::mcp_resource_spec::create_read_mcp_resource_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_protocol::models::function_call_output_content_items_to_text;
use codex_protocol::protocol::McpInvocation;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use rmcp::model::ReadResourceRequestParams;
@@ -31,6 +33,14 @@ impl ToolHandler for ReadMcpResourceHandler {
ToolName::plain("read_mcp_resource")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_read_mcp_resource_tool())
}
fn supports_parallel_tool_calls(&self) -> bool {
true
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -64,12 +64,14 @@ pub use request_user_input::RequestUserInputHandler;
pub use shell::ContainerExecHandler;
pub use shell::LocalShellHandler;
pub use shell::ShellCommandHandler;
pub(crate) use shell::ShellCommandHandlerOptions;
pub use shell::ShellHandler;
pub use test_sync::TestSyncHandler;
pub use tool_search::ToolSearchHandler;
pub use unavailable_tool::UnavailableToolHandler;
pub(crate) use unavailable_tool::unavailable_tool_message;
pub use unified_exec::ExecCommandHandler;
pub(crate) use unified_exec::ExecCommandHandlerOptions;
pub use unified_exec::WriteStdinHandler;
pub use view_image::ViewImageHandler;

View File

@@ -1,5 +1,7 @@
use super::*;
use crate::tools::handlers::multi_agents_spec::create_close_agent_tool_v1;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_tools::ToolSpec;
pub(crate) struct Handler;
@@ -10,6 +12,10 @@ impl ToolHandler for Handler {
ToolName::plain("close_agent")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_close_agent_tool_v1())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -1,6 +1,8 @@
use super::*;
use crate::agent::next_thread_spawn_depth;
use crate::tools::handlers::multi_agents_spec::create_resume_agent_tool;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_tools::ToolSpec;
use std::sync::Arc;
pub(crate) struct Handler;
@@ -12,6 +14,10 @@ impl ToolHandler for Handler {
ToolName::plain("resume_agent")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_resume_agent_tool())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -1,6 +1,8 @@
use super::*;
use crate::agent::control::render_input_preview;
use crate::tools::handlers::multi_agents_spec::create_send_input_tool_v1;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_tools::ToolSpec;
pub(crate) struct Handler;
@@ -11,6 +13,10 @@ impl ToolHandler for Handler {
ToolName::plain("send_input")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_send_input_tool_v1())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -6,9 +6,21 @@ use crate::agent::exceeds_thread_spawn_depth_limit;
use crate::agent::next_thread_spawn_depth;
use crate::agent::role::DEFAULT_ROLE_NAME;
use crate::agent::role::apply_role_to_config;
use crate::tools::handlers::multi_agents_spec::SpawnAgentToolOptions;
use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v1;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_tools::ToolSpec;
pub(crate) struct Handler;
#[derive(Default)]
pub(crate) struct Handler {
options: SpawnAgentToolOptions,
}
impl Handler {
pub(crate) fn new(options: SpawnAgentToolOptions) -> Self {
Self { options }
}
}
impl ToolHandler for Handler {
type Output = SpawnAgentResult;
@@ -17,6 +29,10 @@ impl ToolHandler for Handler {
ToolName::plain("spawn_agent")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_spawn_agent_tool_v1(self.options.clone()))
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -1,7 +1,10 @@
use super::*;
use crate::agent::status::is_final;
use crate::tools::handlers::multi_agents_spec::WaitAgentTimeoutOptions;
use crate::tools::handlers::multi_agents_spec::create_wait_agent_tool_v1;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_protocol::error::CodexErr;
use codex_tools::ToolSpec;
use futures::FutureExt;
use futures::StreamExt;
use futures::stream::FuturesUnordered;
@@ -13,7 +16,16 @@ use tokio::time::Instant;
use tokio::time::timeout_at;
pub(crate) struct Handler;
#[derive(Default)]
pub(crate) struct Handler {
options: WaitAgentTimeoutOptions,
}
impl Handler {
pub(crate) fn new(options: WaitAgentTimeoutOptions) -> Self {
Self { options }
}
}
impl ToolHandler for Handler {
type Output = WaitAgentResult;
@@ -22,6 +34,10 @@ impl ToolHandler for Handler {
ToolName::plain("wait_agent")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_wait_agent_tool_v1(self.options))
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -9,9 +9,9 @@ use std::collections::BTreeMap;
const SPAWN_AGENT_INHERITED_MODEL_GUIDANCE: &str = "Spawned agents inherit your current model by default. Omit `model` to use that preferred default; set `model` only when an explicit override is needed.";
const SPAWN_AGENT_MODEL_OVERRIDE_DESCRIPTION: &str = "Optional model override for the new agent. Leave unset to inherit the same model as the parent, which is the preferred default. Only set this when the user explicitly asks for a different model or the task clearly requires one.";
#[derive(Debug, Clone)]
pub struct SpawnAgentToolOptions<'a> {
pub available_models: &'a [ModelPreset],
#[derive(Debug, Clone, Default)]
pub struct SpawnAgentToolOptions {
pub available_models: Vec<ModelPreset>,
pub agent_type_description: String,
pub hide_agent_type_model_reasoning: bool,
pub include_usage_hint: bool,
@@ -26,9 +26,19 @@ pub struct WaitAgentTimeoutOptions {
pub max_timeout_ms: i64,
}
pub fn create_spawn_agent_tool_v1(options: SpawnAgentToolOptions<'_>) -> ToolSpec {
impl Default for WaitAgentTimeoutOptions {
fn default() -> Self {
Self {
default_timeout_ms: super::multi_agents_common::DEFAULT_WAIT_TIMEOUT_MS,
min_timeout_ms: super::multi_agents_common::MIN_WAIT_TIMEOUT_MS,
max_timeout_ms: super::multi_agents_common::MAX_WAIT_TIMEOUT_MS,
}
}
}
pub fn create_spawn_agent_tool_v1(options: SpawnAgentToolOptions) -> ToolSpec {
let available_models_description = (!options.hide_agent_type_model_reasoning)
.then(|| spawn_agent_models_description(options.available_models));
.then(|| spawn_agent_models_description(&options.available_models));
let return_value_description =
"Returns the spawned agent id plus the user-facing nickname when available.";
let mut properties = spawn_agent_common_properties_v1(&options.agent_type_description);
@@ -51,9 +61,9 @@ pub fn create_spawn_agent_tool_v1(options: SpawnAgentToolOptions<'_>) -> ToolSpe
})
}
pub fn create_spawn_agent_tool_v2(options: SpawnAgentToolOptions<'_>) -> ToolSpec {
pub fn create_spawn_agent_tool_v2(options: SpawnAgentToolOptions) -> ToolSpec {
let available_models_description = (!options.hide_agent_type_model_reasoning)
.then(|| spawn_agent_models_description(options.available_models));
.then(|| spawn_agent_models_description(&options.available_models));
let mut properties = spawn_agent_common_properties_v2(&options.agent_type_description);
if options.hide_agent_type_model_reasoning {
hide_spawn_agent_metadata_options(&mut properties);

View File

@@ -33,7 +33,7 @@ fn model_preset(id: &str, show_in_picker: bool) -> ModelPreset {
#[test]
fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {
let tool = create_spawn_agent_tool_v2(SpawnAgentToolOptions {
available_models: &[
available_models: vec![
model_preset("visible", /*show_in_picker*/ true),
model_preset("hidden", /*show_in_picker*/ false),
],
@@ -99,7 +99,7 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {
#[test]
fn spawn_agent_tool_v1_keeps_legacy_fork_context_field() {
let tool = create_spawn_agent_tool_v1(SpawnAgentToolOptions {
available_models: &[],
available_models: Vec::new(),
agent_type_description: "role help".to_string(),
hide_agent_type_model_reasoning: false,
include_usage_hint: true,

View File

@@ -180,7 +180,7 @@ async fn handler_rejects_non_function_payloads() {
input: "hello".to_string(),
},
);
let Err(err) = SpawnAgentHandler.handle(invocation).await else {
let Err(err) = SpawnAgentHandler::default().handle(invocation).await else {
panic!("payload should be rejected");
};
assert_eq!(
@@ -200,7 +200,7 @@ async fn spawn_agent_rejects_empty_message() {
"spawn_agent",
function_payload(json!({"message": " "})),
);
let Err(err) = SpawnAgentHandler.handle(invocation).await else {
let Err(err) = SpawnAgentHandler::default().handle(invocation).await else {
panic!("empty message should be rejected");
};
assert_eq!(
@@ -221,7 +221,7 @@ async fn spawn_agent_rejects_when_message_and_items_are_both_set() {
"items": [{"type": "mention", "name": "drive", "path": "app://drive"}]
})),
);
let Err(err) = SpawnAgentHandler.handle(invocation).await else {
let Err(err) = SpawnAgentHandler::default().handle(invocation).await else {
panic!("message+items should be rejected");
};
assert_eq!(
@@ -268,7 +268,7 @@ async fn spawn_agent_uses_explorer_role_and_preserves_approval_policy() {
"agent_type": "explorer"
})),
);
let output = SpawnAgentHandler
let output = SpawnAgentHandler::default()
.handle(invocation)
.await
.expect("spawn_agent should succeed");
@@ -303,7 +303,7 @@ async fn spawn_agent_fork_context_rejects_agent_type_override() {
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
session.conversation_id = root.thread_id;
let err = SpawnAgentHandler
let err = SpawnAgentHandler::default()
.handle(invocation(
Arc::new(session),
Arc::new(turn),
@@ -336,7 +336,7 @@ async fn spawn_agent_fork_context_rejects_child_model_overrides() {
session.services.agent_control = manager.agent_control();
session.conversation_id = root.thread_id;
let err = SpawnAgentHandler
let err = SpawnAgentHandler::default()
.handle(invocation(
Arc::new(session),
Arc::new(turn),
@@ -380,7 +380,7 @@ async fn multi_agent_v2_spawn_fork_turns_all_rejects_agent_type_override() {
..turn
};
let err = SpawnAgentHandlerV2
let err = SpawnAgentHandlerV2::default()
.handle(invocation(
Arc::new(session),
Arc::new(turn),
@@ -420,7 +420,7 @@ async fn multi_agent_v2_spawn_defaults_to_full_fork_and_rejects_child_model_over
.expect("test config should allow feature update");
turn.config = Arc::new(config);
let err = SpawnAgentHandlerV2
let err = SpawnAgentHandlerV2::default()
.handle(invocation(
Arc::new(session),
Arc::new(turn),
@@ -464,7 +464,7 @@ async fn multi_agent_v2_spawn_partial_fork_turns_allows_agent_type_override() {
..turn
};
let output = SpawnAgentHandlerV2
let output = SpawnAgentHandlerV2::default()
.handle(invocation(
Arc::new(session),
Arc::new(turn),
@@ -506,7 +506,7 @@ async fn spawn_agent_returns_agent_id_without_task_name() {
let manager = thread_manager();
session.services.agent_control = manager.agent_control();
let output = SpawnAgentHandler
let output = SpawnAgentHandler::default()
.handle(invocation(
Arc::new(session),
Arc::new(turn),
@@ -552,7 +552,7 @@ async fn multi_agent_v2_spawn_requires_task_name() {
"message": "inspect this repo"
})),
);
let Err(err) = SpawnAgentHandlerV2.handle(invocation).await else {
let Err(err) = SpawnAgentHandlerV2::default().handle(invocation).await else {
panic!("missing task_name should be rejected");
};
let FunctionCallError::RespondToModel(message) = err else {
@@ -588,7 +588,7 @@ async fn multi_agent_v2_spawn_rejects_legacy_items_field() {
"task_name": "worker"
})),
);
let Err(err) = SpawnAgentHandlerV2.handle(invocation).await else {
let Err(err) = SpawnAgentHandlerV2::default().handle(invocation).await else {
panic!("legacy items field should be rejected");
};
let FunctionCallError::RespondToModel(message) = err else {
@@ -606,7 +606,7 @@ async fn spawn_agent_errors_when_manager_dropped() {
"spawn_agent",
function_payload(json!({"message": "hello"})),
);
let Err(err) = SpawnAgentHandler.handle(invocation).await else {
let Err(err) = SpawnAgentHandler::default().handle(invocation).await else {
panic!("spawn should fail without a manager");
};
assert_eq!(
@@ -640,7 +640,7 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
let session = Arc::new(session);
let turn = Arc::new(turn);
let spawn_output = SpawnAgentHandlerV2
let spawn_output = SpawnAgentHandlerV2::default()
.handle(invocation(
session.clone(),
turn.clone(),
@@ -735,7 +735,7 @@ async fn multi_agent_v2_spawn_rejects_legacy_fork_context() {
.expect("test config should allow feature update");
turn.config = Arc::new(config);
let err = SpawnAgentHandlerV2
let err = SpawnAgentHandlerV2::default()
.handle(invocation(
Arc::new(session),
Arc::new(turn),
@@ -774,7 +774,7 @@ async fn multi_agent_v2_spawn_rejects_invalid_fork_turns_string() {
.expect("test config should allow feature update");
turn.config = Arc::new(config);
let err = SpawnAgentHandlerV2
let err = SpawnAgentHandlerV2::default()
.handle(invocation(
Arc::new(session),
Arc::new(turn),
@@ -813,7 +813,7 @@ async fn multi_agent_v2_spawn_rejects_zero_fork_turns() {
.expect("test config should allow feature update");
turn.config = Arc::new(config);
let err = SpawnAgentHandlerV2
let err = SpawnAgentHandlerV2::default()
.handle(invocation(
Arc::new(session),
Arc::new(turn),
@@ -1008,7 +1008,7 @@ async fn multi_agent_v2_list_agents_returns_completed_status_and_last_task_messa
let session = Arc::new(session);
let turn = Arc::new(turn);
let spawn_output = SpawnAgentHandlerV2
let spawn_output = SpawnAgentHandlerV2::default()
.handle(invocation(
session.clone(),
turn.clone(),
@@ -1189,7 +1189,7 @@ async fn multi_agent_v2_list_agents_omits_closed_agents() {
let session = Arc::new(session);
let turn = Arc::new(turn);
let spawn_output = SpawnAgentHandlerV2
let spawn_output = SpawnAgentHandlerV2::default()
.handle(invocation(
session.clone(),
turn.clone(),
@@ -1253,7 +1253,7 @@ async fn multi_agent_v2_send_message_rejects_legacy_items_field() {
let session = Arc::new(session);
let turn = Arc::new(turn);
SpawnAgentHandlerV2
SpawnAgentHandlerV2::default()
.handle(invocation(
session.clone(),
turn.clone(),
@@ -1309,7 +1309,7 @@ async fn multi_agent_v2_send_message_rejects_interrupt_parameter() {
let session = Arc::new(session);
let turn = Arc::new(turn);
SpawnAgentHandlerV2
SpawnAgentHandlerV2::default()
.handle(invocation(
session.clone(),
turn.clone(),
@@ -1382,7 +1382,7 @@ async fn multi_agent_v2_followup_task_completion_notifies_parent_on_every_turn()
let session = Arc::new(session);
let turn = Arc::new(turn);
SpawnAgentHandlerV2
SpawnAgentHandlerV2::default()
.handle(invocation(
session.clone(),
turn.clone(),
@@ -1517,7 +1517,7 @@ async fn multi_agent_v2_followup_task_rejects_legacy_items_field() {
let session = Arc::new(session);
let turn = Arc::new(turn);
SpawnAgentHandlerV2
SpawnAgentHandlerV2::default()
.handle(invocation(
session.clone(),
turn.clone(),
@@ -1570,7 +1570,7 @@ async fn multi_agent_v2_interrupted_turn_does_not_notify_parent() {
let session = Arc::new(session);
let turn = Arc::new(turn);
SpawnAgentHandlerV2
SpawnAgentHandlerV2::default()
.handle(invocation(
session.clone(),
turn.clone(),
@@ -1648,7 +1648,7 @@ async fn multi_agent_v2_spawn_omits_agent_id_when_named() {
.expect("test config should allow feature update");
turn.config = Arc::new(config);
let output = SpawnAgentHandlerV2
let output = SpawnAgentHandlerV2::default()
.handle(invocation(
Arc::new(session),
Arc::new(turn),
@@ -1696,7 +1696,7 @@ async fn multi_agent_v2_spawn_surfaces_task_name_validation_errors() {
"task_name": "BadName"
})),
);
let Err(err) = SpawnAgentHandlerV2.handle(invocation).await else {
let Err(err) = SpawnAgentHandlerV2::default().handle(invocation).await else {
panic!("invalid agent name should be rejected");
};
assert_eq!(
@@ -1754,7 +1754,7 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() {
"agent_type": "explorer"
})),
);
let output = SpawnAgentHandler
let output = SpawnAgentHandler::default()
.handle(invocation)
.await
.expect("spawn_agent should succeed");
@@ -1815,7 +1815,7 @@ async fn spawn_agent_rejects_when_depth_limit_exceeded() {
"spawn_agent",
function_payload(json!({"message": "hello"})),
);
let Err(err) = SpawnAgentHandler.handle(invocation).await else {
let Err(err) = SpawnAgentHandler::default().handle(invocation).await else {
panic!("spawn should fail when depth limit exceeded");
};
assert_eq!(
@@ -1855,7 +1855,7 @@ async fn spawn_agent_allows_depth_up_to_configured_max_depth() {
"spawn_agent",
function_payload(json!({"message": "hello"})),
);
let output = SpawnAgentHandler
let output = SpawnAgentHandler::default()
.handle(invocation)
.await
.expect("spawn should succeed within configured depth");
@@ -1914,7 +1914,7 @@ async fn multi_agent_v2_spawn_agent_ignores_configured_max_depth() {
"fork_turns": "none"
})),
);
let output = SpawnAgentHandlerV2
let output = SpawnAgentHandlerV2::default()
.handle(invocation)
.await
.expect("multi-agent v2 spawn should ignore max depth");
@@ -2306,7 +2306,7 @@ async fn wait_agent_rejects_non_positive_timeout() {
"timeout_ms": 0
})),
);
let Err(err) = WaitAgentHandler.handle(invocation).await else {
let Err(err) = WaitAgentHandler::default().handle(invocation).await else {
panic!("non-positive timeout should be rejected");
};
assert_eq!(
@@ -2324,7 +2324,7 @@ async fn wait_agent_rejects_invalid_target() {
"wait_agent",
function_payload(json!({"targets": ["invalid"]})),
);
let Err(err) = WaitAgentHandler.handle(invocation).await else {
let Err(err) = WaitAgentHandler::default().handle(invocation).await else {
panic!("invalid id should be rejected");
};
let FunctionCallError::RespondToModel(msg) = err else {
@@ -2342,7 +2342,7 @@ async fn wait_agent_rejects_empty_targets() {
"wait_agent",
function_payload(json!({"targets": []})),
);
let Err(err) = WaitAgentHandler.handle(invocation).await else {
let Err(err) = WaitAgentHandler::default().handle(invocation).await else {
panic!("empty ids should be rejected");
};
assert_eq!(
@@ -2370,7 +2370,7 @@ async fn multi_agent_v2_wait_agent_accepts_timeout_only_argument() {
let session = Arc::new(session);
let turn = Arc::new(turn);
SpawnAgentHandlerV2
SpawnAgentHandlerV2::default()
.handle(invocation(
session.clone(),
turn.clone(),
@@ -2400,7 +2400,7 @@ async fn multi_agent_v2_wait_agent_accepts_timeout_only_argument() {
let session = session.clone();
let turn = turn.clone();
async move {
WaitAgentHandlerV2
WaitAgentHandlerV2::default()
.handle(invocation(
session,
turn,
@@ -2452,7 +2452,7 @@ async fn multi_agent_v2_wait_agent_uses_configured_min_timeout() {
let early = timeout(
Duration::from_millis(/*millis*/ 20),
WaitAgentHandlerV2.handle(invocation(
WaitAgentHandlerV2::default().handle(invocation(
session.clone(),
turn.clone(),
"wait_agent",
@@ -2467,7 +2467,7 @@ async fn multi_agent_v2_wait_agent_uses_configured_min_timeout() {
let output = timeout(
Duration::from_secs(/*secs*/ 1),
WaitAgentHandlerV2.handle(invocation(
WaitAgentHandlerV2::default().handle(invocation(
session,
turn,
"wait_agent",
@@ -2506,7 +2506,7 @@ async fn wait_agent_returns_not_found_for_missing_agents() {
"timeout_ms": 1000
})),
);
let output = WaitAgentHandler
let output = WaitAgentHandler::default()
.handle(invocation)
.await
.expect("wait_agent should succeed");
@@ -2546,7 +2546,7 @@ async fn wait_agent_times_out_when_status_is_not_final() {
"timeout_ms": MIN_WAIT_TIMEOUT_MS
})),
);
let output = WaitAgentHandler
let output = WaitAgentHandler::default()
.handle(invocation)
.await
.expect("wait_agent should succeed");
@@ -2592,7 +2592,7 @@ async fn wait_agent_clamps_short_timeouts_to_minimum() {
let early = timeout(
Duration::from_millis(50),
WaitAgentHandler.handle(invocation),
WaitAgentHandler::default().handle(invocation),
)
.await;
assert!(
@@ -2642,7 +2642,7 @@ async fn wait_agent_returns_final_status_without_timeout() {
"timeout_ms": 1000
})),
);
let output = WaitAgentHandler
let output = WaitAgentHandler::default()
.handle(invocation)
.await
.expect("wait_agent should succeed");
@@ -2678,7 +2678,7 @@ async fn multi_agent_v2_wait_agent_returns_summary_for_mailbox_activity() {
let session = Arc::new(session);
let turn = Arc::new(turn);
let spawn_output = SpawnAgentHandlerV2
let spawn_output = SpawnAgentHandlerV2::default()
.handle(invocation(
session.clone(),
turn.clone(),
@@ -2713,7 +2713,7 @@ async fn multi_agent_v2_wait_agent_returns_summary_for_mailbox_activity() {
let session = session.clone();
let turn = turn.clone();
async move {
WaitAgentHandlerV2
WaitAgentHandlerV2::default()
.handle(invocation(
session,
turn,
@@ -2769,7 +2769,7 @@ async fn multi_agent_v2_wait_agent_returns_for_already_queued_mail() {
let session = Arc::new(session);
let turn = Arc::new(turn);
SpawnAgentHandlerV2
SpawnAgentHandlerV2::default()
.handle(invocation(
session.clone(),
turn.clone(),
@@ -2805,7 +2805,7 @@ async fn multi_agent_v2_wait_agent_returns_for_already_queued_mail() {
let output = timeout(
Duration::from_millis(500),
WaitAgentHandlerV2.handle(invocation(
WaitAgentHandlerV2::default().handle(invocation(
session,
turn,
"wait_agent",
@@ -2848,7 +2848,7 @@ async fn multi_agent_v2_wait_agent_wakes_on_any_mailbox_notification() {
let turn = Arc::new(turn);
for task_name in ["worker_a", "worker_b"] {
SpawnAgentHandlerV2
SpawnAgentHandlerV2::default()
.handle(invocation(
session.clone(),
turn.clone(),
@@ -2879,7 +2879,7 @@ async fn multi_agent_v2_wait_agent_wakes_on_any_mailbox_notification() {
let session = session.clone();
let turn = turn.clone();
async move {
WaitAgentHandlerV2
WaitAgentHandlerV2::default()
.handle(invocation(
session,
turn,
@@ -2935,7 +2935,7 @@ async fn multi_agent_v2_wait_agent_does_not_return_completed_content() {
let session = Arc::new(session);
let turn = Arc::new(turn);
SpawnAgentHandlerV2
SpawnAgentHandlerV2::default()
.handle(invocation(
session.clone(),
turn.clone(),
@@ -2964,7 +2964,7 @@ async fn multi_agent_v2_wait_agent_does_not_return_completed_content() {
let session = session.clone();
let turn = turn.clone();
async move {
WaitAgentHandlerV2
WaitAgentHandlerV2::default()
.handle(invocation(
session,
turn,
@@ -3021,7 +3021,7 @@ async fn multi_agent_v2_close_agent_accepts_task_name_target() {
let session = Arc::new(session);
let turn = Arc::new(turn);
SpawnAgentHandlerV2
SpawnAgentHandlerV2::default()
.handle(invocation(
session.clone(),
turn.clone(),
@@ -3176,7 +3176,7 @@ async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtr
let parent_thread_id = parent.thread_id;
let parent_session = parent.thread.codex.session.clone();
let child_spawn_output = SpawnAgentHandler
let child_spawn_output = SpawnAgentHandler::default()
.handle(invocation(
parent_session.clone(),
parent_session.new_default_turn().await,
@@ -3201,7 +3201,7 @@ async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtr
.await
.expect("child thread should exist");
let child_session = child_thread.codex.session.clone();
let grandchild_spawn_output = SpawnAgentHandler
let grandchild_spawn_output = SpawnAgentHandler::default()
.handle(invocation(
child_session.clone(),
child_session.new_default_turn().await,

View File

@@ -1,5 +1,7 @@
use super::*;
use crate::tools::handlers::multi_agents_spec::create_close_agent_tool_v2;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_tools::ToolSpec;
pub(crate) struct Handler;
@@ -10,6 +12,10 @@ impl ToolHandler for Handler {
ToolName::plain("close_agent")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_close_agent_tool_v2())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -3,6 +3,8 @@ use super::message_tool::MessageDeliveryMode;
use super::message_tool::handle_message_string_tool;
use super::*;
use crate::tools::context::FunctionToolOutput;
use crate::tools::handlers::multi_agents_spec::create_followup_task_tool;
use codex_tools::ToolSpec;
pub(crate) struct Handler;
@@ -13,6 +15,10 @@ impl ToolHandler for Handler {
ToolName::plain("followup_task")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_followup_task_tool())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -1,5 +1,7 @@
use super::*;
use crate::agent::control::ListedAgent;
use crate::tools::handlers::multi_agents_spec::create_list_agents_tool;
use codex_tools::ToolSpec;
pub(crate) struct Handler;
@@ -10,6 +12,10 @@ impl ToolHandler for Handler {
ToolName::plain("list_agents")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_list_agents_tool())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -3,6 +3,8 @@ use super::message_tool::SendMessageArgs;
use super::message_tool::handle_message_string_tool;
use super::*;
use crate::tools::context::FunctionToolOutput;
use crate::tools::handlers::multi_agents_spec::create_send_message_tool;
use codex_tools::ToolSpec;
pub(crate) struct Handler;
@@ -13,6 +15,10 @@ impl ToolHandler for Handler {
ToolName::plain("send_message")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_send_message_tool())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -5,12 +5,24 @@ use crate::agent::control::render_input_preview;
use crate::agent::next_thread_spawn_depth;
use crate::agent::role::DEFAULT_ROLE_NAME;
use crate::agent::role::apply_role_to_config;
use crate::tools::handlers::multi_agents_spec::SpawnAgentToolOptions;
use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v2;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_protocol::AgentPath;
use codex_protocol::protocol::InterAgentCommunication;
use codex_protocol::protocol::Op;
use codex_tools::ToolSpec;
pub(crate) struct Handler;
#[derive(Default)]
pub(crate) struct Handler {
options: SpawnAgentToolOptions,
}
impl Handler {
pub(crate) fn new(options: SpawnAgentToolOptions) -> Self {
Self { options }
}
}
impl ToolHandler for Handler {
type Output = SpawnAgentResult;
@@ -19,6 +31,10 @@ impl ToolHandler for Handler {
ToolName::plain("spawn_agent")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_spawn_agent_tool_v2(self.options.clone()))
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -1,11 +1,23 @@
use super::*;
use crate::tools::handlers::multi_agents_spec::WaitAgentTimeoutOptions;
use crate::tools::handlers::multi_agents_spec::create_wait_agent_tool_v2;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_tools::ToolSpec;
use std::collections::HashMap;
use std::time::Duration;
use tokio::time::Instant;
use tokio::time::timeout_at;
pub(crate) struct Handler;
#[derive(Default)]
pub(crate) struct Handler {
options: WaitAgentTimeoutOptions,
}
impl Handler {
pub(crate) fn new(options: WaitAgentTimeoutOptions) -> Self {
Self { options }
}
}
impl ToolHandler for Handler {
type Output = WaitAgentResult;
@@ -14,6 +26,10 @@ impl ToolHandler for Handler {
ToolName::plain("wait_agent")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_wait_agent_tool_v2(self.options))
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -2,6 +2,7 @@ use crate::function_tool::FunctionCallError;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::plan_spec::create_update_plan_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_protocol::config_types::ModeKind;
@@ -10,6 +11,7 @@ use codex_protocol::models::ResponseInputItem;
use codex_protocol::plan_tool::UpdatePlanArgs;
use codex_protocol::protocol::EventMsg;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use serde_json::Value as JsonValue;
pub struct PlanHandler;
@@ -49,6 +51,10 @@ impl ToolHandler for PlanHandler {
ToolName::plain("update_plan")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_update_plan_tool())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -6,9 +6,12 @@ use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments_with_base_path;
use crate::tools::handlers::shell_spec::create_request_permissions_tool;
use crate::tools::handlers::shell_spec::request_permissions_tool_description;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
pub struct RequestPermissionsHandler;
@@ -19,6 +22,12 @@ impl ToolHandler for RequestPermissionsHandler {
ToolName::plain("request_permissions")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_request_permissions_tool(
request_permissions_tool_description(),
))
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -12,10 +12,13 @@ use codex_tools::REQUEST_PLUGIN_INSTALL_PERSIST_ALWAYS_VALUE;
use codex_tools::REQUEST_PLUGIN_INSTALL_PERSIST_KEY;
use codex_tools::REQUEST_PLUGIN_INSTALL_TOOL_NAME;
use codex_tools::RequestPluginInstallArgs;
use codex_tools::RequestPluginInstallEntry;
use codex_tools::RequestPluginInstallResult;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use codex_tools::all_requested_connectors_picked_up;
use codex_tools::build_request_plugin_install_elicitation_request;
use codex_tools::collect_request_plugin_install_entries;
use codex_tools::filter_request_plugin_install_discoverable_tools_for_client;
use codex_tools::verified_connector_install_completed;
use rmcp::model::RequestId;
@@ -30,10 +33,22 @@ use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::handlers::request_plugin_install_spec::create_request_plugin_install_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
pub struct RequestPluginInstallHandler;
#[derive(Default)]
pub struct RequestPluginInstallHandler {
discoverable_tools: Vec<RequestPluginInstallEntry>,
}
impl RequestPluginInstallHandler {
pub(crate) fn new(discoverable_tools: &[DiscoverableTool]) -> Self {
Self {
discoverable_tools: collect_request_plugin_install_entries(discoverable_tools),
}
}
}
impl ToolHandler for RequestPluginInstallHandler {
type Output = FunctionToolOutput;
@@ -42,6 +57,14 @@ impl ToolHandler for RequestPluginInstallHandler {
ToolName::plain(REQUEST_PLUGIN_INSTALL_TOOL_NAME)
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_request_plugin_install_tool(&self.discoverable_tools))
}
fn supports_parallel_tool_calls(&self) -> bool {
true
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -4,13 +4,16 @@ use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::handlers::request_user_input_spec::REQUEST_USER_INPUT_TOOL_NAME;
use crate::tools::handlers::request_user_input_spec::create_request_user_input_tool;
use crate::tools::handlers::request_user_input_spec::normalize_request_user_input_args;
use crate::tools::handlers::request_user_input_spec::request_user_input_tool_description;
use crate::tools::handlers::request_user_input_spec::request_user_input_unavailable_message;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_protocol::config_types::ModeKind;
use codex_protocol::request_user_input::RequestUserInputArgs;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
pub struct RequestUserInputHandler {
pub available_modes: Vec<ModeKind>,
@@ -23,6 +26,12 @@ impl ToolHandler for RequestUserInputHandler {
ToolName::plain(REQUEST_USER_INPUT_TOOL_NAME)
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_request_user_input_tool(
request_user_input_tool_description(&self.available_modes),
))
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -38,6 +38,7 @@ mod shell_handler;
pub use container_exec::ContainerExecHandler;
pub use local_shell::LocalShellHandler;
pub use shell_command::ShellCommandHandler;
pub(crate) use shell_command::ShellCommandHandlerOptions;
pub use shell_handler::ShellHandler;
fn shell_function_payload_command(payload: &ToolPayload) -> Option<String> {

View File

@@ -12,13 +12,24 @@ use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use crate::tools::runtimes::shell::ShellRuntimeBackend;
use codex_tools::ToolSpec;
use super::super::shell_spec::create_local_shell_tool;
use super::RunExecLikeArgs;
use super::local_shell_payload_command;
use super::run_exec_like;
use super::shell_handler::ShellHandler;
pub struct LocalShellHandler;
#[derive(Default)]
pub struct LocalShellHandler {
include_spec: bool,
}
impl LocalShellHandler {
pub(crate) fn new() -> Self {
Self { include_spec: true }
}
}
impl ToolHandler for LocalShellHandler {
type Output = FunctionToolOutput;
@@ -27,6 +38,14 @@ impl ToolHandler for LocalShellHandler {
ToolName::plain("local_shell")
}
fn spec(&self) -> Option<ToolSpec> {
self.include_spec.then(create_local_shell_tool)
}
fn supports_parallel_tool_calls(&self) -> bool {
self.include_spec
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -23,7 +23,10 @@ use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use crate::tools::runtimes::shell::ShellRuntimeBackend;
use codex_tools::ToolSpec;
use super::super::shell_spec::CommandToolOptions;
use super::super::shell_spec::create_shell_command_tool;
use super::RunExecLikeArgs;
use super::run_exec_like;
use super::shell_command_payload_command;
@@ -36,9 +39,24 @@ enum ShellCommandBackend {
pub struct ShellCommandHandler {
backend: ShellCommandBackend,
options: Option<ShellCommandHandlerOptions>,
}
#[derive(Clone, Copy)]
pub(crate) struct ShellCommandHandlerOptions {
pub(crate) backend_config: ShellCommandBackendConfig,
pub(crate) allow_login_shell: bool,
pub(crate) exec_permission_approvals_enabled: bool,
}
impl ShellCommandHandler {
pub(crate) fn new(options: ShellCommandHandlerOptions) -> Self {
Self {
options: Some(options),
..Self::from(options.backend_config)
}
}
fn shell_runtime_backend(&self) -> ShellRuntimeBackend {
match self.backend {
ShellCommandBackend::Classic => ShellRuntimeBackend::ShellCommandClassic,
@@ -99,7 +117,10 @@ impl From<ShellCommandBackendConfig> for ShellCommandHandler {
ShellCommandBackendConfig::Classic => ShellCommandBackend::Classic,
ShellCommandBackendConfig::ZshFork => ShellCommandBackend::ZshFork,
};
Self { backend }
Self {
backend,
options: None,
}
}
}
@@ -110,6 +131,19 @@ impl ToolHandler for ShellCommandHandler {
ToolName::plain("shell_command")
}
fn spec(&self) -> Option<ToolSpec> {
self.options.map(|options| {
create_shell_command_tool(CommandToolOptions {
allow_login_shell: options.allow_login_shell,
exec_permission_approvals_enabled: options.exec_permission_approvals_enabled,
})
})
}
fn supports_parallel_tool_calls(&self) -> bool {
self.options.is_some()
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -18,15 +18,27 @@ use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use crate::tools::runtimes::shell::ShellRuntimeBackend;
use codex_tools::ToolSpec;
use super::super::shell_spec::ShellToolOptions;
use super::super::shell_spec::create_shell_tool;
use super::RunExecLikeArgs;
use super::run_exec_like;
use super::shell_function_post_tool_use_payload;
use super::shell_function_pre_tool_use_payload;
pub struct ShellHandler;
#[derive(Default)]
pub struct ShellHandler {
options: Option<ShellToolOptions>,
}
impl ShellHandler {
pub(crate) fn new(options: ShellToolOptions) -> Self {
Self {
options: Some(options),
}
}
pub(super) fn to_exec_params(
params: &ShellToolCallParams,
turn_context: &TurnContext,
@@ -58,6 +70,14 @@ impl ToolHandler for ShellHandler {
ToolName::plain("shell")
}
fn spec(&self) -> Option<ToolSpec> {
self.options.map(create_shell_tool)
}
fn supports_parallel_tool_calls(&self) -> bool {
self.options.is_some()
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -221,7 +221,7 @@ async fn local_shell_pre_tool_use_payload_uses_joined_command() {
},
};
let (session, turn) = make_session_and_context().await;
let handler = LocalShellHandler;
let handler = LocalShellHandler::default();
assert_eq!(
handler.pre_tool_use_payload(&ToolInvocation {

View File

@@ -13,9 +13,11 @@ use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::handlers::test_sync_spec::create_test_sync_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
pub struct TestSyncHandler;
@@ -61,6 +63,14 @@ impl ToolHandler for TestSyncHandler {
ToolName::plain("test_sync_tool")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_test_sync_tool())
}
fn supports_parallel_tool_calls(&self) -> bool {
true
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -2,6 +2,7 @@ use crate::function_tool::FunctionCallError;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::context::ToolSearchOutput;
use crate::tools::handlers::tool_search_spec::create_tool_search_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use crate::tools::tool_search_entry::ToolSearchEntry;
@@ -13,6 +14,8 @@ use codex_tools::LoadableToolSpec;
use codex_tools::TOOL_SEARCH_DEFAULT_LIMIT;
use codex_tools::TOOL_SEARCH_TOOL_NAME;
use codex_tools::ToolName;
use codex_tools::ToolSearchSourceInfo;
use codex_tools::ToolSpec;
use codex_tools::coalesce_loadable_tool_specs;
use std::collections::HashMap;
@@ -21,11 +24,15 @@ const COMPUTER_USE_TOOL_SEARCH_LIMIT: usize = 20;
pub struct ToolSearchHandler {
entries: Vec<ToolSearchEntry>,
search_source_infos: Vec<ToolSearchSourceInfo>,
search_engine: SearchEngine<usize>,
}
impl ToolSearchHandler {
pub(crate) fn new(entries: Vec<ToolSearchEntry>) -> Self {
pub(crate) fn new(
entries: Vec<ToolSearchEntry>,
search_source_infos: Vec<ToolSearchSourceInfo>,
) -> Self {
let documents: Vec<Document<usize>> = entries
.iter()
.map(|entry| entry.search_text.clone())
@@ -37,6 +44,7 @@ impl ToolSearchHandler {
Self {
entries,
search_source_infos,
search_engine,
}
}
@@ -49,6 +57,17 @@ impl ToolHandler for ToolSearchHandler {
ToolName::plain(TOOL_SEARCH_TOOL_NAME)
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_tool_search_tool(
&self.search_source_infos,
TOOL_SEARCH_DEFAULT_LIMIT,
))
}
fn supports_parallel_tool_calls(&self) -> bool {
true
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}
@@ -415,6 +434,9 @@ mod tests {
mcp_tools: Option<&[ToolInfo]>,
dynamic_tools: &[DynamicToolSpec],
) -> ToolSearchHandler {
ToolSearchHandler::new(build_tool_search_entries(mcp_tools, dynamic_tools))
ToolSearchHandler::new(
build_tool_search_entries(mcp_tools, dynamic_tools),
Vec::new(),
)
}
}

View File

@@ -5,14 +5,26 @@ use crate::tools::context::ToolPayload;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
pub struct UnavailableToolHandler {
tool_name: ToolName,
spec: Option<ToolSpec>,
}
impl UnavailableToolHandler {
pub fn new(tool_name: ToolName) -> Self {
Self { tool_name }
pub fn new(tool_name: ToolName, spec: ToolSpec) -> Self {
Self {
tool_name,
spec: Some(spec),
}
}
pub fn without_spec(tool_name: ToolName) -> Self {
Self {
tool_name,
spec: None,
}
}
}
@@ -32,6 +44,10 @@ impl ToolHandler for UnavailableToolHandler {
self.tool_name.clone()
}
fn spec(&self) -> Option<ToolSpec> {
self.spec.clone()
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -22,6 +22,7 @@ mod exec_command;
mod write_stdin;
pub use exec_command::ExecCommandHandler;
pub(crate) use exec_command::ExecCommandHandlerOptions;
pub use write_stdin::WriteStdinHandler;
#[derive(Debug, Deserialize)]

View File

@@ -27,15 +27,45 @@ use codex_otel::SessionTelemetry;
use codex_otel::TOOL_CALL_UNIFIED_EXEC_METRIC;
use codex_shell_command::is_safe_command::is_known_safe_command;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use codex_utils_output_truncation::approx_token_count;
use super::super::shell_spec::CommandToolOptions;
use super::super::shell_spec::create_exec_command_tool_with_environment_id;
use super::ExecCommandArgs;
use super::ExecCommandEnvironmentArgs;
use super::effective_max_output_tokens;
use super::get_command;
use super::post_unified_exec_tool_use_payload;
pub struct ExecCommandHandler;
#[derive(Clone, Copy)]
pub(crate) struct ExecCommandHandlerOptions {
pub(crate) allow_login_shell: bool,
pub(crate) exec_permission_approvals_enabled: bool,
pub(crate) include_environment_id: bool,
}
pub struct ExecCommandHandler {
options: ExecCommandHandlerOptions,
}
impl Default for ExecCommandHandler {
fn default() -> Self {
Self {
options: ExecCommandHandlerOptions {
allow_login_shell: false,
exec_permission_approvals_enabled: false,
include_environment_id: false,
},
}
}
}
impl ExecCommandHandler {
pub(crate) fn new(options: ExecCommandHandlerOptions) -> Self {
Self { options }
}
}
impl ToolHandler for ExecCommandHandler {
type Output = ExecCommandToolOutput;
@@ -44,6 +74,20 @@ impl ToolHandler for ExecCommandHandler {
ToolName::plain("exec_command")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_exec_command_tool_with_environment_id(
CommandToolOptions {
allow_login_shell: self.options.allow_login_shell,
exec_permission_approvals_enabled: self.options.exec_permission_approvals_enabled,
},
self.options.include_environment_id,
))
}
fn supports_parallel_tool_calls(&self) -> bool {
true
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -10,8 +10,10 @@ use crate::unified_exec::WriteStdinRequest;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::TerminalInteractionEvent;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use serde::Deserialize;
use super::super::shell_spec::create_write_stdin_tool;
use super::effective_max_output_tokens;
use super::post_unified_exec_tool_use_payload;
@@ -36,6 +38,10 @@ impl ToolHandler for WriteStdinHandler {
ToolName::plain("write_stdin")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_write_stdin_tool())
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -184,7 +184,7 @@ async fn exec_command_pre_tool_use_payload_uses_raw_command() {
arguments: serde_json::json!({ "cmd": "printf exec command" }).to_string(),
};
let (session, turn) = make_session_and_context().await;
let handler = ExecCommandHandler;
let handler = ExecCommandHandler::default();
assert_eq!(
handler.pre_tool_use_payload(&ToolInvocation {
@@ -244,7 +244,7 @@ async fn exec_command_post_tool_use_payload_uses_output_for_noninteractive_one_s
hook_command: Some("echo three".to_string()),
};
let invocation = invocation_for_payload("exec_command", "call-43", payload).await;
let handler = ExecCommandHandler;
let handler = ExecCommandHandler::default();
assert_eq!(
handler.post_tool_use_payload(&invocation, &output),
Some(crate::tools::registry::PostToolUsePayload {
@@ -273,7 +273,7 @@ async fn exec_command_post_tool_use_payload_uses_output_for_interactive_completi
hook_command: Some("echo three".to_string()),
};
let invocation = invocation_for_payload("exec_command", "call-44", payload).await;
let handler = ExecCommandHandler;
let handler = ExecCommandHandler::default();
assert_eq!(
handler.post_tool_use_payload(&invocation, &output),
@@ -303,7 +303,7 @@ async fn exec_command_post_tool_use_payload_skips_running_sessions() {
hook_command: Some("echo three".to_string()),
};
let invocation = invocation_for_payload("exec_command", "call-45", payload).await;
let handler = ExecCommandHandler;
let handler = ExecCommandHandler::default();
assert_eq!(handler.post_tool_use_payload(&invocation, &output), None);
}

View File

@@ -17,11 +17,32 @@ use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::handlers::view_image_spec::ViewImageToolOptions;
use crate::tools::handlers::view_image_spec::create_view_image_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
pub struct ViewImageHandler;
pub struct ViewImageHandler {
options: ViewImageToolOptions,
}
impl Default for ViewImageHandler {
fn default() -> Self {
Self {
options: ViewImageToolOptions {
can_request_original_image_detail: false,
},
}
}
}
impl ViewImageHandler {
pub(crate) fn new(options: ViewImageToolOptions) -> Self {
Self { options }
}
}
const VIEW_IMAGE_UNSUPPORTED_MESSAGE: &str =
"view_image is not allowed because you do not support image inputs";
@@ -44,6 +65,14 @@ impl ToolHandler for ViewImageHandler {
ToolName::plain("view_image")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_view_image_tool(self.options))
}
fn supports_parallel_tool_calls(&self) -> bool {
true
}
fn kind(&self) -> ToolKind {
ToolKind::Function
}

View File

@@ -18,6 +18,7 @@ use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
use crate::tools::hook_names::HookToolName;
use crate::tools::tool_dispatch_trace::ToolDispatchTrace;
use crate::util::error_or_panic;
use codex_hooks::HookEvent;
use codex_hooks::HookEventAfterToolUse;
use codex_hooks::HookPayload;
@@ -47,6 +48,14 @@ pub trait ToolHandler: Send + Sync {
/// The concrete tool name handled by this handler instance.
fn tool_name(&self) -> ToolName;
fn spec(&self) -> Option<ToolSpec> {
None
}
fn supports_parallel_tool_calls(&self) -> bool {
false
}
fn kind(&self) -> ToolKind;
fn matches_kind(&self, payload: &ToolPayload) -> bool {
@@ -512,37 +521,26 @@ impl ToolRegistry {
pub struct ToolRegistryBuilder {
handlers: HashMap<ToolName, Arc<dyn AnyToolHandler>>,
specs: Vec<ConfiguredToolSpec>,
code_mode_enabled: bool,
}
impl ToolRegistryBuilder {
pub fn new() -> Self {
pub fn new(code_mode_enabled: bool) -> Self {
Self {
handlers: HashMap::new(),
specs: Vec::new(),
code_mode_enabled,
}
}
pub fn push_spec_with_parallel_support(
&mut self,
spec: ToolSpec,
supports_parallel_tool_calls: bool,
) {
self.specs
.push(ConfiguredToolSpec::new(spec, supports_parallel_tool_calls));
}
pub(crate) fn push_spec(
&mut self,
spec: ToolSpec,
supports_parallel_tool_calls: bool,
code_mode_enabled: bool,
) {
let spec = if code_mode_enabled {
pub(crate) fn push_spec(&mut self, spec: ToolSpec, supports_parallel_tool_calls: bool) {
let spec = if self.code_mode_enabled {
codex_tools::augment_tool_spec_for_code_mode(spec)
} else {
spec
};
self.push_spec_with_parallel_support(spec, supports_parallel_tool_calls);
self.specs
.push(ConfiguredToolSpec::new(spec, supports_parallel_tool_calls));
}
pub fn register_handler<H>(&mut self, handler: Arc<H>)
@@ -550,11 +548,18 @@ impl ToolRegistryBuilder {
H: ToolHandler + 'static,
{
let name = handler.tool_name();
let display_name = name.display();
let handler: Arc<dyn AnyToolHandler> = handler;
if self.handlers.insert(name, handler).is_some() {
warn!("overwriting handler for tool {display_name}");
if self.handlers.contains_key(&name) {
error_or_panic(format!("handler for tool {name} already registered"));
return;
}
if let Some(spec) = handler.spec() {
let supports_parallel_tool_calls = handler.supports_parallel_tool_calls();
self.push_spec(spec, supports_parallel_tool_calls);
}
let handler: Arc<dyn AnyToolHandler> = handler;
self.handlers.insert(name, handler);
}
pub(crate) fn specs(&self) -> &[ConfiguredToolSpec] {

View File

@@ -1,4 +1,7 @@
use super::*;
use crate::tools::handlers::GetGoalHandler;
use crate::tools::handlers::goal_spec::GET_GOAL_TOOL_NAME;
use crate::tools::handlers::goal_spec::create_get_goal_tool;
use pretty_assertions::assert_eq;
struct TestHandler {
@@ -62,3 +65,18 @@ fn handler_looks_up_namespaced_aliases_explicitly() {
.is_some_and(|handler| Arc::ptr_eq(handler, &namespaced_handler))
);
}
#[test]
fn register_handler_adds_handler_and_augments_specs_for_code_mode() {
let mut builder = ToolRegistryBuilder::new(/*code_mode_enabled*/ true);
builder.register_handler(Arc::new(GetGoalHandler));
let (specs, registry) = builder.build();
assert_eq!(specs.len(), 1);
assert_eq!(
specs[0].spec,
codex_tools::augment_tool_spec_for_code_mode(create_get_goal_tool())
);
assert!(registry.has_handler(&codex_tools::ToolName::plain(GET_GOAL_TOOL_NAME)));
}

View File

@@ -153,13 +153,15 @@ pub(crate) fn build_specs_with_discoverable_tools(
output_schema: None,
defer_loading: None,
});
builder.push_spec(
builder.register_handler(Arc::new(UnavailableToolHandler::new(
unavailable_tool,
spec,
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
)));
} else {
builder.register_handler(Arc::new(UnavailableToolHandler::without_spec(
unavailable_tool,
)));
}
builder.register_handler(Arc::new(UnavailableToolHandler::new(unavailable_tool)));
}
builder
}

View File

@@ -1,5 +1,4 @@
use crate::tools::code_mode::execute_spec::create_code_mode_tool;
use crate::tools::code_mode::wait_spec::create_wait_tool;
use crate::tools::handlers::ApplyPatchHandler;
use crate::tools::handlers::CodeModeExecuteHandler;
use crate::tools::handlers::CodeModeWaitHandler;
@@ -7,6 +6,7 @@ use crate::tools::handlers::ContainerExecHandler;
use crate::tools::handlers::CreateGoalHandler;
use crate::tools::handlers::DynamicToolHandler;
use crate::tools::handlers::ExecCommandHandler;
use crate::tools::handlers::ExecCommandHandlerOptions;
use crate::tools::handlers::GetGoalHandler;
use crate::tools::handlers::ListMcpResourceTemplatesHandler;
use crate::tools::handlers::ListMcpResourcesHandler;
@@ -18,6 +18,7 @@ use crate::tools::handlers::RequestPermissionsHandler;
use crate::tools::handlers::RequestPluginInstallHandler;
use crate::tools::handlers::RequestUserInputHandler;
use crate::tools::handlers::ShellCommandHandler;
use crate::tools::handlers::ShellCommandHandlerOptions;
use crate::tools::handlers::ShellHandler;
use crate::tools::handlers::TestSyncHandler;
use crate::tools::handlers::ToolSearchHandler;
@@ -26,67 +27,29 @@ use crate::tools::handlers::ViewImageHandler;
use crate::tools::handlers::WriteStdinHandler;
use crate::tools::handlers::agent_jobs::ReportAgentJobResultHandler;
use crate::tools::handlers::agent_jobs::SpawnAgentsOnCsvHandler;
use crate::tools::handlers::agent_jobs_spec::create_report_agent_job_result_tool;
use crate::tools::handlers::agent_jobs_spec::create_spawn_agents_on_csv_tool;
use crate::tools::handlers::apply_patch_spec::create_apply_patch_freeform_tool;
use crate::tools::handlers::apply_patch_spec::create_apply_patch_json_tool;
use crate::tools::handlers::goal_spec::create_create_goal_tool;
use crate::tools::handlers::goal_spec::create_get_goal_tool;
use crate::tools::handlers::goal_spec::create_update_goal_tool;
use crate::tools::handlers::mcp_resource_spec::create_list_mcp_resource_templates_tool;
use crate::tools::handlers::mcp_resource_spec::create_list_mcp_resources_tool;
use crate::tools::handlers::mcp_resource_spec::create_read_mcp_resource_tool;
use crate::tools::handlers::multi_agents::CloseAgentHandler;
use crate::tools::handlers::multi_agents::ResumeAgentHandler;
use crate::tools::handlers::multi_agents::SendInputHandler;
use crate::tools::handlers::multi_agents::SpawnAgentHandler;
use crate::tools::handlers::multi_agents::WaitAgentHandler;
use crate::tools::handlers::multi_agents_spec::SpawnAgentToolOptions;
use crate::tools::handlers::multi_agents_spec::create_close_agent_tool_v1;
use crate::tools::handlers::multi_agents_spec::create_close_agent_tool_v2;
use crate::tools::handlers::multi_agents_spec::create_followup_task_tool;
use crate::tools::handlers::multi_agents_spec::create_list_agents_tool;
use crate::tools::handlers::multi_agents_spec::create_resume_agent_tool;
use crate::tools::handlers::multi_agents_spec::create_send_input_tool_v1;
use crate::tools::handlers::multi_agents_spec::create_send_message_tool;
use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v1;
use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v2;
use crate::tools::handlers::multi_agents_spec::create_wait_agent_tool_v1;
use crate::tools::handlers::multi_agents_spec::create_wait_agent_tool_v2;
use crate::tools::handlers::multi_agents_v2::CloseAgentHandler as CloseAgentHandlerV2;
use crate::tools::handlers::multi_agents_v2::FollowupTaskHandler as FollowupTaskHandlerV2;
use crate::tools::handlers::multi_agents_v2::ListAgentsHandler as ListAgentsHandlerV2;
use crate::tools::handlers::multi_agents_v2::SendMessageHandler as SendMessageHandlerV2;
use crate::tools::handlers::multi_agents_v2::SpawnAgentHandler as SpawnAgentHandlerV2;
use crate::tools::handlers::multi_agents_v2::WaitAgentHandler as WaitAgentHandlerV2;
use crate::tools::handlers::plan_spec::create_update_plan_tool;
use crate::tools::handlers::request_plugin_install_spec::create_request_plugin_install_tool;
use crate::tools::handlers::request_user_input_spec::create_request_user_input_tool;
use crate::tools::handlers::request_user_input_spec::request_user_input_tool_description;
use crate::tools::handlers::shell_spec::CommandToolOptions;
use crate::tools::handlers::shell_spec::ShellToolOptions;
use crate::tools::handlers::shell_spec::create_exec_command_tool_with_environment_id;
use crate::tools::handlers::shell_spec::create_local_shell_tool;
use crate::tools::handlers::shell_spec::create_request_permissions_tool;
use crate::tools::handlers::shell_spec::create_shell_command_tool;
use crate::tools::handlers::shell_spec::create_shell_tool;
use crate::tools::handlers::shell_spec::create_write_stdin_tool;
use crate::tools::handlers::shell_spec::request_permissions_tool_description;
use crate::tools::handlers::test_sync_spec::create_test_sync_tool;
use crate::tools::handlers::tool_search_spec::create_tool_search_tool;
use crate::tools::handlers::view_image_spec::ViewImageToolOptions;
use crate::tools::handlers::view_image_spec::create_view_image_tool;
use crate::tools::hosted_spec::WebSearchToolOptions;
use crate::tools::hosted_spec::create_image_generation_tool;
use crate::tools::hosted_spec::create_web_search_tool;
use crate::tools::registry::ToolRegistryBuilder;
use crate::tools::spec_plan_types::ToolRegistryBuildParams;
use crate::tools::spec_plan_types::agent_type_description;
use codex_protocol::openai_models::ApplyPatchToolType;
use codex_protocol::openai_models::ConfigShellToolType;
use codex_tools::ResponsesApiNamespace;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::TOOL_SEARCH_DEFAULT_LIMIT;
use codex_tools::ToolEnvironmentMode;
use codex_tools::ToolName;
use codex_tools::ToolSearchSource;
@@ -95,7 +58,6 @@ use codex_tools::ToolSpec;
use codex_tools::ToolsConfig;
use codex_tools::coalesce_loadable_tool_specs;
use codex_tools::collect_code_mode_exec_prompt_tool_definitions;
use codex_tools::collect_request_plugin_install_entries;
use codex_tools::collect_tool_search_source_infos;
use codex_tools::default_namespace_description;
use codex_tools::dynamic_tool_to_loadable_tool_spec;
@@ -108,7 +70,7 @@ pub fn build_tool_registry_builder(
config: &ToolsConfig,
params: ToolRegistryBuildParams<'_>,
) -> ToolRegistryBuilder {
let mut builder = ToolRegistryBuilder::new();
let mut builder = ToolRegistryBuilder::new(config.code_mode_enabled);
let exec_permission_approvals_enabled = config.exec_permission_approvals_enabled;
if config.code_mode_enabled {
@@ -142,7 +104,7 @@ pub fn build_tool_registry_builder(
);
enabled_tools
.sort_by(|left, right| compare_code_mode_tools(left, right, &namespace_descriptions));
builder.push_spec(
builder.register_handler(Arc::new(CodeModeExecuteHandler::new(
create_code_mode_tool(
&enabled_tools,
&namespace_descriptions,
@@ -152,15 +114,7 @@ pub fn build_tool_registry_builder(
.deferred_mcp_tools
.is_some_and(|tools| !tools.is_empty()),
),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
builder.register_handler(Arc::new(CodeModeExecuteHandler));
builder.push_spec(
create_wait_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
)));
builder.register_handler(Arc::new(CodeModeWaitHandler));
}
@@ -169,51 +123,32 @@ pub fn build_tool_registry_builder(
matches!(config.environment_mode, ToolEnvironmentMode::Multiple);
match &config.shell_type {
ConfigShellToolType::Default => {
builder.push_spec(
create_shell_tool(ShellToolOptions {
exec_permission_approvals_enabled,
}),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
builder.register_handler(Arc::new(ShellHandler::new(ShellToolOptions {
exec_permission_approvals_enabled,
})));
}
ConfigShellToolType::Local => {
builder.push_spec(
create_local_shell_tool(),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
builder.register_handler(Arc::new(LocalShellHandler::new()));
}
ConfigShellToolType::UnifiedExec => {
builder.push_spec(
create_exec_command_tool_with_environment_id(
CommandToolOptions {
allow_login_shell: config.allow_login_shell,
exec_permission_approvals_enabled,
},
builder.register_handler(Arc::new(ExecCommandHandler::new(
ExecCommandHandlerOptions {
allow_login_shell: config.allow_login_shell,
exec_permission_approvals_enabled,
include_environment_id,
),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
builder.push_spec(
create_write_stdin_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
builder.register_handler(Arc::new(ExecCommandHandler));
},
)));
builder.register_handler(Arc::new(WriteStdinHandler));
}
ConfigShellToolType::Disabled => {}
ConfigShellToolType::ShellCommand => {
builder.push_spec(
create_shell_command_tool(CommandToolOptions {
builder.register_handler(Arc::new(ShellCommandHandler::new(
ShellCommandHandlerOptions {
backend_config: config.shell_command_backend,
allow_login_shell: config.allow_login_shell,
exec_permission_approvals_enabled,
}),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
},
)));
}
}
}
@@ -221,79 +156,56 @@ pub fn build_tool_registry_builder(
if config.environment_mode.has_environment()
&& config.shell_type != ConfigShellToolType::Disabled
{
builder.register_handler(Arc::new(ShellHandler));
builder.register_handler(Arc::new(ContainerExecHandler));
builder.register_handler(Arc::new(LocalShellHandler));
builder.register_handler(Arc::new(ShellCommandHandler::from(
config.shell_command_backend,
)));
match &config.shell_type {
ConfigShellToolType::Default => {
builder.register_handler(Arc::new(ContainerExecHandler));
builder.register_handler(Arc::new(LocalShellHandler::default()));
builder.register_handler(Arc::new(ShellCommandHandler::from(
config.shell_command_backend,
)));
}
ConfigShellToolType::Local => {
builder.register_handler(Arc::new(ShellHandler::default()));
builder.register_handler(Arc::new(ContainerExecHandler));
builder.register_handler(Arc::new(ShellCommandHandler::from(
config.shell_command_backend,
)));
}
ConfigShellToolType::UnifiedExec => {
builder.register_handler(Arc::new(ShellHandler::default()));
builder.register_handler(Arc::new(ContainerExecHandler));
builder.register_handler(Arc::new(LocalShellHandler::default()));
builder.register_handler(Arc::new(ShellCommandHandler::from(
config.shell_command_backend,
)));
}
ConfigShellToolType::ShellCommand => {
builder.register_handler(Arc::new(ShellHandler::default()));
builder.register_handler(Arc::new(ContainerExecHandler));
builder.register_handler(Arc::new(LocalShellHandler::default()));
}
ConfigShellToolType::Disabled => {}
}
}
if params.mcp_tools.is_some() {
builder.push_spec(
create_list_mcp_resources_tool(),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
builder.push_spec(
create_list_mcp_resource_templates_tool(),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
builder.push_spec(
create_read_mcp_resource_tool(),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
builder.register_handler(Arc::new(ListMcpResourcesHandler));
builder.register_handler(Arc::new(ListMcpResourceTemplatesHandler));
builder.register_handler(Arc::new(ReadMcpResourceHandler));
}
builder.push_spec(
create_update_plan_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
builder.register_handler(Arc::new(PlanHandler));
if config.goal_tools {
builder.push_spec(
create_get_goal_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
builder.register_handler(Arc::new(GetGoalHandler));
builder.push_spec(
create_create_goal_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
builder.register_handler(Arc::new(CreateGoalHandler));
builder.push_spec(
create_update_goal_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
builder.register_handler(Arc::new(UpdateGoalHandler));
}
builder.push_spec(
create_request_user_input_tool(request_user_input_tool_description(
&config.request_user_input_available_modes,
)),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
builder.register_handler(Arc::new(RequestUserInputHandler {
available_modes: config.request_user_input_available_modes.clone(),
}));
if config.request_permissions_tool_enabled {
builder.push_spec(
create_request_permissions_tool(request_permissions_tool_description()),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
builder.register_handler(Arc::new(RequestPermissionsHandler));
}
@@ -330,13 +242,9 @@ pub fn build_tool_registry_builder(
});
}
builder.push_spec(
create_tool_search_tool(&search_source_infos, TOOL_SEARCH_DEFAULT_LIMIT),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
builder.register_handler(Arc::new(ToolSearchHandler::new(
params.tool_search_entries.to_vec(),
search_source_infos,
)));
}
@@ -344,36 +252,17 @@ pub fn build_tool_registry_builder(
&& let Some(discoverable_tools) =
params.discoverable_tools.filter(|tools| !tools.is_empty())
{
builder.push_spec(
create_request_plugin_install_tool(&collect_request_plugin_install_entries(
discoverable_tools,
)),
/*supports_parallel_tool_calls*/ true,
/*code_mode_enabled*/ false,
);
builder.register_handler(Arc::new(RequestPluginInstallHandler));
builder.register_handler(Arc::new(RequestPluginInstallHandler::new(
discoverable_tools,
)));
}
if config.environment_mode.has_environment()
&& let Some(apply_patch_tool_type) = &config.apply_patch_tool_type
{
match apply_patch_tool_type {
ApplyPatchToolType::Freeform => {
builder.push_spec(
create_apply_patch_freeform_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
}
ApplyPatchToolType::Function => {
builder.push_spec(
create_apply_patch_json_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
}
}
builder.register_handler(Arc::new(ApplyPatchHandler));
builder.register_handler(Arc::new(ApplyPatchHandler::new(
apply_patch_tool_type.clone(),
)));
}
if config
@@ -381,11 +270,6 @@ pub fn build_tool_registry_builder(
.iter()
.any(|tool| tool == "test_sync_tool")
{
builder.push_spec(
create_test_sync_tool(),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
builder.register_handler(Arc::new(TestSyncHandler));
}
@@ -394,135 +278,62 @@ pub fn build_tool_registry_builder(
web_search_config: config.web_search_config.as_ref(),
web_search_tool_type: config.web_search_tool_type,
}) {
builder.push_spec(
web_search_tool,
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
builder.push_spec(web_search_tool, /*supports_parallel_tool_calls*/ false);
}
if config.image_gen_tool {
builder.push_spec(
create_image_generation_tool("png"),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
}
if config.environment_mode.has_environment() {
builder.push_spec(
create_view_image_tool(ViewImageToolOptions {
can_request_original_image_detail: config.can_request_original_image_detail,
}),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
builder.register_handler(Arc::new(ViewImageHandler));
builder.register_handler(Arc::new(ViewImageHandler::new(ViewImageToolOptions {
can_request_original_image_detail: config.can_request_original_image_detail,
})));
}
if config.collab_tools {
if config.multi_agent_v2 {
let agent_type_description =
agent_type_description(config, params.default_agent_type_description);
builder.push_spec(
create_spawn_agent_tool_v2(SpawnAgentToolOptions {
available_models: &config.available_models,
agent_type_description,
hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata,
include_usage_hint: config.spawn_agent_usage_hint,
usage_hint_text: config.spawn_agent_usage_hint_text.clone(),
max_concurrent_threads_per_session: config.max_concurrent_threads_per_session,
}),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
builder.push_spec(
create_send_message_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
builder.push_spec(
create_followup_task_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
builder.push_spec(
create_wait_agent_tool_v2(params.wait_agent_timeouts),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
builder.push_spec(
create_close_agent_tool_v2(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
builder.push_spec(
create_list_agents_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
builder.register_handler(Arc::new(SpawnAgentHandlerV2));
builder.register_handler(Arc::new(SpawnAgentHandlerV2::new(SpawnAgentToolOptions {
available_models: config.available_models.clone(),
agent_type_description,
hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata,
include_usage_hint: config.spawn_agent_usage_hint,
usage_hint_text: config.spawn_agent_usage_hint_text.clone(),
max_concurrent_threads_per_session: config.max_concurrent_threads_per_session,
})));
builder.register_handler(Arc::new(SendMessageHandlerV2));
builder.register_handler(Arc::new(FollowupTaskHandlerV2));
builder.register_handler(Arc::new(WaitAgentHandlerV2));
builder.register_handler(Arc::new(WaitAgentHandlerV2::new(
params.wait_agent_timeouts,
)));
builder.register_handler(Arc::new(CloseAgentHandlerV2));
builder.register_handler(Arc::new(ListAgentsHandlerV2));
} else {
let agent_type_description =
agent_type_description(config, params.default_agent_type_description);
builder.push_spec(
create_spawn_agent_tool_v1(SpawnAgentToolOptions {
available_models: &config.available_models,
agent_type_description,
hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata,
include_usage_hint: config.spawn_agent_usage_hint,
usage_hint_text: config.spawn_agent_usage_hint_text.clone(),
max_concurrent_threads_per_session: config.max_concurrent_threads_per_session,
}),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
builder.push_spec(
create_send_input_tool_v1(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
builder.push_spec(
create_resume_agent_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
builder.register_handler(Arc::new(ResumeAgentHandler));
builder.push_spec(
create_wait_agent_tool_v1(params.wait_agent_timeouts),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
builder.push_spec(
create_close_agent_tool_v1(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
builder.register_handler(Arc::new(SpawnAgentHandler));
builder.register_handler(Arc::new(SpawnAgentHandler::new(SpawnAgentToolOptions {
available_models: config.available_models.clone(),
agent_type_description,
hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata,
include_usage_hint: config.spawn_agent_usage_hint,
usage_hint_text: config.spawn_agent_usage_hint_text.clone(),
max_concurrent_threads_per_session: config.max_concurrent_threads_per_session,
})));
builder.register_handler(Arc::new(SendInputHandler));
builder.register_handler(Arc::new(WaitAgentHandler));
builder.register_handler(Arc::new(ResumeAgentHandler));
builder.register_handler(Arc::new(WaitAgentHandler::new(params.wait_agent_timeouts)));
builder.register_handler(Arc::new(CloseAgentHandler));
}
}
if config.agent_jobs_tools {
builder.push_spec(
create_spawn_agents_on_csv_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
builder.register_handler(Arc::new(SpawnAgentsOnCsvHandler));
if config.agent_jobs_worker_tools {
builder.push_spec(
create_report_agent_job_result_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
builder.register_handler(Arc::new(ReportAgentJobResultHandler));
}
}
@@ -584,7 +395,6 @@ pub fn build_tool_registry_builder(
tools,
}),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
}
}
@@ -609,11 +419,7 @@ pub fn build_tool_registry_builder(
for spec in coalesce_loadable_tool_specs(dynamic_tool_specs) {
let spec = spec.into();
if config.namespace_tools || !matches!(spec, ToolSpec::Namespace(_)) {
builder.push_spec(
spec,
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
builder.push_spec(spec, /*supports_parallel_tool_calls*/ false);
}
}

View File

@@ -1,8 +1,29 @@
use super::*;
use crate::tools::handlers::apply_patch_spec::create_apply_patch_freeform_tool;
use crate::tools::handlers::goal_spec::create_create_goal_tool;
use crate::tools::handlers::goal_spec::create_get_goal_tool;
use crate::tools::handlers::goal_spec::create_update_goal_tool;
use crate::tools::handlers::multi_agents_spec::WaitAgentTimeoutOptions;
use crate::tools::handlers::multi_agents_spec::create_close_agent_tool_v1;
use crate::tools::handlers::multi_agents_spec::create_close_agent_tool_v2;
use crate::tools::handlers::multi_agents_spec::create_resume_agent_tool;
use crate::tools::handlers::multi_agents_spec::create_send_input_tool_v1;
use crate::tools::handlers::multi_agents_spec::create_send_message_tool;
use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v1;
use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v2;
use crate::tools::handlers::multi_agents_spec::create_wait_agent_tool_v1;
use crate::tools::handlers::multi_agents_spec::create_wait_agent_tool_v2;
use crate::tools::handlers::plan_spec::create_update_plan_tool;
use crate::tools::handlers::request_user_input_spec::REQUEST_USER_INPUT_TOOL_NAME;
use crate::tools::handlers::request_user_input_spec::create_request_user_input_tool;
use crate::tools::handlers::request_user_input_spec::request_user_input_tool_description;
use crate::tools::handlers::shell_spec::CommandToolOptions;
use crate::tools::handlers::shell_spec::create_exec_command_tool;
use crate::tools::handlers::shell_spec::create_request_permissions_tool;
use crate::tools::handlers::shell_spec::create_write_stdin_tool;
use crate::tools::handlers::shell_spec::request_permissions_tool_description;
use crate::tools::handlers::view_image_spec::ViewImageToolOptions;
use crate::tools::handlers::view_image_spec::create_view_image_tool;
use crate::tools::registry::ToolRegistry;
use crate::tools::spec_plan_types::ToolNamespace;
use crate::tools::spec_plan_types::ToolRegistryBuildDeferredTool;
@@ -2423,9 +2444,9 @@ fn request_user_input_tool_spec(available_modes: &[ModeKind]) -> ToolSpec {
create_request_user_input_tool(request_user_input_tool_description(available_modes))
}
fn spawn_agent_tool_options(config: &ToolsConfig) -> SpawnAgentToolOptions<'_> {
fn spawn_agent_tool_options(config: &ToolsConfig) -> SpawnAgentToolOptions {
SpawnAgentToolOptions {
available_models: &config.available_models,
available_models: config.available_models.clone(),
agent_type_description: agent_type_description(config, DEFAULT_AGENT_TYPE_DESCRIPTION),
hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata,
include_usage_hint: config.spawn_agent_usage_hint,

View File

@@ -446,6 +446,11 @@ impl WebSocketHandshake {
pub struct WebSocketConnectionConfig {
pub requests: Vec<Vec<Value>>,
pub response_headers: Vec<(String, String)>,
/// Optional notification fired after the TCP connection is accepted and before the websocket
/// handshake is accepted.
pub accept_started: Option<Arc<Notify>>,
/// Optional gate that blocks websocket handshake acceptance until the notifier is signalled.
pub accept_release: Option<Arc<Notify>>,
/// Optional delay inserted before accepting the websocket handshake.
///
/// Tests use this to force websocket setup into an in-flight state so first-turn warmup paths
@@ -1254,6 +1259,8 @@ pub async fn start_websocket_server(connections: Vec<Vec<Vec<Value>>>) -> WebSoc
.map(|requests| WebSocketConnectionConfig {
requests,
response_headers: Vec::new(),
accept_started: None,
accept_release: None,
accept_delay: None,
close_after_requests: true,
})
@@ -1298,12 +1305,27 @@ pub async fn start_websocket_server_with_headers(
continue;
};
if let Some(accept_started) = &connection.accept_started {
accept_started.notify_one();
}
if let Some(accept_release) = &connection.accept_release {
tokio::select! {
_ = accept_release.notified() => {}
_ = &mut shutdown_rx => return,
}
}
if let Some(delay) = connection.accept_delay {
tokio::time::sleep(delay).await;
tokio::select! {
_ = tokio::time::sleep(delay) => {}
_ = &mut shutdown_rx => return,
}
}
let response_headers = connection.response_headers.clone();
let handshake_log = Arc::clone(&handshakes);
let pending_handshake = Arc::new(Mutex::new(None));
let callback_handshake = Arc::clone(&pending_handshake);
let callback = move |req: &Request, mut response: Response| {
let headers = req
.headers()
@@ -1315,7 +1337,7 @@ pub async fn start_websocket_server_with_headers(
.map(|value| (name.as_str().to_string(), value.to_string()))
})
.collect();
handshake_log.lock().unwrap().push(WebSocketHandshake {
*callback_handshake.lock().unwrap() = Some(WebSocketHandshake {
uri: req.uri().to_string(),
headers,
});
@@ -1344,6 +1366,10 @@ pub async fn start_websocket_server_with_headers(
Err(_) => continue,
};
if let Some(handshake) = pending_handshake.lock().unwrap().take() {
handshakes.lock().unwrap().push(handshake);
}
let connection_index = {
let mut log = requests.lock().unwrap();
log.push(Vec::new());

View File

@@ -121,6 +121,8 @@ async fn websocket_first_turn_handles_handshake_delay_with_startup_prewarm() ->
],
],
response_headers: Vec::new(),
accept_started: None,
accept_release: None,
// Delay handshake so turn processing must tolerate websocket startup latency.
accept_delay: Some(Duration::from_millis(150)),
close_after_requests: true,

View File

@@ -941,6 +941,8 @@ async fn responses_websocket_emits_reasoning_included_event() {
let server = start_websocket_server_with_headers(vec![WebSocketConnectionConfig {
requests: vec![vec![ev_response_created("resp-1"), ev_completed("resp-1")]],
response_headers: vec![("X-Reasoning-Included".to_string(), "true".to_string())],
accept_started: None,
accept_release: None,
accept_delay: None,
close_after_requests: true,
}])
@@ -1015,6 +1017,8 @@ async fn responses_websocket_emits_rate_limit_events() {
("X-Models-Etag".to_string(), "etag-123".to_string()),
("X-Reasoning-Included".to_string(), "true".to_string()),
],
accept_started: None,
accept_release: None,
accept_delay: None,
close_after_requests: true,
}])
@@ -1751,6 +1755,8 @@ async fn responses_websocket_v2_surfaces_terminal_error_without_close_handshake(
})],
],
response_headers: Vec::new(),
accept_started: None,
accept_release: None,
accept_delay: None,
close_after_requests: false,
}])

View File

@@ -48,6 +48,7 @@ use std::process::Command;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;
use tokio::sync::Notify;
use tokio::sync::oneshot;
use tokio::time::timeout;
use wiremock::Match;
@@ -492,6 +493,8 @@ async fn conversation_webrtc_start_posts_generated_session() -> Result<()> {
vec![],
],
response_headers: Vec::new(),
accept_started: None,
accept_release: None,
accept_delay: Some(sideband_accept_delay),
close_after_requests: false,
}])
@@ -659,10 +662,14 @@ async fn conversation_webrtc_close_while_sideband_connecting_drops_pending_join(
)
.mount(&server)
.await;
let accept_started = Arc::new(Notify::new());
let accept_release = Arc::new(Notify::new());
let realtime_server = start_websocket_server_with_headers(vec![WebSocketConnectionConfig {
requests: vec![vec![]],
response_headers: Vec::new(),
accept_delay: Some(Duration::from_millis(500)),
accept_started: Some(Arc::clone(&accept_started)),
accept_release: Some(Arc::clone(&accept_release)),
accept_delay: None,
close_after_requests: false,
}])
.await;
@@ -699,6 +706,9 @@ async fn conversation_webrtc_close_while_sideband_connecting_drops_pending_join(
realtime_server.handshakes().is_empty(),
"sideband websocket should still be pending when SDP is emitted"
);
timeout(Duration::from_secs(5), accept_started.notified())
.await
.context("sideband websocket should connect before close")?;
test.codex.submit(Op::RealtimeConversationClose).await?;
let closed = wait_for_event_match(&test.codex, |msg| match msg {
@@ -726,9 +736,17 @@ async fn conversation_webrtc_close_while_sideband_connecting_drops_pending_join(
"pending sideband task leaked after close: {:?}",
stale_event.ok()
);
accept_release.notify_one();
let stale_request = timeout(Duration::from_millis(250), async {
realtime_server
.wait_for_request(/*connection_index*/ 0, /*request_index*/ 0)
.await
})
.await;
assert!(
realtime_server.handshakes().is_empty(),
"pending sideband task should abort before websocket handshake completes"
stale_request.is_err(),
"pending sideband task sent websocket request after close: {:?}",
stale_request.ok().map(|request| request.body_json())
);
realtime_server.shutdown().await;
@@ -749,11 +767,13 @@ async fn conversation_webrtc_sideband_connect_failure_closes_with_error() -> Res
)
.mount(&server)
.await;
let mut builder = test_codex().with_config(|config| {
let realtime_base_url = server.uri();
let mut builder = test_codex().with_config(move |config| {
config.experimental_realtime_ws_backend_prompt = Some("backend prompt".to_string());
config.experimental_realtime_ws_model = Some("realtime-test-model".to_string());
config.experimental_realtime_ws_startup_context = Some(String::new());
config.experimental_realtime_ws_base_url = Some("http://127.0.0.1:1".to_string());
config.experimental_realtime_ws_base_url = Some(realtime_base_url);
config.model_provider.request_max_retries = Some(0);
config.realtime.version = RealtimeWsVersion::V1;
});
let test = builder.build(&server).await?;

View File

@@ -102,6 +102,8 @@ async fn websocket_turn_state_persists_within_turn_and_resets_after() -> Result<
ev_completed("resp-1"),
]],
response_headers: vec![(TURN_STATE_HEADER.to_string(), "ts-1".to_string())],
accept_started: None,
accept_release: None,
accept_delay: None,
close_after_requests: true,
},
@@ -112,6 +114,8 @@ async fn websocket_turn_state_persists_within_turn_and_resets_after() -> Result<
ev_completed("resp-2"),
]],
response_headers: Vec::new(),
accept_started: None,
accept_release: None,
accept_delay: None,
close_after_requests: true,
},
@@ -122,6 +126,8 @@ async fn websocket_turn_state_persists_within_turn_and_resets_after() -> Result<
ev_completed("resp-3"),
]],
response_headers: Vec::new(),
accept_started: None,
accept_release: None,
accept_delay: None,
close_after_requests: true,
},

View File

@@ -52,7 +52,7 @@ Use the separate `codex mcp` subcommand to manage configured MCP server launcher
Use the v2 thread and turn APIs for all new integrations. `thread/start` creates a thread, `turn/start` submits user input, `turn/interrupt` stops an in-flight turn, and `thread/list` / `thread/read` expose persisted history.
`getConversationSummary` remains as a compatibility helper for clients that still need a summary lookup by `conversationId` or `rolloutPath`.
`getConversationSummary` remains as a compatibility helper for clients that still need a summary lookup by `conversationId` or `rolloutPath`. Lookups by `conversationId` are preferred; lookups by `rolloutPath` won't work with non-local thread stores.
For complete request and response shapes, see the app-server README and the protocol definitions in `app-server-protocol/src/protocol/v2.rs`.

View File

@@ -372,7 +372,16 @@ async fn wait_for_single_request(mock: &ResponseMock) -> ResponsesRequest {
async fn wait_for_file_removed(path: &Path) -> anyhow::Result<()> {
let deadline = Instant::now() + Duration::from_secs(10);
loop {
if !tokio::fs::try_exists(path).await? {
let exists = match tokio::fs::try_exists(path).await {
Ok(exists) => exists,
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => {
// Windows can transiently deny metadata reads while another task
// is removing or resetting files in this workspace.
true
}
Err(err) => return Err(err.into()),
};
if !exists {
return Ok(());
}
assert!(

View File

@@ -227,22 +227,23 @@ WHERE id = ?
Ok(())
}
pub async fn mark_agent_job_completed(&self, job_id: &str) -> anyhow::Result<()> {
pub async fn mark_agent_job_completed(&self, job_id: &str) -> anyhow::Result<bool> {
let now = Utc::now().timestamp();
sqlx::query(
let result = sqlx::query(
r#"
UPDATE agent_jobs
SET status = ?, updated_at = ?, completed_at = ?, last_error = NULL
WHERE id = ?
WHERE id = ? AND status = ?
"#,
)
.bind(AgentJobStatus::Completed.as_str())
.bind(now)
.bind(now)
.bind(job_id)
.bind(AgentJobStatus::Running.as_str())
.execute(self.pool.as_ref())
.await?;
Ok(())
Ok(result.rows_affected() > 0)
}
pub async fn mark_agent_job_failed(
@@ -428,9 +429,46 @@ WHERE job_id = ? AND item_id = ? AND status = ?
item_id: &str,
reporting_thread_id: &str,
result_json: &Value,
) -> anyhow::Result<bool> {
self.report_agent_job_item_result_inner(
job_id,
item_id,
reporting_thread_id,
result_json,
/*cancel_job_reason*/ None,
)
.await
}
pub async fn report_agent_job_item_result_and_cancel_job(
&self,
job_id: &str,
item_id: &str,
reporting_thread_id: &str,
result_json: &Value,
cancel_job_reason: &str,
) -> anyhow::Result<bool> {
self.report_agent_job_item_result_inner(
job_id,
item_id,
reporting_thread_id,
result_json,
Some(cancel_job_reason),
)
.await
}
async fn report_agent_job_item_result_inner(
&self,
job_id: &str,
item_id: &str,
reporting_thread_id: &str,
result_json: &Value,
cancel_job_reason: Option<&str>,
) -> anyhow::Result<bool> {
let now = Utc::now().timestamp();
let serialized = serde_json::to_string(result_json)?;
let mut tx = self.pool.begin().await?;
let result = sqlx::query(
r#"
UPDATE agent_job_items
@@ -446,7 +484,7 @@ WHERE
job_id = ?
AND item_id = ?
AND status = ?
AND assigned_thread_id = ?
AND (assigned_thread_id = ? OR assigned_thread_id IS NULL)
"#,
)
.bind(AgentJobItemStatus::Completed.as_str())
@@ -458,9 +496,29 @@ WHERE
.bind(item_id)
.bind(AgentJobItemStatus::Running.as_str())
.bind(reporting_thread_id)
.execute(self.pool.as_ref())
.execute(&mut *tx)
.await?;
Ok(result.rows_affected() > 0)
let accepted = result.rows_affected() > 0;
if accepted && let Some(reason) = cancel_job_reason {
sqlx::query(
r#"
UPDATE agent_jobs
SET status = ?, updated_at = ?, completed_at = ?, last_error = ?
WHERE id = ? AND status IN (?, ?)
"#,
)
.bind(AgentJobStatus::Cancelled.as_str())
.bind(now)
.bind(now)
.bind(reason)
.bind(job_id)
.bind(AgentJobStatus::Pending.as_str())
.bind(AgentJobStatus::Running.as_str())
.execute(&mut *tx)
.await?;
}
tx.commit().await?;
Ok(accepted)
}
pub async fn mark_agent_job_item_completed(
@@ -652,6 +710,113 @@ mod tests {
Ok(())
}
#[tokio::test]
async fn report_agent_job_item_result_can_cancel_job_atomically() -> anyhow::Result<()> {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home, "test-provider".to_string()).await?;
let (job_id, item_id, thread_id) = create_running_single_item_job(runtime.as_ref()).await?;
let accepted = runtime
.report_agent_job_item_result_and_cancel_job(
job_id.as_str(),
item_id.as_str(),
thread_id.as_str(),
&json!({"ok": true}),
"cancelled by worker request",
)
.await?;
assert!(accepted);
let job = runtime
.get_agent_job(job_id.as_str())
.await?
.expect("job should exist");
assert_eq!(job.status, AgentJobStatus::Cancelled);
assert_eq!(
job.last_error,
Some("cancelled by worker request".to_string())
);
let item = runtime
.get_agent_job_item(job_id.as_str(), item_id.as_str())
.await?
.expect("job item should exist");
assert_eq!(item.status, AgentJobItemStatus::Completed);
assert_eq!(item.result_json, Some(json!({"ok": true})));
assert_eq!(item.assigned_thread_id, None);
let completed = runtime.mark_agent_job_completed(job_id.as_str()).await?;
assert!(!completed);
let job = runtime
.get_agent_job(job_id.as_str())
.await?
.expect("job should exist");
assert_eq!(job.status, AgentJobStatus::Cancelled);
Ok(())
}
#[tokio::test]
async fn report_agent_job_item_result_accepts_unassigned_running_item() -> anyhow::Result<()> {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home, "test-provider".to_string()).await?;
let job_id = "job-1".to_string();
let item_id = "item-1".to_string();
let thread_id = "thread-1".to_string();
runtime
.create_agent_job(
&AgentJobCreateParams {
id: job_id.clone(),
name: "test-job".to_string(),
instruction: "Return a result".to_string(),
auto_export: true,
max_runtime_seconds: None,
output_schema_json: None,
input_headers: vec!["path".to_string()],
input_csv_path: "/tmp/in.csv".to_string(),
output_csv_path: "/tmp/out.csv".to_string(),
},
&[AgentJobItemCreateParams {
item_id: item_id.clone(),
row_index: 0,
source_id: None,
row_json: json!({"path":"file-1"}),
}],
)
.await?;
runtime.mark_agent_job_running(job_id.as_str()).await?;
let marked_running = runtime
.mark_agent_job_item_running(job_id.as_str(), item_id.as_str())
.await?;
assert!(marked_running);
let accepted = runtime
.report_agent_job_item_result_and_cancel_job(
job_id.as_str(),
item_id.as_str(),
thread_id.as_str(),
&json!({"ok": true}),
"cancelled by worker request",
)
.await?;
assert!(accepted);
let job = runtime
.get_agent_job(job_id.as_str())
.await?
.expect("job should exist");
assert_eq!(job.status, AgentJobStatus::Cancelled);
let item = runtime
.get_agent_job_item(job_id.as_str(), item_id.as_str())
.await?
.expect("job item should exist");
assert_eq!(item.status, AgentJobItemStatus::Completed);
assert_eq!(item.result_json, Some(json!({"ok": true})));
assert_eq!(item.assigned_thread_id, None);
Ok(())
}
#[tokio::test]
async fn report_agent_job_item_result_rejects_late_reports() -> anyhow::Result<()> {
let codex_home = unique_temp_dir();

View File

@@ -256,10 +256,16 @@ fn stored_thread_from_state(
items: history_items.clone(),
});
let name = state.names.get(&thread_id).cloned().flatten();
let rollout_path = state
.rollout_paths
.iter()
.find_map(|(path, mapped_thread_id)| {
(*mapped_thread_id == thread_id).then(|| path.clone())
});
Ok(StoredThread {
thread_id,
rollout_path: None,
rollout_path,
forked_from_id: created.forked_from_id,
preview: String::new(),
name,

View File

@@ -69,6 +69,8 @@ const DENY_ACCESS: i32 = 3;
mod read_acl_mutex;
mod sandbox_users;
#[path = "setup_runtime_bin.rs"]
mod setup_runtime_bin;
use read_acl_mutex::acquire_read_acl_mutex;
use read_acl_mutex::read_acl_mutex_exists;
use sandbox_users::provision_sandbox_users;
@@ -510,8 +512,7 @@ fn run_read_acl_only(payload: &Payload, log: &mut File) -> Result<()> {
fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<()> {
let refresh_only = payload.refresh_only;
if refresh_only {
} else {
if !refresh_only {
let provision_result = provision_sandbox_users(
&payload.codex_home,
&payload.offline_username,
@@ -647,6 +648,14 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<(
}
}
if refresh_only {
setup_runtime_bin::ensure_codex_app_runtime_bin_readable(
sandbox_group_psid,
&mut refresh_errors,
log,
)?;
}
let cap_sid_str = caps.workspace;
let sandbox_group_sid_str =
string_from_sid_bytes(&sandbox_group_sid).map_err(anyhow::Error::msg)?;

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