Merge remote-tracking branch 'refs/remotes/origin/main' into abhinav/codex-env-file-conflict-fix

# Conflicts:
#	codex-rs/core/src/tools/runtimes/mod.rs
#	codex-rs/core/src/tools/runtimes/shell.rs
#	codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs
#	codex-rs/core/src/tools/runtimes/unified_exec.rs
This commit is contained in:
Abhinav Vedmala
2026-05-28 15:16:25 -07:00
516 changed files with 19606 additions and 5332 deletions
+1 -1
View File
@@ -8,4 +8,4 @@ script = ""
[[actions]]
name = "Run"
icon = "run"
command = "cargo +1.93.0 run --manifest-path=codex-rs/Cargo.toml --bin codex -- -c mcp_oauth_credentials_store=file"
command = "cargo +1.95.0 run --manifest-path=codex-rs/Cargo.toml --bin codex -- -c mcp_oauth_credentials_store=file"
+1 -1
View File
@@ -3,7 +3,7 @@ FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04
ARG TZ
ARG DEBIAN_FRONTEND=noninteractive
ARG NODE_MAJOR=22
ARG RUST_TOOLCHAIN=1.92.0
ARG RUST_TOOLCHAIN=1.95.0
# Keep this in sync with .devcontainer/codex-install/package.json and pnpm-lock.yaml.
ARG CODEX_NPM_VERSION=0.121.0
+1 -1
View File
@@ -7,7 +7,7 @@
"args": {
"TZ": "${localEnv:TZ:UTC}",
"NODE_MAJOR": "22",
"RUST_TOOLCHAIN": "1.92.0",
"RUST_TOOLCHAIN": "1.95.0",
"CODEX_NPM_VERSION": "0.121.0"
}
},
+11 -2
View File
@@ -150,7 +150,9 @@ for arg in "\$@"; do
args+=("\${arg}")
done
exec "${zig_bin}" cc -target "${zig_target}" "\${args[@]}"
# Zig enables UBSan for debug C builds by default. Rust links these objects
# without Zig's sanitizer runtime, so keep native dependencies uninstrumented.
exec "${zig_bin}" cc -target "${zig_target}" "\${args[@]}" -fno-sanitize=undefined
EOF
cat >"${cxx}" <<EOF
#!/usr/bin/env bash
@@ -207,7 +209,9 @@ for arg in "\$@"; do
args+=("\${arg}")
done
exec "${zig_bin}" c++ -target "${zig_target}" "\${args[@]}"
# Zig enables UBSan for debug C++ builds by default. Rust links these objects
# without Zig's sanitizer runtime, so keep native dependencies uninstrumented.
exec "${zig_bin}" c++ -target "${zig_target}" "\${args[@]}" -fno-sanitize=undefined
EOF
chmod +x "${cc}" "${cxx}"
@@ -270,6 +274,11 @@ echo "PKG_CONFIG_PATH=${pkg_config_path}" >> "$GITHUB_ENV"
pkg_config_path_var="PKG_CONFIG_PATH_${TARGET}"
pkg_config_path_var="${pkg_config_path_var//-/_}"
echo "${pkg_config_path_var}=${libcap_pkgconfig_dir}" >> "$GITHUB_ENV"
pkg_config_libdir_var="PKG_CONFIG_LIBDIR_${TARGET}"
pkg_config_libdir_var="${pkg_config_libdir_var//-/_}"
# Do not let musl cross-builds resolve native libraries from the host glibc
# pkg-config directories. libcap is the only target package provided here.
echo "${pkg_config_libdir_var}=${libcap_pkgconfig_dir}" >> "$GITHUB_ENV"
if [[ -n "${sysroot}" && "${sysroot}" != "/" ]]; then
echo "PKG_CONFIG_SYSROOT_DIR=${sysroot}" >> "$GITHUB_ENV"
+14 -4
View File
@@ -141,7 +141,9 @@ jobs:
- 2
- 3
- 4
runs-on: windows-latest
runs-on:
group: codex-runners
labels: codex-windows-x64
name: Bazel test on windows-latest for x86_64-pc-windows-gnullvm shard ${{ matrix.shard }}/4
steps:
@@ -246,7 +248,9 @@ jobs:
# it a larger timeout.
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
timeout-minutes: 40
runs-on: windows-latest
runs-on:
group: codex-runners
labels: codex-windows-x64
name: Bazel test on windows-latest for x86_64-pc-windows-gnullvm (native main)
steps:
@@ -332,7 +336,10 @@ jobs:
target: aarch64-apple-darwin
- os: windows-latest
target: x86_64-pc-windows-gnullvm
runs-on: ${{ matrix.os }}
runs_on:
group: codex-runners
labels: codex-windows-x64
runs-on: ${{ matrix.runs_on || matrix.os }}
name: Bazel clippy on ${{ matrix.os }} for ${{ matrix.target }}
steps:
@@ -422,7 +429,10 @@ jobs:
target: aarch64-apple-darwin
- os: windows-latest
target: x86_64-pc-windows-gnullvm
runs-on: ${{ matrix.os }}
runs_on:
group: codex-runners
labels: codex-windows-x64
runs-on: ${{ matrix.runs_on || matrix.os }}
name: Verify release build on ${{ matrix.os }} for ${{ matrix.target }}
steps:
+2 -2
View File
@@ -20,10 +20,10 @@ jobs:
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
- name: Run cargo-deny
uses: EmbarkStudios/cargo-deny-action@82eb9f621fbc699dd0918f3ea06864c14cc84246 # v2
with:
rust-version: 1.93.0
rust-version: 1.95.0
manifest-path: ./codex-rs/Cargo.toml
+91
View File
@@ -0,0 +1,91 @@
name: python-sdk-release
on:
push:
tags:
- "python-v*"
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
jobs:
build-python-sdk:
if: github.repository == 'openai/codex'
name: build-python-sdk
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Validate tag and build Python SDK package
shell: bash
run: |
set -euo pipefail
sdk_version="${GITHUB_REF_NAME#python-v}"
if [[ ! "${sdk_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+b[0-9]+$ ]]; then
echo "Python SDK release tags must identify a beta release, for example python-v0.1.0b1."
exit 1
fi
# The pinned runtime currently publishes a musllinux Linux wheel.
# Build in Alpine so release type generation installs that wheel.
docker run --rm \
--user "$(id -u):$(id -g)" \
-e HOME=/tmp/codex-python-sdk-home \
-e UV_LINK_MODE=copy \
-e SDK_VERSION="${sdk_version}" \
-e SDK_STAGE_DIR="${RUNNER_TEMP}/openai-codex" \
-e SDK_DIST_DIR="${GITHUB_WORKSPACE}/dist/python-sdk" \
-v "${GITHUB_WORKSPACE}:${GITHUB_WORKSPACE}" \
-v "${RUNNER_TEMP}:${RUNNER_TEMP}" \
-w "${GITHUB_WORKSPACE}/sdk/python" \
python:3.12-alpine \
sh -euxc '
python -m venv /tmp/release-tools
/tmp/release-tools/bin/python -m pip install build twine uv==0.11.3
/tmp/release-tools/bin/uv sync --extra dev --frozen
/tmp/release-tools/bin/uv run --extra dev --frozen python scripts/update_sdk_artifacts.py \
stage-sdk "${SDK_STAGE_DIR}" \
--sdk-version "${SDK_VERSION}"
/tmp/release-tools/bin/python -m build \
--wheel \
--sdist \
--outdir "${SDK_DIST_DIR}" \
"${SDK_STAGE_DIR}"
/tmp/release-tools/bin/python -m twine check --strict "${SDK_DIST_DIR}/"*
'
- name: Upload Python SDK package
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: python-sdk-package
path: dist/python-sdk/*
if-no-files-found: error
publish-python-sdk:
name: publish-python-sdk
needs: build-python-sdk
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: read
id-token: write # Required for PyPI trusted publishing.
steps:
- name: Download Python SDK package
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: python-sdk-package
path: dist/python-sdk
- name: Publish Python SDK to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
packages-dir: dist/python-sdk
@@ -94,7 +94,7 @@ jobs:
- name: Install DotSlash
uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
targets: ${{ inputs.target }}
@@ -319,7 +319,7 @@ jobs:
- name: Install DotSlash
uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
targets: ${{ inputs.target }}
+5 -69
View File
@@ -25,7 +25,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
components: rustfmt
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
@@ -46,7 +46,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: cargo-shear@1.11.2
@@ -63,7 +63,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
toolchain: nightly-2025-09-18
components: llvm-tools-preview, rustc-dev, rust-src
@@ -260,13 +260,9 @@ jobs:
set -euo pipefail
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update -y
packages=(pkg-config libcap-dev)
if [[ "${{ matrix.target }}" == 'x86_64-unknown-linux-musl' || "${{ matrix.target }}" == 'aarch64-unknown-linux-musl' ]]; then
packages+=(libubsan1)
fi
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "${packages[@]}"
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
fi
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
targets: ${{ matrix.target }}
components: clippy
@@ -348,14 +344,6 @@ jobs:
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Disable sccache wrapper (musl)
shell: bash
run: |
set -euo pipefail
echo "RUSTC_WRAPPER=" >> "$GITHUB_ENV"
echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV"
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Prepare APT cache directories (musl)
shell: bash
@@ -389,58 +377,6 @@ jobs:
shell: bash
run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh"
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Configure rustc UBSan wrapper (musl host)
shell: bash
run: |
set -euo pipefail
ubsan=""
if command -v ldconfig >/dev/null 2>&1; then
ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')"
fi
wrapper_root="${RUNNER_TEMP:-/tmp}"
wrapper="${wrapper_root}/rustc-ubsan-wrapper"
cat > "${wrapper}" <<EOF
#!/usr/bin/env bash
set -euo pipefail
if [[ -n "${ubsan}" ]]; then
export LD_PRELOAD="${ubsan}\${LD_PRELOAD:+:\${LD_PRELOAD}}"
fi
exec "\$1" "\${@:2}"
EOF
chmod +x "${wrapper}"
echo "RUSTC_WRAPPER=${wrapper}" >> "$GITHUB_ENV"
echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV"
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Clear sanitizer flags (musl)
shell: bash
run: |
set -euo pipefail
# Clear global Rust flags so host/proc-macro builds don't pull in UBSan.
echo "RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV"
# Override any runner-level Cargo config rustflags as well.
echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV"
sanitize_flags() {
local input="$1"
input="${input//-fsanitize=undefined/}"
input="${input//-fno-sanitize-recover=undefined/}"
input="${input//-fno-sanitize-trap=undefined/}"
echo "$input"
}
cflags="$(sanitize_flags "${CFLAGS-}")"
cxxflags="$(sanitize_flags "${CXXFLAGS-}")"
echo "CFLAGS=${cflags}" >> "$GITHUB_ENV"
echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV"
- if: ${{ !contains(matrix.target, 'windows') }}
name: Configure rusty_v8 artifact overrides and verify checksums
uses: ./.github/actions/setup-rusty-v8
+3 -3
View File
@@ -67,7 +67,7 @@ jobs:
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
components: rustfmt
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
@@ -91,7 +91,7 @@ jobs:
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: cargo-shear@1.11.2
@@ -111,7 +111,7 @@ jobs:
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
- name: Install nightly argument-comment-lint toolchain
shell: bash
run: |
@@ -60,7 +60,7 @@ jobs:
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
toolchain: nightly-2025-09-18
targets: ${{ matrix.target }}
+1 -1
View File
@@ -100,7 +100,7 @@ jobs:
Write-Host "Total RAM: $ramGiB GiB"
Write-Host "Disk usage:"
Get-PSDrive -PSProvider FileSystem | Format-Table -AutoSize Name, @{Name='Size(GB)';Expression={[math]::Round(($_.Used + $_.Free) / 1GB, 1)}}, @{Name='Free(GB)';Expression={[math]::Round($_.Free / 1GB, 1)}}
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
targets: ${{ matrix.target }}
+33 -72
View File
@@ -56,7 +56,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
- name: Validate tag matches Cargo.toml version
shell: bash
env:
@@ -245,16 +245,7 @@ jobs:
set -euo pipefail
sudo apt-get update -y
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
- name: Install UBSan runtime (musl)
if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl' }}
shell: bash
run: |
set -euo pipefail
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update -y
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1
fi
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
targets: ${{ matrix.target }}
@@ -283,30 +274,7 @@ jobs:
run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh"
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Configure rustc UBSan wrapper (musl host)
shell: bash
run: |
set -euo pipefail
ubsan=""
if command -v ldconfig >/dev/null 2>&1; then
ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')"
fi
wrapper_root="${RUNNER_TEMP:-/tmp}"
wrapper="${wrapper_root}/rustc-ubsan-wrapper"
cat > "${wrapper}" <<EOF
#!/usr/bin/env bash
set -euo pipefail
if [[ -n "${ubsan}" ]]; then
export LD_PRELOAD="${ubsan}\${LD_PRELOAD:+:\${LD_PRELOAD}}"
fi
exec "\$1" "\${@:2}"
EOF
chmod +x "${wrapper}"
echo "RUSTC_WRAPPER=${wrapper}" >> "$GITHUB_ENV"
echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV"
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Clear sanitizer flags (musl)
name: Disable aws-lc jitter entropy (musl)
shell: bash
run: |
set -euo pipefail
@@ -316,30 +284,6 @@ jobs:
target_no_jitter="${target_no_jitter//-/_}"
echo "${target_no_jitter}=1" >> "$GITHUB_ENV"
# Clear global Rust flags so host/proc-macro builds don't pull in UBSan.
echo "RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV"
# Override any runner-level Cargo config rustflags as well.
echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV"
sanitize_flags() {
local input="$1"
input="${input//-fsanitize=undefined/}"
input="${input//-fno-sanitize-recover=undefined/}"
input="${input//-fno-sanitize-trap=undefined/}"
echo "$input"
}
cflags="$(sanitize_flags "${CFLAGS-}")"
cxxflags="$(sanitize_flags "${CXXFLAGS-}")"
echo "CFLAGS=${cflags}" >> "$GITHUB_ENV"
echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV"
- name: Configure rusty_v8 artifact overrides and verify checksums
uses: ./.github/actions/setup-rusty-v8
with:
@@ -1282,19 +1226,6 @@ jobs:
tag: ${{ github.ref_name }}
config: .github/dotslash-argument-comment-lint-config.json
- name: Trigger developers.openai.com deploy
# Only trigger the deploy if the release is not a pre-release.
# The deploy is used to update the developers.openai.com website with the new config schema json file.
if: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }}
continue-on-error: true
env:
DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL: ${{ secrets.DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL }}
run: |
if ! curl -sS -f -o /dev/null -X POST "$DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL"; then
echo "::warning title=developers.openai.com deploy hook failed::Vercel deploy hook POST failed for ${GITHUB_REF_NAME}"
exit 1
fi
# Publish to npm using OIDC authentication.
# July 31, 2025: https://github.blog/changelog/2025-07-31-npm-trusted-publishing-with-oidc-is-generally-available/
# npm docs: https://docs.npmjs.com/trusted-publishers
@@ -1507,6 +1438,36 @@ jobs:
packages-dir: dist/python-runtime
skip-existing: true
deploy-dev-website:
name: Trigger developers.openai.com deploy
needs: release
# Only trigger the deploy for a stable signed release.
# The deploy updates developers.openai.com with the new config schema json file.
if: >-
${{
!cancelled() &&
needs.release.result == 'success' &&
needs.release.outputs.sign_macos == 'true' &&
!contains(needs.release.outputs.version, '-')
}}
runs-on: ubuntu-latest
continue-on-error: true
permissions: {}
environment:
name: dev-website-vercel-deploy
deployment: false
steps:
- name: Trigger developers.openai.com deploy
continue-on-error: true
env:
DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL: ${{ secrets.DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL }}
run: |
if ! curl -sS -f -o /dev/null -X POST "$DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL"; then
echo "::warning title=developers.openai.com deploy hook failed::Vercel deploy hook POST failed for ${GITHUB_REF_NAME}"
exit 1
fi
winget:
name: winget
needs: release
+2 -2
View File
@@ -152,9 +152,9 @@ jobs:
python-version: "3.12"
- name: Set up Rust toolchain for Cargo smoke
uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
toolchain: "1.93.0"
toolchain: "1.95.0"
- name: Build Bazel V8 release pair
env:
+5 -5
View File
@@ -166,9 +166,9 @@ jobs:
python-version: "3.12"
- name: Set up Rust toolchain for Cargo smoke
uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
toolchain: "1.93.0"
toolchain: "1.95.0"
- name: Build Bazel V8 release pair
env:
@@ -310,9 +310,9 @@ jobs:
architecture: x64
- name: Set up Codex Rust toolchain for Cargo smoke
uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
toolchain: "1.93.0"
toolchain: "1.95.0"
targets: ${{ matrix.target }}
- name: Install rusty_v8 Rust toolchain
@@ -401,7 +401,7 @@ jobs:
cd codex-rs
RUSTY_V8_ARCHIVE="${GITHUB_WORKSPACE}/${archive}" \
RUSTY_V8_SRC_BINDING_PATH="${GITHUB_WORKSPACE}/${binding}" \
cargo +1.93.0 test -p codex-v8-poc --target "${TARGET}" --features sandbox --no-run
cargo +1.95.0 test -p codex-v8-poc --target "${TARGET}" --features sandbox --no-run
)
- name: Upload staged artifacts
+1 -1
View File
@@ -163,7 +163,7 @@ use_repo(nightly_rust, "rust_toolchains")
toolchains = use_extension("@rules_rs//rs/experimental/toolchains:module_extension.bzl", "toolchains")
toolchains.toolchain(
edition = "2024",
version = "1.93.0",
version = "1.95.0",
)
use_repo(toolchains, "default_rust_toolchains")
+147 -133
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
working-directory: codex-rs
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
- name: Install cargo-audit
uses: taiki-e/install-action@v2
with:
+644 -278
View File
File diff suppressed because it is too large Load Diff
+7 -4
View File
@@ -47,6 +47,7 @@ members = [
"ext/extension-api",
"ext/goal",
"ext/guardian",
"ext/image-generation",
"ext/memories",
"ext/web-search",
"external-agent-migration",
@@ -165,6 +166,7 @@ codex-execpolicy = { path = "execpolicy" }
codex-extension-api = { path = "ext/extension-api" }
codex-goal-extension = { path = "ext/goal" }
codex-guardian = { path = "ext/guardian" }
codex-image-generation-extension = { path = "ext/image-generation" }
codex-external-agent-migration = { path = "external-agent-migration" }
codex-external-agent-sessions = { path = "external-agent-sessions" }
codex-experimental-api-macros = { path = "codex-experimental-api-macros" }
@@ -342,7 +344,7 @@ rcgen = { version = "0.14.7", default-features = false, features = [
regex = "1.12.3"
regex-lite = "0.1.8"
reqwest = { version = "0.12", features = ["cookies"] }
rmcp = { version = "0.15.0", default-features = false }
rmcp = { version = "1.7.0", default-features = false }
runfiles = { git = "https://github.com/dzbarsky/rules_rust", rev = "b56cbaa8465e74127f1ea216f813cd377295ad81" }
rustls = { version = "0.23", default-features = false, features = [
"ring",
@@ -366,13 +368,14 @@ sha2 = "0.10"
shlex = "1.3.0"
similar = "2.7.0"
socket2 = "0.6.1"
sqlx = { version = "0.8.6", default-features = false, features = [
sqlx = { version = "0.9.0", default-features = false, features = [
"chrono",
"json",
"macros",
"migrate",
"runtime-tokio-rustls",
"sqlite",
"runtime-tokio",
"tls-rustls",
"sqlite-bundled",
"time",
"uuid",
] }
@@ -263,6 +263,7 @@ fn sample_thread_resume_response_with_source(
sandbox: AppServerSandboxPolicy::DangerFullAccess,
active_permission_profile: None,
reasoning_effort: None,
initial_turns_page: None,
})
}
@@ -271,6 +272,7 @@ fn sample_turn_start_request(thread_id: &str, request_id: i64) -> ClientRequest
request_id: RequestId::Integer(request_id),
params: TurnStartParams {
thread_id: thread_id.to_string(),
client_user_message_id: None,
input: vec![
UserInput::Text {
text: "hello".to_string(),
@@ -390,6 +392,7 @@ fn sample_turn_steer_request(
params: TurnSteerParams {
thread_id: thread_id.to_string(),
expected_turn_id: expected_turn_id.to_string(),
client_user_message_id: None,
input: vec![
UserInput::Text {
text: "more".to_string(),
@@ -1213,6 +1216,7 @@ fn compaction_event_serializes_expected_shape() {
completed_at: 106,
duration_ms: Some(6543),
},
"session-thread-1".to_string(),
sample_app_server_client_metadata(),
sample_runtime_metadata(),
Some(ThreadSource::User),
@@ -1229,6 +1233,7 @@ fn compaction_event_serializes_expected_shape() {
"event_type": "codex_compaction_event",
"event_params": {
"thread_id": "thread-1",
"session_id": "session-thread-1",
"turn_id": "turn-1",
"app_server_client": {
"product_client_id": DEFAULT_ORIGINATOR,
@@ -1307,6 +1312,7 @@ fn thread_initialized_event_serializes_expected_shape() {
event_type: "codex_thread_initialized",
event_params: ThreadInitializedEventParams {
thread_id: "thread-0".to_string(),
session_id: "session-thread-0".to_string(),
app_server_client: CodexAppServerClientMetadata {
product_client_id: DEFAULT_ORIGINATOR.to_string(),
client_name: Some("codex-tui".to_string()),
@@ -1338,6 +1344,7 @@ fn thread_initialized_event_serializes_expected_shape() {
"event_type": "codex_thread_initialized",
"event_params": {
"thread_id": "thread-0",
"session_id": "session-thread-0",
"app_server_client": {
"product_client_id": DEFAULT_ORIGINATOR,
"client_name": "codex-tui",
@@ -1605,6 +1612,7 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize
let payload = serde_json::to_value(&events).expect("serialize events");
assert_eq!(payload.as_array().expect("events array").len(), 1);
assert_eq!(payload[0]["event_type"], "codex_thread_initialized");
assert_eq!(payload[0]["event_params"]["session_id"], "session-thread-1");
assert_eq!(
payload[0]["event_params"]["app_server_client"]["product_client_id"],
DEFAULT_ORIGINATOR
@@ -1781,6 +1789,7 @@ async fn compaction_event_ingests_custom_fact() {
let payload = serde_json::to_value(&events).expect("serialize events");
assert_eq!(payload.as_array().expect("events array").len(), 1);
assert_eq!(payload[0]["event_type"], "codex_compaction_event");
assert_eq!(payload[0]["event_params"]["session_id"], "session-thread-1");
assert_eq!(payload[0]["event_params"]["thread_id"], "thread-1");
assert_eq!(payload[0]["event_params"]["turn_id"], "turn-compact");
assert_eq!(
@@ -1905,6 +1914,10 @@ async fn guardian_review_event_ingests_custom_fact_with_optional_target_item() {
let payload = serde_json::to_value(&events).expect("serialize events");
assert_eq!(payload.as_array().expect("events array").len(), 1);
assert_eq!(payload[0]["event_type"], "codex_guardian_review");
assert_eq!(
payload[0]["event_params"]["session_id"],
"session-thread-guardian"
);
assert_eq!(payload[0]["event_params"]["thread_id"], "thread-guardian");
assert_eq!(payload[0]["event_params"]["turn_id"], "turn-guardian");
assert_eq!(payload[0]["event_params"]["review_id"], "review-guardian");
@@ -2397,6 +2410,7 @@ async fn item_review_summaries_do_not_cross_threads_with_reused_item_ids() {
fn subagent_thread_started_review_serializes_expected_shape() {
let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request(
SubAgentThreadStartedInput {
session_id: "session-root".to_string(),
thread_id: "thread-review".to_string(),
parent_thread_id: None,
product_client_id: "codex-tui".to_string(),
@@ -2440,6 +2454,7 @@ fn subagent_thread_started_thread_spawn_serializes_parent_thread_id() {
.expect("valid thread id");
let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request(
SubAgentThreadStartedInput {
session_id: "session-root".to_string(),
thread_id: "thread-spawn".to_string(),
parent_thread_id: None,
product_client_id: "codex-tui".to_string(),
@@ -2459,18 +2474,21 @@ fn subagent_thread_started_thread_spawn_serializes_parent_thread_id() {
));
let payload = serde_json::to_value(&event).expect("serialize thread spawn subagent event");
assert_eq!(payload["event_params"]["thread_id"], "thread-spawn");
assert_eq!(payload["event_params"]["thread_source"], "subagent");
assert_eq!(payload["event_params"]["subagent_source"], "thread_spawn");
assert_eq!(
payload["event_params"]["parent_thread_id"],
"11111111-1111-1111-1111-111111111111"
);
assert_eq!(payload["event_params"]["session_id"], "session-root");
}
#[test]
fn subagent_thread_started_memory_consolidation_serializes_expected_shape() {
let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request(
SubAgentThreadStartedInput {
session_id: "session-root".to_string(),
thread_id: "thread-memory".to_string(),
parent_thread_id: None,
product_client_id: "codex-tui".to_string(),
@@ -2496,6 +2514,7 @@ fn subagent_thread_started_memory_consolidation_serializes_expected_shape() {
fn subagent_thread_started_other_serializes_expected_shape() {
let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request(
SubAgentThreadStartedInput {
session_id: "session-root".to_string(),
thread_id: "thread-guardian".to_string(),
parent_thread_id: None,
product_client_id: "codex-tui".to_string(),
@@ -2517,6 +2536,7 @@ fn subagent_thread_started_other_serializes_expected_shape() {
fn subagent_thread_started_other_serializes_explicit_parent_thread_id() {
let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request(
SubAgentThreadStartedInput {
session_id: "session-root".to_string(),
thread_id: "thread-guardian".to_string(),
parent_thread_id: Some("parent-thread-guardian".to_string()),
product_client_id: "codex-tui".to_string(),
@@ -2546,6 +2566,7 @@ async fn subagent_thread_started_publishes_without_initialize() {
.ingest(
AnalyticsFact::Custom(CustomAnalyticsFact::SubAgentThreadStarted(
SubAgentThreadStartedInput {
session_id: "session-root".to_string(),
thread_id: "thread-review".to_string(),
parent_thread_id: None,
product_client_id: "codex-tui".to_string(),
@@ -2619,6 +2640,7 @@ async fn subagent_thread_started_inherits_parent_connection_for_new_thread() {
.ingest(
AnalyticsFact::Custom(CustomAnalyticsFact::SubAgentThreadStarted(
SubAgentThreadStartedInput {
session_id: "session-root".to_string(),
thread_id: "thread-review".to_string(),
parent_thread_id: None,
product_client_id: "parent-client".to_string(),
@@ -2666,6 +2688,8 @@ async fn subagent_thread_started_inherits_parent_connection_for_new_thread() {
.await;
let payload = serde_json::to_value(&events).expect("serialize events");
assert_eq!(payload[0]["event_params"]["session_id"], "session-root");
assert_eq!(payload[0]["event_params"]["thread_id"], "thread-review");
assert_eq!(
payload[0]["event_params"]["app_server_client"]["product_client_id"],
"parent-client"
@@ -2686,6 +2710,7 @@ async fn subagent_tool_items_inherit_parent_connection_metadata() {
.ingest(
AnalyticsFact::Custom(CustomAnalyticsFact::SubAgentThreadStarted(
SubAgentThreadStartedInput {
session_id: "session-root".to_string(),
thread_id: "thread-subagent".to_string(),
parent_thread_id: Some("thread-1".to_string()),
product_client_id: "codex-tui".to_string(),
@@ -3191,6 +3216,7 @@ fn turn_event_serializes_expected_shape() {
event_type: "codex_turn_event",
event_params: crate::events::CodexTurnEventParams {
thread_id: "thread-2".to_string(),
session_id: "session-thread-2".to_string(),
turn_id: "turn-2".to_string(),
app_server_client: sample_app_server_client_metadata(),
runtime: sample_runtime_metadata(),
@@ -3241,6 +3267,7 @@ fn turn_event_serializes_expected_shape() {
"event_type": "codex_turn_event",
"event_params": {
"thread_id": "thread-2",
"session_id": "session-thread-2",
"turn_id": "turn-2",
"submission_type": null,
"app_server_client": {
@@ -3342,6 +3369,10 @@ async fn accepted_turn_steer_emits_expected_event() {
let payload = serde_json::to_value(&out[0]).expect("serialize turn steer event");
assert_eq!(payload["event_type"], json!("codex_turn_steer_event"));
assert_eq!(payload["event_params"]["thread_id"], json!("thread-2"));
assert_eq!(
payload["event_params"]["session_id"],
json!("session-thread-2")
);
assert_eq!(payload["event_params"]["expected_turn_id"], json!("turn-2"));
assert_eq!(payload["event_params"]["accepted_turn_id"], json!("turn-2"));
assert_eq!(payload["event_params"]["num_input_images"], json!(1));
@@ -3559,6 +3590,10 @@ async fn turn_lifecycle_emits_turn_event() {
let payload = serde_json::to_value(&out[0]).expect("serialize turn event");
assert_eq!(payload["event_type"], json!("codex_turn_event"));
assert_eq!(payload["event_params"]["thread_id"], json!("thread-2"));
assert_eq!(
payload["event_params"]["session_id"],
json!("session-thread-2")
);
assert_eq!(payload["event_params"]["turn_id"], json!("turn-2"));
assert_eq!(
payload["event_params"]["app_server_client"],
+3
View File
@@ -89,6 +89,7 @@ fn sample_turn_start_request() -> ClientRequest {
request_id: RequestId::Integer(1),
params: TurnStartParams {
thread_id: "thread-1".to_string(),
client_user_message_id: None,
input: Vec::new(),
..Default::default()
},
@@ -101,6 +102,7 @@ fn sample_turn_steer_request() -> ClientRequest {
params: TurnSteerParams {
thread_id: "thread-1".to_string(),
expected_turn_id: "turn-1".to_string(),
client_user_message_id: None,
input: Vec::new(),
responsesapi_client_metadata: None,
additional_context: None,
@@ -172,6 +174,7 @@ fn sample_thread_resume_response() -> ClientResponsePayload {
sandbox: AppServerSandboxPolicy::DangerFullAccess,
active_permission_profile: None,
reasoning_effort: None,
initial_turns_page: None,
})
}
+8
View File
@@ -147,6 +147,7 @@ pub(crate) struct CodexRuntimeMetadata {
#[derive(Serialize)]
pub(crate) struct ThreadInitializedEventParams {
pub(crate) thread_id: String,
pub(crate) session_id: String,
pub(crate) app_server_client: CodexAppServerClientMetadata,
pub(crate) runtime: CodexRuntimeMetadata,
pub(crate) model: String,
@@ -420,6 +421,7 @@ impl GuardianReviewAnalyticsResult {
#[derive(Serialize)]
pub(crate) struct GuardianReviewEventPayload {
pub(crate) session_id: String,
pub(crate) app_server_client: CodexAppServerClientMetadata,
pub(crate) runtime: CodexRuntimeMetadata,
#[serde(flatten)]
@@ -738,6 +740,7 @@ pub(crate) struct CodexHookRunEventRequest {
#[derive(Serialize)]
pub(crate) struct CodexCompactionEventParams {
pub(crate) thread_id: String,
pub(crate) session_id: String,
pub(crate) turn_id: String,
pub(crate) app_server_client: CodexAppServerClientMetadata,
pub(crate) runtime: CodexRuntimeMetadata,
@@ -767,6 +770,7 @@ pub(crate) struct CodexCompactionEventRequest {
#[derive(Serialize)]
pub(crate) struct CodexTurnEventParams {
pub(crate) thread_id: String,
pub(crate) session_id: String,
pub(crate) turn_id: String,
// TODO(rhan-oai): Populate once queued/default submission type is plumbed from
// the turn/start callsites instead of always being reported as None.
@@ -821,6 +825,7 @@ pub(crate) struct CodexTurnEventRequest {
#[derive(Serialize)]
pub(crate) struct CodexTurnSteerEventParams {
pub(crate) thread_id: String,
pub(crate) session_id: String,
pub(crate) expected_turn_id: Option<String>,
pub(crate) accepted_turn_id: Option<String>,
pub(crate) app_server_client: CodexAppServerClientMetadata,
@@ -926,6 +931,7 @@ pub(crate) fn codex_plugin_metadata(plugin: PluginTelemetryMetadata) -> CodexPlu
pub(crate) fn codex_compaction_event_params(
input: CodexCompactionEvent,
session_id: String,
app_server_client: CodexAppServerClientMetadata,
runtime: CodexRuntimeMetadata,
thread_source: Option<ThreadSource>,
@@ -934,6 +940,7 @@ pub(crate) fn codex_compaction_event_params(
) -> CodexCompactionEventParams {
CodexCompactionEventParams {
thread_id: input.thread_id,
session_id,
turn_id: input.turn_id,
app_server_client,
runtime,
@@ -1026,6 +1033,7 @@ pub(crate) fn subagent_thread_started_event_request(
) -> ThreadInitializedEvent {
let event_params = ThreadInitializedEventParams {
thread_id: input.thread_id,
session_id: input.session_id,
app_server_client: CodexAppServerClientMetadata {
product_client_id: input.product_client_id,
client_name: Some(input.client_name),
+1
View File
@@ -199,6 +199,7 @@ pub struct AppInvocation {
#[derive(Clone)]
pub struct SubAgentThreadStartedInput {
pub session_id: String,
pub thread_id: String,
pub parent_thread_id: Option<String>,
pub product_client_id: String,
+13 -2
View File
@@ -255,6 +255,7 @@ struct ItemReviewSummary {
#[derive(Clone)]
struct ThreadMetadataState {
session_id: String,
thread_source: Option<ThreadSource>,
initialization_mode: ThreadInitializationMode,
subagent_source: Option<String>,
@@ -263,6 +264,7 @@ struct ThreadMetadataState {
impl ThreadMetadataState {
fn from_thread_metadata(
session_id: String,
session_source: &SessionSource,
thread_source: Option<ThreadSource>,
initialization_mode: ThreadInitializationMode,
@@ -281,6 +283,7 @@ impl ThreadMetadataState {
| SessionSource::Unknown => (None, None),
};
Self {
session_id,
thread_source,
initialization_mode,
subagent_source,
@@ -525,6 +528,7 @@ impl AnalyticsReducer {
thread_state
.metadata
.get_or_insert_with(|| ThreadMetadataState {
session_id: input.session_id.clone(),
thread_source: Some(ThreadSource::Subagent),
initialization_mode: ThreadInitializationMode::New,
subagent_source: Some(subagent_source_name(&input.subagent_source)),
@@ -543,8 +547,8 @@ impl AnalyticsReducer {
input: GuardianReviewEventParams,
out: &mut Vec<TrackEventRequest>,
) {
let Some(connection_state) =
self.thread_connection_or_warn(AnalyticsDropSite::guardian(&input))
let Some((connection_state, thread_metadata)) =
self.thread_context_or_warn(AnalyticsDropSite::guardian(&input))
else {
return;
};
@@ -552,6 +556,7 @@ impl AnalyticsReducer {
GuardianReviewEventRequest {
event_type: "codex_guardian_review",
event_params: GuardianReviewEventPayload {
session_id: thread_metadata.session_id.clone(),
app_server_client: connection_state.app_server_client.clone(),
runtime: connection_state.runtime.clone(),
guardian_review: input,
@@ -1231,11 +1236,13 @@ impl AnalyticsReducer {
out: &mut Vec<TrackEventRequest>,
) {
let session_source: SessionSource = thread.source.into();
let session_id = thread.session_id;
let thread_id = thread.id;
let Some(connection_state) = self.connections.get(&connection_id) else {
return;
};
let thread_metadata = ThreadMetadataState::from_thread_metadata(
session_id.clone(),
&session_source,
thread.thread_source.map(Into::into),
initialization_mode,
@@ -1252,6 +1259,7 @@ impl AnalyticsReducer {
event_type: "codex_thread_initialized",
event_params: ThreadInitializedEventParams {
thread_id,
session_id,
app_server_client: connection_state.app_server_client.clone(),
runtime: connection_state.runtime.clone(),
model,
@@ -1277,6 +1285,7 @@ impl AnalyticsReducer {
event_type: "codex_compaction_event",
event_params: codex_compaction_event_params(
input,
thread_metadata.session_id.clone(),
connection_state.app_server_client.clone(),
connection_state.runtime.clone(),
thread_metadata.thread_source,
@@ -1379,6 +1388,7 @@ impl AnalyticsReducer {
event_type: "codex_turn_steer_event",
event_params: CodexTurnSteerEventParams {
thread_id: pending_request.thread_id,
session_id: thread_metadata.session_id.clone(),
expected_turn_id: Some(pending_request.expected_turn_id),
accepted_turn_id,
app_server_client: connection_state.app_server_client.clone(),
@@ -2447,6 +2457,7 @@ fn codex_turn_event_params(
let token_usage = turn_state.token_usage.clone();
CodexTurnEventParams {
thread_id,
session_id: thread_metadata.session_id.clone(),
turn_id,
app_server_client,
runtime,
@@ -1085,6 +1085,8 @@
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
@@ -3541,6 +3543,42 @@
}
]
},
"ThreadResumeInitialTurnsPageParams": {
"properties": {
"itemsView": {
"anyOf": [
{
"$ref": "#/definitions/TurnItemsView"
},
{
"type": "null"
}
],
"description": "How much item detail to include for each returned turn; defaults to summary."
},
"limit": {
"description": "Optional turn page size.",
"format": "uint32",
"minimum": 0.0,
"type": [
"integer",
"null"
]
},
"sortDirection": {
"anyOf": [
{
"$ref": "#/definitions/SortDirection"
},
{
"type": "null"
}
],
"description": "Optional turn pagination direction; defaults to descending."
}
},
"type": "object"
},
"ThreadResumeParams": {
"description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nFor non-running threads, the precedence is: history > non-empty path > thread_id. If using history or a non-empty path for a non-running thread, the thread_id param will be ignored.\n\nIf thread_id identifies a running thread, app-server rejoins that thread and treats a non-empty path as a consistency check against the active rollout path. Empty string path values are treated as absent.\n\nPrefer using thread_id whenever possible.",
"properties": {
@@ -3947,6 +3985,12 @@
],
"description": "Override where approval requests are routed for review on this turn and subsequent turns."
},
"clientUserMessageId": {
"type": [
"string",
"null"
]
},
"cwd": {
"description": "Override the working directory for this turn and subsequent turns.",
"type": [
@@ -4033,6 +4077,12 @@
},
"TurnSteerParams": {
"properties": {
"clientUserMessageId": {
"type": [
"string",
"null"
]
},
"expectedTurnId": {
"description": "Required active turn id precondition. The request fails when it does not match the currently active turn.",
"type": "string"
@@ -2031,6 +2031,8 @@
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
@@ -3559,6 +3561,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"
@@ -10160,6 +10160,8 @@
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
@@ -10962,6 +10964,47 @@
"title": "McpResourceReadResponse",
"type": "object"
},
"McpServerInfo": {
"description": "Presentation metadata advertised by an initialized MCP server.",
"properties": {
"description": {
"type": [
"string",
"null"
]
},
"icons": {
"items": true,
"type": [
"array",
"null"
]
},
"name": {
"type": "string"
},
"title": {
"type": [
"string",
"null"
]
},
"version": {
"type": "string"
},
"websiteUrl": {
"type": [
"string",
"null"
]
}
},
"required": [
"name",
"version"
],
"type": "object"
},
"McpServerMigration": {
"properties": {
"name": {
@@ -11072,6 +11115,16 @@
},
"type": "array"
},
"serverInfo": {
"anyOf": [
{
"$ref": "#/definitions/v2/McpServerInfo"
},
{
"type": "null"
}
]
},
"tools": {
"additionalProperties": {
"$ref": "#/definitions/v2/Tool"
@@ -15943,6 +15996,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/v2/UserInput"
@@ -17148,6 +17207,42 @@
"title": "ThreadRealtimeTranscriptDoneNotification",
"type": "object"
},
"ThreadResumeInitialTurnsPageParams": {
"properties": {
"itemsView": {
"anyOf": [
{
"$ref": "#/definitions/v2/TurnItemsView"
},
{
"type": "null"
}
],
"description": "How much item detail to include for each returned turn; defaults to summary."
},
"limit": {
"description": "Optional turn page size.",
"format": "uint32",
"minimum": 0.0,
"type": [
"integer",
"null"
]
},
"sortDirection": {
"anyOf": [
{
"$ref": "#/definitions/v2/SortDirection"
},
{
"type": "null"
}
],
"description": "Optional turn pagination direction; defaults to descending."
}
},
"type": "object"
},
"ThreadResumeParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nFor non-running threads, the precedence is: history > non-empty path > thread_id. If using history or a non-empty path for a non-running thread, the thread_id param will be ignored.\n\nIf thread_id identifies a running thread, app-server rejoins that thread and treats a non-empty path as a consistency check against the active rollout path. Empty string path values are treated as absent.\n\nPrefer using thread_id whenever possible.",
@@ -18324,6 +18419,12 @@
],
"description": "Override where approval requests are routed for review on this turn and subsequent turns."
},
"clientUserMessageId": {
"type": [
"string",
"null"
]
},
"cwd": {
"description": "Override the working directory for this turn and subsequent turns.",
"type": [
@@ -18451,6 +18552,12 @@
"TurnSteerParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"clientUserMessageId": {
"type": [
"string",
"null"
]
},
"expectedTurnId": {
"description": "Required active turn id precondition. The request fails when it does not match the currently active turn.",
"type": "string"
@@ -18486,6 +18593,32 @@
"title": "TurnSteerResponse",
"type": "object"
},
"TurnsPage": {
"properties": {
"backwardsCursor": {
"type": [
"string",
"null"
]
},
"data": {
"items": {
"$ref": "#/definitions/v2/Turn"
},
"type": "array"
},
"nextCursor": {
"type": [
"string",
"null"
]
}
},
"required": [
"data"
],
"type": "object"
},
"UserInput": {
"oneOf": [
{
@@ -6640,6 +6640,8 @@
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
@@ -7491,6 +7493,47 @@
"title": "McpResourceReadResponse",
"type": "object"
},
"McpServerInfo": {
"description": "Presentation metadata advertised by an initialized MCP server.",
"properties": {
"description": {
"type": [
"string",
"null"
]
},
"icons": {
"items": true,
"type": [
"array",
"null"
]
},
"name": {
"type": "string"
},
"title": {
"type": [
"string",
"null"
]
},
"version": {
"type": "string"
},
"websiteUrl": {
"type": [
"string",
"null"
]
}
},
"required": [
"name",
"version"
],
"type": "object"
},
"McpServerMigration": {
"properties": {
"name": {
@@ -7601,6 +7644,16 @@
},
"type": "array"
},
"serverInfo": {
"anyOf": [
{
"$ref": "#/definitions/McpServerInfo"
},
{
"type": "null"
}
]
},
"tools": {
"additionalProperties": {
"$ref": "#/definitions/Tool"
@@ -13767,6 +13820,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"
@@ -14972,6 +15031,42 @@
"title": "ThreadRealtimeTranscriptDoneNotification",
"type": "object"
},
"ThreadResumeInitialTurnsPageParams": {
"properties": {
"itemsView": {
"anyOf": [
{
"$ref": "#/definitions/TurnItemsView"
},
{
"type": "null"
}
],
"description": "How much item detail to include for each returned turn; defaults to summary."
},
"limit": {
"description": "Optional turn page size.",
"format": "uint32",
"minimum": 0.0,
"type": [
"integer",
"null"
]
},
"sortDirection": {
"anyOf": [
{
"$ref": "#/definitions/SortDirection"
},
{
"type": "null"
}
],
"description": "Optional turn pagination direction; defaults to descending."
}
},
"type": "object"
},
"ThreadResumeParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nFor non-running threads, the precedence is: history > non-empty path > thread_id. If using history or a non-empty path for a non-running thread, the thread_id param will be ignored.\n\nIf thread_id identifies a running thread, app-server rejoins that thread and treats a non-empty path as a consistency check against the active rollout path. Empty string path values are treated as absent.\n\nPrefer using thread_id whenever possible.",
@@ -16148,6 +16243,12 @@
],
"description": "Override where approval requests are routed for review on this turn and subsequent turns."
},
"clientUserMessageId": {
"type": [
"string",
"null"
]
},
"cwd": {
"description": "Override the working directory for this turn and subsequent turns.",
"type": [
@@ -16275,6 +16376,12 @@
"TurnSteerParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"clientUserMessageId": {
"type": [
"string",
"null"
]
},
"expectedTurnId": {
"description": "Required active turn id precondition. The request fails when it does not match the currently active turn.",
"type": "string"
@@ -16310,6 +16417,32 @@
"title": "TurnSteerResponse",
"type": "object"
},
"TurnsPage": {
"properties": {
"backwardsCursor": {
"type": [
"string",
"null"
]
},
"data": {
"items": {
"$ref": "#/definitions/Turn"
},
"type": "array"
},
"nextCursor": {
"type": [
"string",
"null"
]
}
},
"required": [
"data"
],
"type": "object"
},
"UserInput": {
"oneOf": [
{
@@ -287,6 +287,8 @@
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
@@ -498,6 +500,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"
@@ -287,6 +287,8 @@
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
@@ -498,6 +500,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"
@@ -10,6 +10,47 @@
],
"type": "string"
},
"McpServerInfo": {
"description": "Presentation metadata advertised by an initialized MCP server.",
"properties": {
"description": {
"type": [
"string",
"null"
]
},
"icons": {
"items": true,
"type": [
"array",
"null"
]
},
"name": {
"type": "string"
},
"title": {
"type": [
"string",
"null"
]
},
"version": {
"type": "string"
},
"websiteUrl": {
"type": [
"string",
"null"
]
}
},
"required": [
"name",
"version"
],
"type": "object"
},
"McpServerStatus": {
"properties": {
"authStatus": {
@@ -30,6 +71,16 @@
},
"type": "array"
},
"serverInfo": {
"anyOf": [
{
"$ref": "#/definitions/McpServerInfo"
},
{
"type": "null"
}
]
},
"tools": {
"additionalProperties": {
"$ref": "#/definitions/Tool"
@@ -165,6 +165,8 @@
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
@@ -424,6 +424,8 @@
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
@@ -642,6 +644,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"
@@ -529,6 +529,8 @@
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
@@ -1119,6 +1121,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"
@@ -450,6 +450,8 @@
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
@@ -934,6 +936,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"
@@ -450,6 +450,8 @@
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
@@ -934,6 +936,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"
@@ -450,6 +450,8 @@
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
@@ -934,6 +936,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"
@@ -224,6 +224,8 @@
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
@@ -981,6 +983,74 @@
"danger-full-access"
],
"type": "string"
},
"SortDirection": {
"enum": [
"asc",
"desc"
],
"type": "string"
},
"ThreadResumeInitialTurnsPageParams": {
"properties": {
"itemsView": {
"anyOf": [
{
"$ref": "#/definitions/TurnItemsView"
},
{
"type": "null"
}
],
"description": "How much item detail to include for each returned turn; defaults to summary."
},
"limit": {
"description": "Optional turn page size.",
"format": "uint32",
"minimum": 0.0,
"type": [
"integer",
"null"
]
},
"sortDirection": {
"anyOf": [
{
"$ref": "#/definitions/SortDirection"
},
{
"type": "null"
}
],
"description": "Optional turn pagination direction; defaults to descending."
}
},
"type": "object"
},
"TurnItemsView": {
"oneOf": [
{
"description": "`items` was not loaded for this turn. The field is intentionally empty.",
"enum": [
"notLoaded"
],
"type": "string"
},
{
"description": "`items` contains only a display summary for this turn.",
"enum": [
"summary"
],
"type": "string"
},
{
"description": "`items` contains every ThreadItem available from persisted app-server history for this turn.",
"enum": [
"full"
],
"type": "string"
}
]
}
},
"description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nFor non-running threads, the precedence is: history > non-empty path > thread_id. If using history or a non-empty path for a non-running thread, the thread_id param will be ignored.\n\nIf thread_id identifies a running thread, app-server rejoins that thread and treats a non-empty path as a consistency check against the active rollout path. Empty string path values are treated as absent.\n\nPrefer using thread_id whenever possible.",
@@ -529,6 +529,8 @@
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
@@ -1119,6 +1121,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"
@@ -1993,6 +2001,32 @@
],
"type": "string"
},
"TurnsPage": {
"properties": {
"backwardsCursor": {
"type": [
"string",
"null"
]
},
"data": {
"items": {
"$ref": "#/definitions/Turn"
},
"type": "array"
},
"nextCursor": {
"type": [
"string",
"null"
]
}
},
"required": [
"data"
],
"type": "object"
},
"UserInput": {
"oneOf": [
{
@@ -450,6 +450,8 @@
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
@@ -934,6 +936,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"
@@ -529,6 +529,8 @@
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
@@ -1119,6 +1121,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"
@@ -450,6 +450,8 @@
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
@@ -934,6 +936,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"
@@ -450,6 +450,8 @@
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
@@ -934,6 +936,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"
@@ -424,6 +424,8 @@
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
@@ -642,6 +644,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"
@@ -123,6 +123,8 @@
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
@@ -514,6 +516,12 @@
],
"description": "Override where approval requests are routed for review on this turn and subsequent turns."
},
"clientUserMessageId": {
"type": [
"string",
"null"
]
},
"cwd": {
"description": "Override the working directory for this turn and subsequent turns.",
"type": [
@@ -424,6 +424,8 @@
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
@@ -642,6 +644,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"
@@ -424,6 +424,8 @@
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
@@ -642,6 +644,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"
@@ -44,6 +44,8 @@
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
@@ -216,6 +218,12 @@
}
},
"properties": {
"clientUserMessageId": {
"type": [
"string",
"null"
]
},
"expectedTurnId": {
"description": "Required active turn id precondition. The request fails when it does not match the currently active turn.",
"type": "string"
@@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ImageDetail = "high" | "original";
export type ImageDetail = "auto" | "low" | "high" | "original";
@@ -0,0 +1,9 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { JsonValue } from "./serde_json/JsonValue";
/**
* Presentation metadata advertised by an initialized MCP server.
*/
export type McpServerInfo = { name: string, title: string | null, version: string, description: string | null, icons: Array<JsonValue> | null, websiteUrl: string | null, };
+1
View File
@@ -42,6 +42,7 @@ export type { InternalSessionSource } from "./InternalSessionSource";
export type { LocalShellAction } from "./LocalShellAction";
export type { LocalShellExecAction } from "./LocalShellExecAction";
export type { LocalShellStatus } from "./LocalShellStatus";
export type { McpServerInfo } from "./McpServerInfo";
export type { MessagePhase } from "./MessagePhase";
export type { ModeKind } from "./ModeKind";
export type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment";
@@ -1,9 +1,10 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { McpServerInfo } from "../McpServerInfo";
import type { Resource } from "../Resource";
import type { ResourceTemplate } from "../ResourceTemplate";
import type { Tool } from "../Tool";
import type { McpAuthStatus } from "./McpAuthStatus";
export type McpServerStatus = { name: string, tools: { [key in string]?: Tool }, resources: Array<Resource>, resourceTemplates: Array<ResourceTemplate>, authStatus: McpAuthStatus, };
export type McpServerStatus = { name: string, serverInfo: McpServerInfo | null, tools: { [key in string]?: Tool }, resources: Array<Resource>, resourceTemplates: Array<ResourceTemplate>, authStatus: McpAuthStatus, };
@@ -23,7 +23,7 @@ import type { PatchApplyStatus } from "./PatchApplyStatus";
import type { UserInput } from "./UserInput";
import type { WebSearchAction } from "./WebSearchAction";
export type ThreadItem = { "type": "userMessage", id: string, content: Array<UserInput>, } | { "type": "hookPrompt", id: string, fragments: Array<HookPromptFragment>, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, memoryCitation: MemoryCitation | null, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array<string>, content: Array<string>, } | { "type": "commandExecution", id: string,
export type ThreadItem = { "type": "userMessage", id: string, clientId: string | null, content: Array<UserInput>, } | { "type": "hookPrompt", id: string, fragments: Array<HookPromptFragment>, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, memoryCitation: MemoryCitation | null, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array<string>, content: Array<string>, } | { "type": "commandExecution", id: string,
/**
* The command to be executed.
*/
@@ -0,0 +1,19 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { SortDirection } from "./SortDirection";
import type { TurnItemsView } from "./TurnItemsView";
export type ThreadResumeInitialTurnsPageParams = {
/**
* Optional turn page size.
*/
limit?: number | null,
/**
* Optional turn pagination direction; defaults to descending.
*/
sortDirection?: SortDirection | null,
/**
* How much item detail to include for each returned turn; defaults to summary.
*/
itemsView?: TurnItemsView | null, };
@@ -10,7 +10,7 @@ import type { AskForApproval } from "./AskForApproval";
import type { SandboxPolicy } from "./SandboxPolicy";
import type { UserInput } from "./UserInput";
export type TurnStartParams = {threadId: string, input: Array<UserInput>, /**
export type TurnStartParams = {threadId: string, clientUserMessageId?: string | null, input: Array<UserInput>, /**
* Override the working directory for this turn and subsequent turns.
*/
cwd?: string | null, /**
@@ -3,7 +3,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { UserInput } from "./UserInput";
export type TurnSteerParams = {threadId: string, input: Array<UserInput>, /**
export type TurnSteerParams = {threadId: string, clientUserMessageId?: string | null, input: Array<UserInput>, /**
* Required active turn id precondition. The request fails when it does not
* match the currently active turn.
*/
@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Turn } from "./Turn";
export type TurnsPage = { data: Array<Turn>, nextCursor: string | null, backwardsCursor: string | null, };
@@ -398,6 +398,7 @@ export type { ThreadRealtimeStartTransport } from "./ThreadRealtimeStartTranspor
export type { ThreadRealtimeStartedNotification } from "./ThreadRealtimeStartedNotification";
export type { ThreadRealtimeTranscriptDeltaNotification } from "./ThreadRealtimeTranscriptDeltaNotification";
export type { ThreadRealtimeTranscriptDoneNotification } from "./ThreadRealtimeTranscriptDoneNotification";
export type { ThreadResumeInitialTurnsPageParams } from "./ThreadResumeInitialTurnsPageParams";
export type { ThreadResumeParams } from "./ThreadResumeParams";
export type { ThreadResumeResponse } from "./ThreadResumeResponse";
export type { ThreadRollbackParams } from "./ThreadRollbackParams";
@@ -450,6 +451,7 @@ export type { TurnStartedNotification } from "./TurnStartedNotification";
export type { TurnStatus } from "./TurnStatus";
export type { TurnSteerParams } from "./TurnSteerParams";
export type { TurnSteerResponse } from "./TurnSteerResponse";
export type { TurnsPage } from "./TurnsPage";
export type { UserInput } from "./UserInput";
export type { WarningNotification } from "./WarningNotification";
export type { WebSearchAction } from "./WebSearchAction";
+8 -16
View File
@@ -948,10 +948,8 @@ impl ScanState {
'(' => self.depth.paren += 1,
')' => self.depth.paren = (self.depth.paren - 1).max(0),
'<' => self.depth.angle += 1,
'>' => {
if self.depth.angle > 0 {
self.depth.angle -= 1;
}
'>' if self.depth.angle > 0 => {
self.depth.angle -= 1;
}
_ => {}
}
@@ -2212,20 +2210,14 @@ mod tests {
continue;
}
match ch {
'\\' => {
if in_single || in_double {
escape = true;
}
'\\' if (in_single || in_double) => {
escape = true;
}
'\'' => {
if !in_double {
in_single = !in_single;
}
'\'' if !in_double => {
in_single = !in_single;
}
'"' => {
if !in_single {
in_double = !in_double;
}
'"' if !in_single => {
in_double = !in_double;
}
'{' if !in_single && !in_double => level_brace += 1,
'}' if !in_single && !in_double => level_brace -= 1,
@@ -278,7 +278,11 @@ impl ThreadHistoryBuilder {
.unwrap_or_else(|| self.new_turn(/*id*/ None));
let id = self.next_item_id();
let content = self.build_user_inputs(payload);
turn.items.push(ThreadItem::UserMessage { id, content });
turn.items.push(ThreadItem::UserMessage {
id,
client_id: payload.client_id.clone(),
content,
});
self.current_turn = Some(turn);
}
@@ -1246,6 +1250,7 @@ mod tests {
fn builds_multiple_turns_with_reasoning_items() {
let events = vec![
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "First turn".into(),
images: Some(vec!["https://example.com/one.png".into()]),
text_elements: Vec::new(),
@@ -1264,6 +1269,7 @@ mod tests {
text: "full reasoning".into(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "Second turn".into(),
images: None,
text_elements: Vec::new(),
@@ -1292,6 +1298,7 @@ mod tests {
first.items[0],
ThreadItem::UserMessage {
id: "item-1".into(),
client_id: None,
content: vec![
UserInput::Text {
text: "First turn".into(),
@@ -1330,6 +1337,7 @@ mod tests {
second.items[0],
ThreadItem::UserMessage {
id: "item-4".into(),
client_id: None,
content: vec![UserInput::Text {
text: "Second turn".into(),
text_elements: Vec::new(),
@@ -1352,6 +1360,7 @@ mod tests {
let local_path = PathBuf::from("/tmp/local.png");
let events = vec![RolloutItem::EventMsg(EventMsg::UserMessage(
UserMessageEvent {
client_id: None,
message: "inspect these".into(),
images: Some(vec!["https://example.com/image.png".into()]),
image_details: vec![Some(ImageDetail::Original)],
@@ -1368,6 +1377,7 @@ mod tests {
turns[0].items[0],
ThreadItem::UserMessage {
id: "item-1".into(),
client_id: None,
content: vec![
UserInput::Text {
text: "inspect these".into(),
@@ -1399,6 +1409,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "hello".into(),
images: None,
text_elements: Vec::new(),
@@ -1410,6 +1421,7 @@ mod tests {
turn_id: turn_id.to_string(),
item: CoreTurnItem::UserMessage(CoreUserMessageItem {
id: "user-item-id".to_string(),
client_id: None,
content: Vec::new(),
}),
started_at_ms: 0,
@@ -1434,6 +1446,7 @@ mod tests {
turns[0].items[0],
ThreadItem::UserMessage {
id: "item-1".into(),
client_id: None,
content: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
@@ -1442,6 +1455,67 @@ mod tests {
);
}
#[test]
fn preserves_user_message_client_id_from_legacy_event() {
let turn_id = "turn-1";
let thread_id = ThreadId::new();
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: turn_id.to_string(),
trace_id: None,
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
EventMsg::ItemStarted(ItemStartedEvent {
thread_id,
turn_id: turn_id.to_string(),
item: CoreTurnItem::UserMessage(CoreUserMessageItem {
id: "user-item-id".to_string(),
client_id: Some("client-message-1".to_string()),
content: vec![codex_protocol::user_input::UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
}),
started_at_ms: 0,
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: Some("client-message-1".to_string()),
message: "hello".into(),
images: None,
text_elements: Vec::new(),
local_images: Vec::new(),
..Default::default()
}),
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: turn_id.to_string(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
}),
];
let items = events
.into_iter()
.map(RolloutItem::EventMsg)
.collect::<Vec<_>>();
let turns = build_turns_from_rollout_items(&items);
assert_eq!(turns.len(), 1);
assert_eq!(
turns[0].items,
vec![ThreadItem::UserMessage {
id: "item-1".into(),
client_id: Some("client-message-1".to_string()),
content: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
}]
);
}
#[test]
fn preserves_agent_message_phase_in_history() {
let events = vec![EventMsg::AgentMessage(AgentMessageEvent {
@@ -1478,6 +1552,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
})),
RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "generate an image".into(),
images: None,
text_elements: Vec::new(),
@@ -1515,6 +1590,7 @@ mod tests {
items: vec![
ThreadItem::UserMessage {
id: "item-1".into(),
client_id: None,
content: vec![UserInput::Text {
text: "generate an image".into(),
text_elements: Vec::new(),
@@ -1536,6 +1612,7 @@ mod tests {
fn splits_reasoning_when_interleaved() {
let events = vec![
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "Turn start".into(),
images: None,
text_elements: Vec::new(),
@@ -1589,6 +1666,7 @@ mod tests {
fn marks_turn_as_interrupted_when_aborted() {
let events = vec![
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "Please do the thing".into(),
images: None,
text_elements: Vec::new(),
@@ -1607,6 +1685,7 @@ mod tests {
duration_ms: None,
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "Let's try again".into(),
images: None,
text_elements: Vec::new(),
@@ -1634,6 +1713,7 @@ mod tests {
first_turn.items[0],
ThreadItem::UserMessage {
id: "item-1".into(),
client_id: None,
content: vec![UserInput::Text {
text: "Please do the thing".into(),
text_elements: Vec::new(),
@@ -1657,6 +1737,7 @@ mod tests {
second_turn.items[0],
ThreadItem::UserMessage {
id: "item-3".into(),
client_id: None,
content: vec![UserInput::Text {
text: "Let's try again".into(),
text_elements: Vec::new(),
@@ -1678,6 +1759,7 @@ mod tests {
fn drops_last_turns_on_thread_rollback() {
let events = vec![
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "First".into(),
images: None,
text_elements: Vec::new(),
@@ -1690,6 +1772,7 @@ mod tests {
memory_citation: None,
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "Second".into(),
images: None,
text_elements: Vec::new(),
@@ -1703,6 +1786,7 @@ mod tests {
}),
EventMsg::ThreadRolledBack(ThreadRolledBackEvent { num_turns: 1 }),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "Third".into(),
images: None,
text_elements: Vec::new(),
@@ -1732,6 +1816,7 @@ mod tests {
vec![
ThreadItem::UserMessage {
id: "item-1".into(),
client_id: None,
content: vec![UserInput::Text {
text: "First".into(),
text_elements: Vec::new(),
@@ -1750,6 +1835,7 @@ mod tests {
vec![
ThreadItem::UserMessage {
id: "item-3".into(),
client_id: None,
content: vec![UserInput::Text {
text: "Third".into(),
text_elements: Vec::new(),
@@ -1769,6 +1855,7 @@ mod tests {
fn thread_rollback_clears_all_turns_when_num_turns_exceeds_history() {
let events = vec![
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "One".into(),
images: None,
text_elements: Vec::new(),
@@ -1781,6 +1868,7 @@ mod tests {
memory_citation: None,
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "Two".into(),
images: None,
text_elements: Vec::new(),
@@ -1814,6 +1902,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "Start".into(),
images: None,
text_elements: Vec::new(),
@@ -1821,6 +1910,7 @@ mod tests {
..Default::default()
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "Steer".into(),
images: None,
text_elements: Vec::new(),
@@ -1848,6 +1938,7 @@ mod tests {
vec![
ThreadItem::UserMessage {
id: "item-1".into(),
client_id: None,
content: vec![UserInput::Text {
text: "Start".into(),
text_elements: Vec::new(),
@@ -1855,6 +1946,7 @@ mod tests {
},
ThreadItem::UserMessage {
id: "item-2".into(),
client_id: None,
content: vec![UserInput::Text {
text: "Steer".into(),
text_elements: Vec::new(),
@@ -1875,6 +1967,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "run tools".into(),
images: None,
text_elements: Vec::new(),
@@ -2054,6 +2147,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "run dynamic tool".into(),
images: None,
text_elements: Vec::new(),
@@ -2121,6 +2215,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "run tools".into(),
images: None,
text_elements: Vec::new(),
@@ -2212,6 +2307,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "review this command".into(),
images: None,
text_elements: Vec::new(),
@@ -2297,6 +2393,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "run a subcommand".into(),
images: None,
text_elements: Vec::new(),
@@ -2362,6 +2459,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "first".into(),
images: None,
text_elements: Vec::new(),
@@ -2383,6 +2481,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "second".into(),
images: None,
text_elements: Vec::new(),
@@ -2458,6 +2557,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "first".into(),
images: None,
text_elements: Vec::new(),
@@ -2479,6 +2579,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "second".into(),
images: None,
text_elements: Vec::new(),
@@ -2528,6 +2629,7 @@ mod tests {
turns[1].items[0],
ThreadItem::UserMessage {
id: "item-2".into(),
client_id: None,
content: vec![UserInput::Text {
text: "second".into(),
text_elements: Vec::new(),
@@ -2549,6 +2651,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "apply patch".into(),
images: None,
text_elements: Vec::new(),
@@ -2584,6 +2687,7 @@ mod tests {
vec![
ThreadItem::UserMessage {
id: "item-1".into(),
client_id: None,
content: vec![UserInput::Text {
text: "apply patch".into(),
text_elements: Vec::new(),
@@ -2615,6 +2719,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "apply patch".into(),
images: None,
text_elements: Vec::new(),
@@ -2652,6 +2757,7 @@ mod tests {
vec![
ThreadItem::UserMessage {
id: "item-1".into(),
client_id: None,
content: vec![UserInput::Text {
text: "apply patch".into(),
text_elements: Vec::new(),
@@ -2681,6 +2787,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "first".into(),
images: None,
text_elements: Vec::new(),
@@ -2702,6 +2809,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "second".into(),
images: None,
text_elements: Vec::new(),
@@ -2751,6 +2859,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "first".into(),
images: None,
text_elements: Vec::new(),
@@ -2772,6 +2881,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "second".into(),
images: None,
text_elements: Vec::new(),
@@ -2846,6 +2956,7 @@ mod tests {
fn reconstructs_collab_resume_end_item() {
let events = vec![
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "resume agent".into(),
images: None,
text_elements: Vec::new(),
@@ -2904,6 +3015,7 @@ mod tests {
.expect("valid receiver thread id");
let events = vec![
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "spawn agent".into(),
images: None,
text_elements: Vec::new(),
@@ -2966,6 +3078,7 @@ mod tests {
.expect("valid receiver thread id");
let events = vec![
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "redirect".into(),
images: None,
text_elements: Vec::new(),
@@ -3030,6 +3143,7 @@ mod tests {
fn rollback_failed_error_does_not_mark_turn_failed() {
let events = vec![
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "hello".into(),
images: None,
text_elements: Vec::new(),
@@ -3068,6 +3182,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "hello".into(),
images: None,
text_elements: Vec::new(),
@@ -3105,6 +3220,7 @@ mod tests {
items_view: TurnItemsView::Full,
items: vec![ThreadItem::UserMessage {
id: "item-1".into(),
client_id: None,
content: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
@@ -3125,6 +3241,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "hello".into(),
images: None,
text_elements: Vec::new(),
@@ -3184,6 +3301,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
})),
RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "hello".into(),
images: None,
text_elements: Vec::new(),
@@ -212,7 +212,11 @@ impl CommandAction {
pub enum ThreadItem {
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
UserMessage { id: String, content: Vec<UserInput> },
UserMessage {
id: String,
client_id: Option<String>,
content: Vec<UserInput>,
},
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
HookPrompt {
@@ -776,6 +780,7 @@ impl From<CoreTurnItem> for ThreadItem {
match value {
CoreTurnItem::UserMessage(user) => ThreadItem::UserMessage {
id: user.id,
client_id: user.client_id,
content: user.content.into_iter().map(UserInput::from).collect(),
},
CoreTurnItem::HookPrompt(hook_prompt) => ThreadItem::HookPrompt {
@@ -2,6 +2,7 @@ use super::shared::v2_enum_from_core;
use codex_protocol::approvals::ElicitationRequest as CoreElicitationRequest;
use codex_protocol::items::McpToolCallError as CoreMcpToolCallError;
use codex_protocol::mcp::CallToolResult as CoreMcpCallToolResult;
use codex_protocol::mcp::McpServerInfo;
use codex_protocol::mcp::Resource as McpResource;
pub use codex_protocol::mcp::ResourceContent as McpResourceContent;
use codex_protocol::mcp::ResourceTemplate as McpResourceTemplate;
@@ -53,6 +54,7 @@ pub enum McpServerStatusDetail {
#[ts(export_to = "v2/")]
pub struct McpServerStatus {
pub name: String,
pub server_info: Option<McpServerInfo>,
pub tools: std::collections::HashMap<String, McpTool>,
pub resources: Vec<McpResource>,
pub resource_templates: Vec<McpResourceTemplate>,
@@ -690,6 +692,7 @@ impl From<McpServerElicitationRequestResponse> for rmcp::model::CreateElicitatio
Self {
action: value.action.into(),
content: value.content,
meta: None,
}
}
}
@@ -11,6 +11,7 @@ use codex_protocol::items::TurnItem;
use codex_protocol::items::UserMessageItem;
use codex_protocol::items::WebSearchItem;
use codex_protocol::mcp::CallToolResult;
use codex_protocol::mcp::McpServerInfo;
use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation;
use codex_protocol::memory_citation::MemoryCitationEntry as CoreMemoryCitationEntry;
use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile;
@@ -111,6 +112,85 @@ fn thread_turns_list_params_accepts_items_view() {
assert_eq!(params.items_view, Some(TurnItemsView::NotLoaded));
}
#[test]
fn thread_resume_params_accept_turns_page_bootstrap() {
let params = serde_json::from_value::<ThreadResumeParams>(json!({
"threadId": "thr_123",
"initialTurnsPage": {
"limit": 25,
"sortDirection": "asc",
"itemsView": "full",
},
}))
.expect("thread resume params should deserialize");
assert_eq!(params.thread_id, "thr_123");
assert_eq!(
params.initial_turns_page,
Some(ThreadResumeInitialTurnsPageParams {
limit: Some(25),
sort_direction: Some(SortDirection::Asc),
items_view: Some(TurnItemsView::Full),
})
);
}
#[test]
fn thread_resume_response_round_trips_initial_turns_page() {
let response = ThreadResumeResponse {
thread: Thread {
id: "thr_123".to_string(),
session_id: "thr_123".to_string(),
forked_from_id: None,
preview: String::new(),
ephemeral: false,
model_provider: "openai".to_string(),
created_at: 1,
updated_at: 1,
status: ThreadStatus::Idle,
path: None,
cwd: absolute_path("tmp"),
cli_version: "0.0.0".to_string(),
source: SessionSource::Exec,
thread_source: None,
agent_nickname: None,
agent_role: None,
git_info: None,
name: None,
turns: Vec::new(),
},
model: "gpt-5".to_string(),
model_provider: "openai".to_string(),
service_tier: None,
cwd: absolute_path("tmp"),
runtime_workspace_roots: Vec::new(),
instruction_sources: Vec::new(),
approval_policy: AskForApproval::OnFailure,
approvals_reviewer: ApprovalsReviewer::User,
sandbox: SandboxPolicy::DangerFullAccess,
active_permission_profile: None,
reasoning_effort: None,
initial_turns_page: Some(TurnsPage {
data: Vec::new(),
next_cursor: Some("cursor_next".to_string()),
backwards_cursor: Some("cursor_back".to_string()),
}),
};
let value = serde_json::to_value(&response).expect("serialize thread resume response");
assert_eq!(
value.get("initialTurnsPage"),
Some(&json!({
"data": [],
"nextCursor": "cursor_next",
"backwardsCursor": "cursor_back",
}))
);
let decoded = serde_json::from_value::<ThreadResumeResponse>(value)
.expect("deserialize thread resume response");
assert_eq!(decoded, response);
}
#[test]
fn thread_turns_items_list_round_trips() {
let params = ThreadTurnsItemsListParams {
@@ -1676,6 +1756,7 @@ fn client_request_turn_start_granular_approval_policy_is_marked_experimental() {
request_id: crate::RequestId::Integer(4),
params: TurnStartParams {
thread_id: "thr_123".to_string(),
client_user_message_id: None,
input: Vec::new(),
approval_policy: Some(AskForApproval::Granular {
sandbox_approval: false,
@@ -1699,6 +1780,7 @@ fn mcp_server_elicitation_response_round_trips_rmcp_result() {
content: Some(json!({
"confirmed": true,
})),
meta: None,
};
let v2_response = McpServerElicitationRequestResponse::from(rmcp_result.clone());
@@ -1923,6 +2005,80 @@ fn mcp_server_elicitation_response_serializes_nullable_content() {
);
}
#[test]
fn mcp_server_status_serializes_absent_server_info_as_null() {
let response = ListMcpServerStatusResponse {
data: vec![McpServerStatus {
name: "not-ready".to_string(),
server_info: None,
tools: HashMap::new(),
resources: Vec::new(),
resource_templates: Vec::new(),
auth_status: McpAuthStatus::Unsupported,
}],
next_cursor: None,
};
assert_eq!(
serde_json::to_value(response).expect("response should serialize"),
json!({
"data": [{
"name": "not-ready",
"serverInfo": null,
"tools": {},
"resources": [],
"resourceTemplates": [],
"authStatus": "unsupported",
}],
"nextCursor": null,
})
);
}
#[test]
fn mcp_server_status_serializes_absent_server_info_metadata_as_null() {
let response = ListMcpServerStatusResponse {
data: vec![McpServerStatus {
name: "initialized".to_string(),
server_info: Some(McpServerInfo {
name: "lookup-server".to_string(),
title: None,
version: "1.0.0".to_string(),
description: None,
icons: None,
website_url: None,
}),
tools: HashMap::new(),
resources: Vec::new(),
resource_templates: Vec::new(),
auth_status: McpAuthStatus::Unsupported,
}],
next_cursor: None,
};
assert_eq!(
serde_json::to_value(response).expect("response should serialize"),
json!({
"data": [{
"name": "initialized",
"serverInfo": {
"name": "lookup-server",
"title": null,
"version": "1.0.0",
"description": null,
"icons": null,
"websiteUrl": null,
},
"tools": {},
"resources": [],
"resourceTemplates": [],
"authStatus": "unsupported",
}],
"nextCursor": null,
})
);
}
#[test]
fn sandbox_policy_round_trips_workspace_write_access() {
let v2_policy = SandboxPolicy::WorkspaceWrite {
@@ -2159,6 +2315,7 @@ fn network_requirements_serializes_canonical_and_legacy_fields() {
fn core_turn_item_into_thread_item_converts_supported_variants() {
let user_item = TurnItem::UserMessage(UserMessageItem {
id: "user-1".to_string(),
client_id: Some("client-message-1".to_string()),
content: vec![
CoreUserInput::Text {
text: "hello".to_string(),
@@ -2187,6 +2344,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
ThreadItem::from(user_item),
ThreadItem::UserMessage {
id: "user-1".to_string(),
client_id: Some("client-message-1".to_string()),
content: vec![
UserInput::Text {
text: "hello".to_string(),
@@ -3405,6 +3563,7 @@ fn thread_lifecycle_responses_default_missing_optional_fields() {
assert_eq!(fork.instruction_sources, Vec::<AbsolutePathBuf>::new());
assert_eq!(start.active_permission_profile, None);
assert_eq!(resume.active_permission_profile, None);
assert_eq!(resume.initial_turns_page, None);
assert_eq!(fork.active_permission_profile, None);
}
@@ -3426,6 +3585,7 @@ fn turn_start_params_preserve_explicit_null_service_tier() {
let without_override = TurnStartParams {
thread_id: "thread_123".to_string(),
client_user_message_id: None,
input: vec![],
responsesapi_client_metadata: None,
additional_context: None,
@@ -397,6 +397,11 @@ pub struct ThreadResumeParams {
#[experimental("thread/resume.excludeTurns")]
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub exclude_turns: bool,
/// When present, include a `thread/turns/list` page in the resume response
/// so clients can bootstrap recent turns without a second request.
#[experimental("thread/resume.initialTurnsPage")]
#[ts(optional = nullable)]
pub initial_turns_page: Option<ThreadResumeInitialTurnsPageParams>,
/// Deprecated and ignored by app-server. Kept only so older clients can
/// continue sending the field while rollout persistence always uses the
/// limited history policy.
@@ -435,6 +440,44 @@ pub struct ThreadResumeResponse {
#[serde(default)]
pub active_permission_profile: Option<ActivePermissionProfile>,
pub reasoning_effort: Option<ReasoningEffort>,
/// `thread/turns/list` page returned when requested by `initialTurnsPage`.
#[experimental("thread/resume.initialTurnsPage")]
#[serde(default)]
pub initial_turns_page: Option<TurnsPage>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadResumeInitialTurnsPageParams {
/// Optional turn page size.
#[ts(optional = nullable)]
pub limit: Option<u32>,
/// Optional turn pagination direction; defaults to descending.
#[ts(optional = nullable)]
pub sort_direction: Option<SortDirection>,
/// How much item detail to include for each returned turn; defaults to summary.
#[ts(optional = nullable)]
pub items_view: Option<TurnItemsView>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct TurnsPage {
pub data: Vec<Turn>,
pub next_cursor: Option<String>,
pub backwards_cursor: Option<String>,
}
impl From<ThreadTurnsListResponse> for TurnsPage {
fn from(response: ThreadTurnsListResponse) -> Self {
Self {
data: response.data,
next_cursor: response.next_cursor,
backwards_cursor: response.backwards_cursor,
}
}
}
#[derive(
@@ -65,6 +65,8 @@ pub struct AdditionalContextEntry {
#[ts(export_to = "v2/")]
pub struct TurnStartParams {
pub thread_id: String,
#[ts(optional = nullable)]
pub client_user_message_id: Option<String>,
pub input: Vec<UserInput>,
/// Optional turn-scoped Responses API client metadata.
#[experimental("turn/start.responsesapiClientMetadata")]
@@ -157,6 +159,8 @@ pub struct TurnStartResponse {
#[ts(export_to = "v2/")]
pub struct TurnSteerParams {
pub thread_id: String,
#[ts(optional = nullable)]
pub client_user_message_id: Option<String>,
pub input: Vec<UserInput>,
/// Optional turn-scoped Responses API client metadata.
#[experimental("turn/steer.responsesapiClientMetadata")]
@@ -196,7 +196,7 @@ fn canonicalize_json(value: &Value) -> Value {
}
Value::Object(map) => {
let mut entries: Vec<_> = map.iter().collect();
entries.sort_by(|(left, _), (right, _)| left.cmp(right));
entries.sort_by_key(|(key, _)| *key);
let mut sorted = Map::with_capacity(map.len());
for (key, child) in entries {
sorted.insert(key.clone(), canonicalize_json(child));
@@ -734,6 +734,7 @@ async fn trigger_zsh_fork_multi_cmd_approval(
let mut turn_params = TurnStartParams {
thread_id: thread_response.thread.id.clone(),
client_user_message_id: None,
input: vec![V2UserInput::Text {
text: message,
text_elements: Vec::new(),
@@ -818,6 +819,7 @@ async fn resume_message_v2(
let turn_response = client.turn_start(TurnStartParams {
thread_id: resume_response.thread.id.clone(),
client_user_message_id: None,
input: vec![V2UserInput::Text {
text: user_message,
text_elements: Vec::new(),
@@ -959,6 +961,7 @@ async fn send_message_v2_with_policies(
println!("< thread/start response: {thread_response:?}");
let mut turn_params = TurnStartParams {
thread_id: thread_response.thread.id.clone(),
client_user_message_id: None,
input: vec![V2UserInput::Text {
text: user_message,
// Test client sends plain text without UI element ranges.
@@ -999,6 +1002,7 @@ async fn send_follow_up_v2(
let first_turn_params = TurnStartParams {
thread_id: thread_response.thread.id.clone(),
client_user_message_id: None,
input: vec![V2UserInput::Text {
text: first_message,
// Test client sends plain text without UI element ranges.
@@ -1012,6 +1016,7 @@ async fn send_follow_up_v2(
let follow_up_params = TurnStartParams {
thread_id: thread_response.thread.id.clone(),
client_user_message_id: None,
input: vec![V2UserInput::Text {
text: follow_up_message,
// Test client sends plain text without UI element ranges.
@@ -1255,6 +1260,7 @@ fn live_elicitation_timeout_pause(
let started_at = Instant::now();
let turn_response = client.turn_start(TurnStartParams {
thread_id: thread_id.clone(),
client_user_message_id: None,
input: vec![V2UserInput::Text {
text: prompt,
text_elements: Vec::new(),
@@ -1390,14 +1390,13 @@ pub(super) async fn connect_remote_control_websocket(
status_publisher.publish_environment_id(/*environment_id*/ None);
}
tungstenite::Error::Http(response)
if matches!(response.status().as_u16(), 401 | 403) =>
if matches!(response.status().as_u16(), 401 | 403)
&& recover_remote_control_auth(auth_recovery, auth_change_rx).await =>
{
if recover_remote_control_auth(auth_recovery, auth_change_rx).await {
return Err(io::Error::other(format!(
"remote control websocket auth failed with HTTP {}; retrying after auth recovery",
response.status()
)));
}
return Err(io::Error::other(format!(
"remote control websocket auth failed with HTTP {}; retrying after auth recovery",
response.status()
)));
}
_ => {}
}
@@ -347,7 +347,7 @@ async fn run_websocket_inbound_loop<M, StreamError>(
incoming_message = websocket_reader.next() => {
match incoming_message {
Some(Ok(message)) => match message.into_incoming() {
Some(IncomingWebSocketMessage::Text(text)) => {
Some(IncomingWebSocketMessage::Text(text))
if !forward_incoming_message(
&transport_event_tx,
&writer_tx_for_reader,
@@ -355,10 +355,10 @@ async fn run_websocket_inbound_loop<M, StreamError>(
&text,
)
.await
{
break;
}
=> {
break;
}
Some(IncomingWebSocketMessage::Text(_)) => {}
Some(IncomingWebSocketMessage::Ping(payload)) => {
match writer_control_tx.try_send(M::pong(payload)) {
Ok(()) => {}
+4
View File
@@ -54,6 +54,7 @@ codex-backend-client = { workspace = true }
codex-file-search = { workspace = true }
codex-chatgpt = { workspace = true }
codex-login = { workspace = true }
codex-image-generation-extension = { workspace = true }
codex-memories-extension = { workspace = true }
codex-web-search-extension = { workspace = true }
codex-memories-write = { workspace = true }
@@ -94,6 +95,9 @@ tracing = { workspace = true, features = ["log"] }
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt", "json"] }
uuid = { workspace = true, features = ["serde", "v7"] }
[target.'cfg(windows)'.dependencies]
codex-windows-sandbox = { workspace = true }
[dev-dependencies]
app_test_support = { workspace = true }
axum = { workspace = true, default-features = false, features = [
+28 -6
View File
@@ -23,7 +23,7 @@ Similar to [MCP](https://modelcontextprotocol.io/), `codex app-server` supports
Supported transports:
- stdio (`--listen stdio://`, default): newline-delimited JSON (JSONL)
- stdio (`--stdio` or `--listen stdio://`, default): newline-delimited JSON (JSONL)
- websocket (`--listen ws://IP:PORT`): one JSON-RPC message per websocket text frame (**experimental / unsupported**)
- unix socket (`--listen unix://` or `--listen unix://PATH`): websocket connections over `$CODEX_HOME/app-server-control/app-server-control.sock` or a custom socket path, using the standard HTTP Upgrade handshake
- off (`--listen off`): do not expose a local transport
@@ -158,9 +158,9 @@ Example with notification opt-out:
- `thread/shellCommand` — run a user-initiated `!` shell command against a thread; this runs unsandboxed with full access rather than inheriting the thread sandbox policy. Returns `{}` immediately while progress streams through standard turn/item notifications and any active turn receives the formatted output in its message stream.
- `thread/backgroundTerminals/clean` — terminate all running background terminals for a thread (experimental; requires `capabilities.experimentalApi`); returns `{}` when the cleanup request is accepted.
- `thread/rollback` — drop the last N turns from the agent’s in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success.
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; relative paths resolve against the effective turn cwd. Prefer experimental `permissions` profile selection by id for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissions`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode".
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; relative paths resolve against the effective turn cwd. Prefer experimental `permissions` profile selection by id for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissions`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode".
- `thread/inject_items` — append raw Responses API items to a loaded thread’s model-visible history without starting a user turn; returns `{}` on success.
- `turn/steer` — add user input to an already in-flight regular turn without starting a new turn; returns the active `turnId` that accepted the input. Review and manual compaction turns reject `turn/steer`.
- `turn/steer` — add user input to an already in-flight regular turn without starting a new turn; returns the active `turnId` that accepted the input. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Review and manual compaction turns reject `turn/steer`.
- `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`.
- `thread/realtime/start` — start a thread-scoped realtime session (experimental); pass `outputModality: "text"` or `outputModality: "audio"` to choose model output, returns `{}` and streams `thread/realtime/*` notifications. Omit `transport` for the websocket transport, or pass `{ "type": "webrtc", "sdp": "..." }` to create a WebRTC session from a browser-generated SDP offer; the remote answer SDP is emitted as `thread/realtime/sdp`.
- `thread/realtime/appendAudio` — append an input audio chunk to the active realtime session (experimental); returns `{}`.
@@ -192,7 +192,7 @@ Example with notification opt-out:
- `modelProvider/capabilities/read` — read provider-level capabilities for the currently configured model provider.
- `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. Pass `threadId` when showing feature state for an existing loaded thread so `enabled` is computed from that thread's refreshed config, including project-local config for the thread's cwd; if omitted, the server uses its default config resolution context. For non-beta flags, `displayName`/`description`/`announcement` are `null`.
- `permissionProfile/list` — beta; list available permission profile ids with optional display `description` text, using cursor pagination. Pass `cwd` when the caller needs project-local `[permissions.<id>]` entries to be included in the current catalog view.
- `experimentalFeature/enablement/set` — patch the in-memory process-wide runtime feature enablement for the currently supported feature keys (`apps`, `memories`, `plugins`, `tool_suggest`, `tool_call_mcp_elicitation`). For each feature, precedence is: cloud requirements > --enable <feature_name> > config.toml > experimentalFeature/enablement/set (new) > code default.
- `experimentalFeature/enablement/set` — patch the in-memory process-wide runtime feature enablement for currently supported feature keys. For each feature, precedence is: cloud requirements > --enable <feature_name> > config.toml > experimentalFeature/enablement/set (new) > code default.
- `environment/add` — experimental; add or replace a named remote environment by `environmentId` and `execServerUrl` for later selection by `thread/start` or `turn/start`; returns `{}` and does not change the default environment.
- `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). Built-in presets do not select a model; the Plan preset selects medium reasoning effort. This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly.
- `skills/list` — list skills for one or more `cwd` values (optional `forceReload`).
@@ -216,7 +216,7 @@ Example with notification opt-out:
- `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; returns an `authorization_url` and later emits `mcpServer/oauthLogin/completed` once the browser flow finishes.
- `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental).
- `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server.
- `mcpServerStatus/list` — enumerate configured MCP servers with their tools and auth status, plus resources/resource templates for `full` detail; supports optional `threadId` and cursor+limit pagination. If `threadId` is omitted, the server reads from the latest global config directly. If `detail` is omitted, the server defaults to `full`.
- `mcpServerStatus/list` — enumerate configured MCP servers with their tools, auth status, server info, plus resources/resource templates for `full` detail; supports optional `threadId` and cursor+limit pagination. If `threadId` is omitted, the server reads from the latest global config directly. If `detail` is omitted, the server defaults to `full`.
- `mcpServer/resource/read` — read a resource from a configured MCP server by optional `threadId`, `server`, and `uri`, returning text/blob resource `contents`. If `threadId` is omitted, the server reads from the latest MCP config directly.
- `mcpServer/tool/call` — call a tool on a thread's configured MCP server by `threadId`, `server`, `tool`, optional `arguments`, and optional `_meta`, returning the MCP tool result.
- `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); accepts an optional absolute `cwd` to target setup for a specific workspace, returns `{ started: true }` immediately, and later emits `windowsSandbox/setupCompleted`.
@@ -281,6 +281,8 @@ To continue a stored session, call `thread/resume` with the `thread.id` you prev
By default, `thread/resume` includes the reconstructed turn history in `thread.turns`. Experimental clients can pass `excludeTurns: true` to return only thread metadata and live resume state, then call `thread/turns/list` separately if they want to page the turn history over the network. In that mode the server also skips replaying restored `thread/tokenUsage/updated`, which avoids rebuilding turns just to attribute historical usage.
Experimental clients that want the live resume subscription plus a turns page in one round trip can pass `initialTurnsPage`. It accepts the same `limit`, `sortDirection`, and `itemsView` controls as `thread/turns/list`; omitted controls use its defaults. The response includes `initialTurnsPage` with `nextCursor` and `backwardsCursor` for follow-up pagination.
By default, resume uses the latest persisted `model` and `reasoningEffort` values associated with the thread. Supplying any of `model`, `modelProvider`, `config.model`, or `config.model_reasoning_effort` disables that persisted fallback and uses the explicit overrides plus normal config resolution instead.
Example:
@@ -297,6 +299,24 @@ Example:
"excludeTurns": true
} }
{ "id": 12, "result": { "thread": { "id": "thr_123", "turns": [], … } } }
{ "method": "thread/resume", "id": 13, "params": {
"threadId": "thr_123",
"excludeTurns": true,
"initialTurnsPage": {
"limit": 20,
"sortDirection": "desc",
"itemsView": "summary"
}
} }
{ "id": 13, "result": {
"thread": { "id": "thr_123", "turns": [], … },
"initialTurnsPage": {
"data": [ ... ],
"nextCursor": "older-turns-cursor-or-null",
"backwardsCursor": "newer-turns-cursor-or-null"
}
} }
```
To branch from a stored session, call `thread/fork` with the `thread.id`. This creates a new thread id and emits a `thread/started` notification for it. The returned `thread.sessionId` identifies the current live session tree root. Root threads use their own `thread.id` as `thread.sessionId`; stored threads that are not loaded also report their own `thread.id`, because resuming one makes it the root of a new live session tree. When the source history includes persisted token usage, the server also emits `thread/tokenUsage/updated` for the new thread immediately after the response. If the source thread is actively running, the fork snapshots it as if the current turn had been interrupted first. Pass `ephemeral: true` when the fork should stay in-memory only:
@@ -642,6 +662,7 @@ You can optionally specify config overrides on the new turn. If specified, these
```json
{ "method": "turn/start", "id": 30, "params": {
"threadId": "thr_123",
"clientUserMessageId": "client_msg_123",
"input": [ { "type": "text", "text": "Run tests" } ],
// Below are optional config overrides
"cwd": "/Users/me/project",
@@ -840,6 +861,7 @@ not emit `turn/started` and does not accept thread settings overrides.
```json
{ "method": "turn/steer", "id": 32, "params": {
"threadId": "thr_123",
"clientUserMessageId": "client_msg_124",
"input": [ { "type": "text", "text": "Actually focus on failing tests first." } ],
"expectedTurnId": "turn_456"
} }
@@ -1225,7 +1247,7 @@ Today both notifications carry an empty `items` array even when item events were
`ThreadItem` is the tagged union carried in turn responses and `item/*` notifications. Currently we support events for the following items:
- `userMessage` — `{id, content}` where `content` is a list of user inputs (`text`, `image`, or `localImage`).
- `userMessage` — `{id, clientId, content}` where `clientId` is the optional `clientUserMessageId` supplied to `turn/start` or `turn/steer`, and `content` is a list of user inputs (`text`, `image`, or `localImage`).
- `agentMessage` — `{id, text}` containing the accumulated agent reply.
- `plan` — `{id, text}` emitted for plan-mode turns; plan text can stream via `item/plan/delta` (experimental).
- `reasoning` — `{id, summary, content}` where `summary` holds streamed reasoning summaries (applicable for most OpenAI models) and `content` holds raw reasoning blocks (applicable for e.g. open source models).
@@ -2153,6 +2153,7 @@ mod tests {
let created_at = Utc::now();
let history_items = vec![
RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "before rollback".to_string(),
images: None,
local_images: Vec::new(),
@@ -3228,6 +3229,7 @@ mod tests {
state.track_current_turn_event(
"turn-1",
&EventMsg::UserMessage(codex_protocol::protocol::UserMessageEvent {
client_id: None,
message: "already tracked".to_string(),
images: None,
local_images: Vec::new(),
+2 -1
View File
@@ -31,7 +31,8 @@ where
let mut builder = ExtensionRegistryBuilder::<Config>::with_event_sink(event_sink);
codex_guardian::install(&mut builder, guardian_agent_spawner);
codex_memories_extension::install(&mut builder, codex_otel::global());
codex_web_search_extension::install(&mut builder, auth_manager);
codex_web_search_extension::install(&mut builder, auth_manager.clone());
codex_image_generation_extension::install(&mut builder, auth_manager);
Arc::new(builder.build())
}
@@ -653,6 +653,7 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> {
params: TurnStartParams {
environments: None,
thread_id,
client_user_message_id: None,
input: vec![UserInput::Text {
text: "hello".to_string(),
text_elements: Vec::new(),
+2 -2
View File
@@ -456,8 +456,8 @@ impl OutgoingMessageSender {
) -> Vec<ServerRequest> {
let request_id_to_callback = self.request_id_to_callback.lock().await;
let mut requests = request_id_to_callback
.iter()
.filter_map(|(_, entry)| {
.values()
.filter_map(|entry| {
(entry.thread_id == Some(thread_id)).then_some(entry.request.clone())
})
.collect::<Vec<_>>();
@@ -213,6 +213,7 @@ use codex_app_server_protocol::ThreadRealtimeStartResponse;
use codex_app_server_protocol::ThreadRealtimeStartTransport;
use codex_app_server_protocol::ThreadRealtimeStopParams;
use codex_app_server_protocol::ThreadRealtimeStopResponse;
use codex_app_server_protocol::ThreadResumeInitialTurnsPageParams;
use codex_app_server_protocol::ThreadResumeParams;
use codex_app_server_protocol::ThreadResumeResponse;
use codex_app_server_protocol::ThreadRollbackParams;
@@ -54,6 +54,7 @@ const SUPPORTED_EXPERIMENTAL_FEATURE_ENABLEMENT: &[&str] = &[
"mentions_v2",
"plugins",
"remote_control",
"remote_plugin",
"tool_suggest",
"tool_call_mcp_elicitation",
];
@@ -1,4 +1,6 @@
use super::*;
#[cfg(target_os = "windows")]
use codex_feedback::WINDOWS_SANDBOX_LOG_ATTACHMENT_FILENAME;
#[derive(Clone)]
pub(crate) struct FeedbackRequestProcessor {
@@ -186,6 +188,12 @@ impl FeedbackRequestProcessor {
)),
});
}
if let Some(sandbox_log_attachment) =
windows_sandbox_log_attachment(&self.config.codex_home)
&& seen_attachment_paths.insert(sandbox_log_attachment.path.clone())
{
attachment_paths.push(sandbox_log_attachment);
}
}
if let Some(extra_log_files) = extra_log_files {
for extra_log_file in extra_log_files {
@@ -264,3 +272,46 @@ impl FeedbackRequestProcessor {
fn auto_review_rollout_filename(thread_id: ThreadId) -> String {
format!("auto-review-rollout-{thread_id}.jsonl")
}
#[cfg(target_os = "windows")]
fn windows_sandbox_log_attachment(codex_home: &Path) -> Option<FeedbackAttachmentPath> {
let sandbox_log_path = codex_windows_sandbox::current_log_file_path_for_codex_home(codex_home);
sandbox_log_path
.is_file()
.then_some(FeedbackAttachmentPath {
path: sandbox_log_path,
attachment_filename_override: Some(WINDOWS_SANDBOX_LOG_ATTACHMENT_FILENAME.to_string()),
})
}
#[cfg(not(target_os = "windows"))]
fn windows_sandbox_log_attachment(_codex_home: &Path) -> Option<FeedbackAttachmentPath> {
None
}
#[cfg(all(test, target_os = "windows"))]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn windows_sandbox_log_attachment_uses_current_log() {
let codex_home = tempfile::tempdir().expect("create tempdir");
let sandbox_dir = codex_windows_sandbox::sandbox_dir(codex_home.path());
std::fs::create_dir_all(&sandbox_dir).expect("create sandbox dir");
let sandbox_log_path =
codex_windows_sandbox::current_log_file_path_for_codex_home(codex_home.path());
std::fs::write(&sandbox_log_path, "sandbox log").expect("write sandbox log");
let attachment = windows_sandbox_log_attachment(codex_home.path())
.map(|attachment| (attachment.path, attachment.attachment_filename_override));
assert_eq!(
attachment,
Some((
sandbox_log_path,
Some(WINDOWS_SANDBOX_LOG_ATTACHMENT_FILENAME.to_string())
))
);
}
}
@@ -276,6 +276,7 @@ impl McpRequestProcessor {
.await;
let McpServerStatusSnapshot {
server_infos,
tools_by_server,
resources,
resource_templates,
@@ -315,6 +316,7 @@ impl McpRequestProcessor {
.iter()
.map(|name| McpServerStatus {
name: name.clone(),
server_info: server_infos.get(name).cloned(),
tools: tools_by_server.get(name).cloned().unwrap_or_default(),
resources: resources.get(name).cloned().unwrap_or_default(),
resource_templates: resource_templates.get(name).cloned().unwrap_or_default(),
@@ -565,8 +565,28 @@ pub(super) async fn handle_pending_thread_resume_request(
has_live_in_progress_turn,
);
let token_usage_thread = pending.include_turns.then(|| thread.clone());
let mut initial_turns_page = if let Some(params) = pending.initial_turns_page.as_ref() {
match super::thread_processor::build_thread_resume_initial_turns_page(
&pending.history_items,
thread.status.clone(),
has_live_in_progress_turn,
active_turn,
params,
) {
Ok(page) => Some(page),
Err(error) => {
outgoing.send_error(request_id, error).await;
return;
}
}
} else {
None
};
if pending.redact_resume_payloads {
redact_thread_resume_payloads(&mut thread);
redact_thread_resume_payloads(&mut thread.turns);
if let Some(initial_turns_page) = initial_turns_page.as_mut() {
redact_thread_resume_payloads(&mut initial_turns_page.data);
}
}
{
@@ -635,6 +655,7 @@ pub(super) async fn handle_pending_thread_resume_request(
sandbox,
active_permission_profile,
reasoning_effort,
initial_turns_page,
};
outgoing.send_response(request_id, response).await;
// Match cold resume: metadata-only resume should attach the listener without
@@ -2241,8 +2241,6 @@ impl ThreadRequestProcessor {
sort_direction,
items_view,
} = params;
let items_view = items_view.unwrap_or(TurnItemsView::Summary);
let thread_uuid = ThreadId::from_string(&thread_id)
.map_err(|err| invalid_request(format!("invalid thread id: {err}")))?;
@@ -2270,60 +2268,20 @@ impl ThreadRequestProcessor {
} else {
None
};
let mut turns = reconstruct_thread_turns_for_turns_list(
build_thread_turns_page_response(
&items,
self.thread_watch_manager
.loaded_status_for_thread(&thread_uuid.to_string())
.await,
has_live_running_thread,
active_turn,
);
for turn in &mut turns {
match items_view {
TurnItemsView::NotLoaded => {
turn.items.clear();
turn.items_view = TurnItemsView::NotLoaded;
}
TurnItemsView::Summary => {
let first_user_message = turn
.items
.iter()
.find(|item| matches!(item, ThreadItem::UserMessage { .. }))
.cloned();
let final_agent_message = turn
.items
.iter()
.rev()
.find(|item| matches!(item, ThreadItem::AgentMessage { .. }))
.cloned();
turn.items = match (first_user_message, final_agent_message) {
(Some(user_message), Some(agent_message))
if user_message.id() != agent_message.id() =>
{
vec![user_message, agent_message]
}
(Some(user_message), _) => vec![user_message],
(None, Some(agent_message)) => vec![agent_message],
(None, None) => Vec::new(),
};
turn.items_view = TurnItemsView::Summary;
}
TurnItemsView::Full => {
turn.items_view = TurnItemsView::Full;
}
}
}
let page = paginate_thread_turns(
turns,
cursor.as_deref(),
limit,
sort_direction.unwrap_or(SortDirection::Desc),
)?;
Ok(ThreadTurnsListResponse {
data: page.turns,
next_cursor: page.next_cursor,
backwards_cursor: page.backwards_cursor,
})
ThreadTurnsPageOptions {
cursor: cursor.as_deref(),
limit,
sort_direction: sort_direction.unwrap_or(SortDirection::Desc),
items_view: items_view.unwrap_or(TurnItemsView::Summary),
},
)
}
async fn load_thread_turns_list_history(
@@ -2531,6 +2489,7 @@ impl ThreadRequestProcessor {
developer_instructions,
personality,
exclude_turns,
initial_turns_page,
persist_extended_history: _persist_extended_history,
} = params;
let include_turns = !exclude_turns;
@@ -2685,8 +2644,28 @@ impl ThreadRequestProcessor {
config_snapshot.active_permission_profile,
);
let token_usage_thread = include_turns.then(|| thread.clone());
let mut initial_turns_page = if let Some(params) = initial_turns_page.as_ref() {
match build_thread_resume_initial_turns_page(
&response_history.get_rollout_items(),
thread.status.clone(),
/*has_live_running_thread*/ false,
/*active_turn*/ None,
params,
) {
Ok(page) => Some(page),
Err(error) => {
self.outgoing.send_error(request_id, error).await;
return Ok(());
}
}
} else {
None
};
if redact_resume_payloads {
redact_thread_resume_payloads(&mut thread);
redact_thread_resume_payloads(&mut thread.turns);
if let Some(initial_turns_page) = initial_turns_page.as_mut() {
redact_thread_resume_payloads(&mut initial_turns_page.data);
}
}
let response = ThreadResumeResponse {
@@ -2702,6 +2681,7 @@ impl ThreadRequestProcessor {
sandbox,
active_permission_profile,
reasoning_effort: session_configured.reasoning_effort,
initial_turns_page,
};
let connection_id = request_id.connection_id;
@@ -2926,6 +2906,7 @@ impl ThreadRequestProcessor {
emit_thread_goal_update,
thread_goal_state_db,
include_turns: !params.exclude_turns,
initial_turns_page: params.initial_turns_page.clone(),
redact_resume_payloads,
}),
);
@@ -3706,6 +3687,95 @@ fn parse_thread_turns_cursor(cursor: &str) -> Result<ThreadTurnsCursor, JSONRPCE
serde_json::from_str(cursor).map_err(|_| invalid_request(format!("invalid cursor: {cursor}")))
}
struct ThreadTurnsPageOptions<'a> {
cursor: Option<&'a str>,
limit: Option<u32>,
sort_direction: SortDirection,
items_view: TurnItemsView,
}
fn build_thread_turns_page_response(
items: &[RolloutItem],
loaded_status: ThreadStatus,
has_live_running_thread: bool,
active_turn: Option<Turn>,
options: ThreadTurnsPageOptions<'_>,
) -> Result<ThreadTurnsListResponse, JSONRPCErrorError> {
let mut turns = reconstruct_thread_turns_for_turns_list(
items,
loaded_status,
has_live_running_thread,
active_turn,
);
apply_thread_turns_items_view(&mut turns, options.items_view);
let page = paginate_thread_turns(turns, options.cursor, options.limit, options.sort_direction)?;
Ok(ThreadTurnsListResponse {
data: page.turns,
next_cursor: page.next_cursor,
backwards_cursor: page.backwards_cursor,
})
}
pub(super) fn build_thread_resume_initial_turns_page(
items: &[RolloutItem],
loaded_status: ThreadStatus,
has_live_running_thread: bool,
active_turn: Option<Turn>,
params: &ThreadResumeInitialTurnsPageParams,
) -> Result<codex_app_server_protocol::TurnsPage, JSONRPCErrorError> {
build_thread_turns_page_response(
items,
loaded_status,
has_live_running_thread,
active_turn,
ThreadTurnsPageOptions {
cursor: None,
limit: params.limit,
sort_direction: params.sort_direction.unwrap_or(SortDirection::Desc),
items_view: params.items_view.unwrap_or(TurnItemsView::Summary),
},
)
.map(Into::into)
}
fn apply_thread_turns_items_view(turns: &mut [Turn], items_view: TurnItemsView) {
for turn in turns {
match items_view {
TurnItemsView::NotLoaded => {
turn.items.clear();
turn.items_view = TurnItemsView::NotLoaded;
}
TurnItemsView::Summary => {
let first_user_message = turn
.items
.iter()
.find(|item| matches!(item, ThreadItem::UserMessage { .. }))
.cloned();
let final_agent_message = turn
.items
.iter()
.rev()
.find(|item| matches!(item, ThreadItem::AgentMessage { .. }))
.cloned();
turn.items = match (first_user_message, final_agent_message) {
(Some(user_message), Some(agent_message))
if user_message.id() != agent_message.id() =>
{
vec![user_message, agent_message]
}
(Some(user_message), _) => vec![user_message],
(None, Some(agent_message)) => vec![agent_message],
(None, None) => Vec::new(),
};
turn.items_view = TurnItemsView::Summary;
}
TurnItemsView::Full => {
turn.items_view = TurnItemsView::Full;
}
}
}
}
fn reconstruct_thread_turns_for_turns_list(
items: &[RolloutItem],
loaded_status: ThreadStatus,
@@ -213,6 +213,7 @@ mod thread_processor_behavior_tests {
fn thread_turns_list_merges_in_progress_active_turn_before_agent_status_running() {
let persisted_items = vec![RolloutItem::EventMsg(EventMsg::UserMessage(
codex_protocol::protocol::UserMessageEvent {
client_id: None,
message: "persisted".to_string(),
images: None,
local_images: Vec::new(),
@@ -224,6 +225,7 @@ mod thread_processor_behavior_tests {
id: "live-turn".to_string(),
items: vec![ThreadItem::UserMessage {
id: "live-user-message".to_string(),
client_id: None,
content: vec![V2UserInput::Text {
text: "live".to_string(),
text_elements: Vec::new(),
@@ -652,6 +654,7 @@ mod thread_processor_behavior_tests {
developer_instructions: None,
personality: None,
exclude_turns: false,
initial_turns_page: None,
persist_extended_history: false,
};
let config_snapshot = ThreadConfigSnapshot {
@@ -1,6 +1,6 @@
use codex_app_server_protocol::McpToolCallResult;
use codex_app_server_protocol::Thread;
use codex_app_server_protocol::ThreadItem;
use codex_app_server_protocol::Turn;
use serde_json::Value as JsonValue;
// Temporary bandaid for remote clients: thread/resume can include large MCP and
@@ -14,8 +14,8 @@ pub(super) fn should_redact_thread_resume_payloads(client_name: Option<&str>) ->
client_name.is_some_and(|client_name| CHATGPT_REMOTE_CLIENT_NAMES.contains(&client_name))
}
pub(super) fn redact_thread_resume_payloads(thread: &mut Thread) {
for turn in &mut thread.turns {
pub(super) fn redact_thread_resume_payloads(turns: &mut [Turn]) {
for turn in turns {
turn.items.retain_mut(|item| match item {
ThreadItem::McpToolCall {
arguments,
@@ -55,8 +55,8 @@ mod tests {
use codex_app_server_protocol::McpToolCallError;
use codex_app_server_protocol::McpToolCallStatus;
use codex_app_server_protocol::SessionSource;
use codex_app_server_protocol::Thread;
use codex_app_server_protocol::ThreadStatus;
use codex_app_server_protocol::Turn;
use codex_app_server_protocol::TurnItemsView;
use codex_app_server_protocol::TurnStatus;
use codex_utils_absolute_path::test_support::PathBufExt;
@@ -100,7 +100,7 @@ mod tests {
},
]);
redact_thread_resume_payloads(&mut thread);
redact_thread_resume_payloads(&mut thread.turns);
assert_eq!(thread.turns[0].items.len(), 2);
assert_eq!(
@@ -146,7 +146,7 @@ mod tests {
duration_ms: Some(8),
}]);
redact_thread_resume_payloads(&mut thread);
redact_thread_resume_payloads(&mut thread.turns);
assert_eq!(
thread.turns[0].items[0],
@@ -148,6 +148,7 @@ mod tests {
fn token_usage_history() -> Vec<RolloutItem> {
vec![
RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "first turn".to_string(),
images: None,
local_images: Vec::new(),
@@ -164,6 +165,7 @@ mod tests {
rate_limits: None,
})),
RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "second turn".to_string(),
images: None,
local_images: Vec::new(),
@@ -416,6 +416,7 @@ impl TurnRequestProcessor {
.into_iter()
.map(V2UserInput::into_core)
.collect();
let client_user_message_id = params.client_user_message_id;
let additional_context = map_additional_context(params.additional_context);
let turn_has_input = !mapped_items.is_empty();
let thread_settings = self
@@ -448,8 +449,12 @@ impl TurnRequestProcessor {
additional_context,
thread_settings,
};
let turn_id = self
.submit_core_op(&request_id, thread.as_ref(), turn_op)
let turn_id = thread
.submit_user_input_with_client_user_message_id(
turn_op,
self.request_trace_context(&request_id).await,
client_user_message_id,
)
.await
.map_err(|err| {
let error = internal_error(format!("failed to start turn: {err}"));
@@ -780,6 +785,7 @@ impl TurnRequestProcessor {
mapped_items,
additional_context,
Some(&params.expected_turn_id),
params.client_user_message_id,
params.responsesapi_client_metadata,
)
.await
@@ -989,6 +995,7 @@ impl TurnRequestProcessor {
} else {
vec![ThreadItem::UserMessage {
id: turn_id.clone(),
client_id: None,
content: vec![V2UserInput::Text {
text: display_text.to_string(),
// Review prompt display text is synthesized; no UI element ranges to preserve.
@@ -76,10 +76,8 @@ impl WindowsSandboxRequestProcessor {
Ok(config) => {
let setup_request = WindowsSandboxSetupRequest {
mode,
policy: config
.permissions
.legacy_sandbox_policy(config.cwd.as_path()),
policy_cwd: config.cwd.to_path_buf(),
permission_profile: config.permissions.effective_permission_profile(),
permission_profile_cwd: config.cwd.to_path_buf(),
command_cwd,
env_map: std::env::vars().collect(),
codex_home: config.codex_home.to_path_buf(),
+2
View File
@@ -35,6 +35,8 @@ pub(crate) struct PendingThreadResumeRequest {
pub(crate) emit_thread_goal_update: bool,
pub(crate) thread_goal_state_db: Option<StateDbHandle>,
pub(crate) include_turns: bool,
pub(crate) initial_turns_page:
Option<codex_app_server_protocol::ThreadResumeInitialTurnsPageParams>,
pub(crate) redact_resume_payloads: bool,
}
@@ -506,6 +506,7 @@ async fn external_auth_refreshes_on_unauthorized() -> Result<()> {
let turn_req = mcp
.send_turn_start_request(codex_app_server_protocol::TurnStartParams {
thread_id: thread.thread.id,
client_user_message_id: None,
input: vec![codex_app_server_protocol::UserInput::Text {
text: "Hello".to_string(),
text_elements: Vec::new(),
@@ -612,6 +613,7 @@ async fn external_auth_refresh_error_fails_turn() -> Result<()> {
let turn_req = mcp
.send_turn_start_request(codex_app_server_protocol::TurnStartParams {
thread_id: thread.thread.id.clone(),
client_user_message_id: None,
input: vec![codex_app_server_protocol::UserInput::Text {
text: "Hello".to_string(),
text_elements: Vec::new(),
@@ -734,6 +736,7 @@ async fn external_auth_refresh_mismatched_workspace_fails_turn() -> Result<()> {
let turn_req = mcp
.send_turn_start_request(codex_app_server_protocol::TurnStartParams {
thread_id: thread.thread.id.clone(),
client_user_message_id: None,
input: vec![codex_app_server_protocol::UserInput::Text {
text: "Hello".to_string(),
text_elements: Vec::new(),
@@ -849,6 +852,7 @@ async fn external_auth_refresh_invalid_access_token_fails_turn() -> Result<()> {
let turn_req = mcp
.send_turn_start_request(codex_app_server_protocol::TurnStartParams {
thread_id: thread.thread.id.clone(),
client_user_message_id: None,
input: vec![codex_app_server_protocol::UserInput::Text {
text: "Hello".to_string(),
text_elements: Vec::new(),
@@ -168,11 +168,13 @@ pub(crate) fn thread_initialized_event(payload: &Value) -> Result<&Value> {
pub(crate) fn assert_basic_thread_initialized_event(
event: &Value,
thread_id: &str,
session_id: &str,
expected_model: &str,
initialization_mode: &str,
expected_thread_source: &str,
) {
assert_eq!(event["event_params"]["thread_id"], thread_id);
assert_eq!(event["event_params"]["session_id"], session_id);
assert_eq!(
event["event_params"]["app_server_client"]["product_client_id"],
DEFAULT_CLIENT_NAME
+68 -4
View File
@@ -316,6 +316,73 @@ connectors = false
Ok(())
}
#[tokio::test]
async fn list_apps_keeps_apps_with_app_only_tools_accessible() -> Result<()> {
let connectors = vec![AppInfo {
id: "beta".to_string(),
name: "Beta".to_string(),
description: Some("Beta connector".to_string()),
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
branding: None,
app_metadata: None,
labels: None,
install_url: None,
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
}];
let mut app_only_tool = connector_tool("beta", "Beta App")?;
app_only_tool
.meta
.as_mut()
.expect("connector tool should include metadata")
.0
.insert("ui".to_string(), json!({ "visibility": ["app"] }));
let tools = vec![app_only_tool];
let (server_url, server_handle) =
start_apps_server_with_delays(connectors, tools, Duration::ZERO, Duration::ZERO).await?;
let codex_home = TempDir::new()?;
write_connectors_config(codex_home.path(), &server_url)?;
write_chatgpt_auth(
codex_home.path(),
ChatGptAuthFixture::new("chatgpt-token")
.account_id("account-123")
.chatgpt_user_id("user-app-only")
.chatgpt_account_id("account-123"),
AuthCredentialsStoreMode::File,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_apps_list_request(AppsListParams {
limit: None,
cursor: None,
thread_id: None,
force_refetch: true,
})
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let AppsListResponse { data, next_cursor } = to_response(response)?;
assert_eq!(data.len(), 1);
assert_eq!(data[0].id, "beta");
assert!(data[0].is_accessible);
assert!(next_cursor.is_none());
server_handle.abort();
let _ = server_handle.await;
Ok(())
}
#[tokio::test]
async fn list_apps_reports_is_enabled_from_config() -> Result<()> {
let connectors = vec![AppInfo {
@@ -1432,10 +1499,7 @@ impl AppsServerControl {
impl ServerHandler for AppListMcpServer {
fn get_info(&self) -> ServerInfo {
ServerInfo {
capabilities: ServerCapabilities::builder().enable_tools().build(),
..ServerInfo::default()
}
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
}
fn list_tools(
@@ -110,6 +110,7 @@ async fn attestation_generate_round_trip_adds_header_to_responses_websocket_hand
let turn_request_id = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id,
client_user_message_id: None,
input: vec![V2UserInput::Text {
text: "Hello".to_string(),
text_elements: Vec::new(),
@@ -73,6 +73,7 @@ async fn turn_start_forwards_client_metadata_to_responses_request_v2() -> Result
let turn_req = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id,
client_user_message_id: None,
input: vec![V2UserInput::Text {
text: "Hello".to_string(),
text_elements: Vec::new(),
@@ -105,6 +106,10 @@ async fn turn_start_forwards_client_metadata_to_responses_request_v2() -> Result
assert_eq!(metadata["thread_source"].as_str(), Some("client-supplied"));
assert_eq!(metadata["turn_id"].as_str(), Some(turn.id.as_str()));
assert!(metadata.get("session_id").is_some());
assert_eq!(
metadata["window_id"].as_str(),
request.header("x-codex-window-id").as_deref()
);
Ok(())
}
@@ -149,6 +154,7 @@ async fn turn_start_sends_fork_lineage_in_turn_metadata_for_thread_fork_v2() ->
let turn_req = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
client_user_message_id: None,
input: vec![V2UserInput::Text {
text: "Continue".to_string(),
text_elements: Vec::new(),
@@ -329,6 +335,7 @@ async fn turn_steer_updates_client_metadata_on_follow_up_responses_request_v2()
let turn_req = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
client_user_message_id: None,
input: vec![V2UserInput::Text {
text: "Run sleep".to_string(),
text_elements: Vec::new(),
@@ -359,6 +366,7 @@ async fn turn_steer_updates_client_metadata_on_follow_up_responses_request_v2()
let steer_req = mcp
.send_turn_steer_request(TurnSteerParams {
thread_id: thread.id.clone(),
client_user_message_id: None,
input: vec![V2UserInput::Text {
text: "Focus on the failure".to_string(),
text_elements: Vec::new(),
@@ -454,6 +462,7 @@ async fn turn_start_forwards_client_metadata_to_responses_websocket_request_body
let turn_req = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id,
client_user_message_id: None,
input: vec![V2UserInput::Text {
text: "Hello".to_string(),
text_elements: Vec::new(),
@@ -497,6 +506,10 @@ async fn turn_start_forwards_client_metadata_to_responses_websocket_request_body
assert_eq!(metadata["origin"].as_str(), Some("gaas"));
assert_eq!(metadata["turn_id"].as_str(), Some(turn.id.as_str()));
assert!(metadata.get("session_id").is_some());
assert_eq!(
metadata["window_id"].as_str(),
request["client_metadata"]["x-codex-window-id"].as_str()
);
websocket_server.shutdown().await;
Ok(())
@@ -190,6 +190,58 @@ async fn auto_compaction_remote_emits_started_and_completed_items() -> Result<()
let response_requests = responses_log.requests();
assert_eq!(response_requests.len(), 3);
let turn_metadata = response_requests
.iter()
.map(|request| {
request
.header("x-codex-turn-metadata")
.as_deref()
.map(parse_json_header)
.unwrap_or_else(|| panic!("turn request should include turn metadata"))
})
.collect::<Vec<_>>();
for (request, metadata) in response_requests.iter().zip(&turn_metadata) {
assert_eq!(metadata["request_kind"].as_str(), Some("turn"));
assert!(
metadata["turn_id"]
.as_str()
.is_some_and(|turn_id| !turn_id.is_empty()),
"turn request should carry a non-empty turn id"
);
assert_eq!(
metadata["window_id"].as_str(),
request.header("x-codex-window-id").as_deref()
);
assert!(metadata.get("compaction").is_none());
}
let compact_metadata = compact_requests[0]
.header("x-codex-turn-metadata")
.as_deref()
.map(parse_json_header)
.unwrap_or_else(|| panic!("compact request should include turn metadata"));
assert_eq!(
compact_metadata["request_kind"].as_str(),
Some("compaction")
);
assert_eq!(
compact_metadata["compaction"],
serde_json::json!({
"trigger": "auto",
"reason": "context_limit",
"implementation": "responses_compact",
"phase": "pre_turn",
"strategy": "memento",
})
);
assert_eq!(
compact_metadata["turn_id"], turn_metadata[2]["turn_id"],
"pre-turn compaction should carry the current turn id"
);
assert_eq!(
compact_metadata["window_id"].as_str(),
compact_requests[0].header("x-codex-window-id").as_deref()
);
Ok(())
}
@@ -342,6 +394,7 @@ async fn send_turn_and_wait(mcp: &mut McpProcess, thread_id: &str, text: &str) -
let turn_id = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread_id.to_string(),
client_user_message_id: None,
input: vec![V2UserInput::Text {
text: text.to_string(),
text_elements: Vec::new(),
@@ -407,3 +460,7 @@ async fn wait_for_context_compaction_completed(
}
}
}
fn parse_json_header(value: &str) -> serde_json::Value {
serde_json::from_str(value).unwrap_or_else(|err| panic!("turn metadata should be json: {err}"))
}
@@ -758,12 +758,15 @@ pub(super) async fn read_response_and_notification_for_method(
JSONRPCMessage::Response(candidate) if candidate.id == target_id => {
response = Some(candidate);
}
JSONRPCMessage::Notification(candidate)
if candidate.method == method && notification.is_some() =>
{
bail!(
"received duplicate notification for method `{method}` before completing paired read"
);
}
JSONRPCMessage::Notification(candidate) if candidate.method == method => {
if notification.replace(candidate).is_some() {
bail!(
"received duplicate notification for method `{method}` before completing paired read"
);
}
notification = Some(candidate);
}
_ => {}
}
@@ -226,6 +226,7 @@ async fn send_turn_start_request(stream: &mut WsClient, id: i64, thread_id: &str
id,
Some(serde_json::to_value(TurnStartParams {
thread_id: thread_id.to_string(),
client_user_message_id: None,
input: vec![V2UserInput::Text {
text: "Hello".to_string(),
text_elements: Vec::new(),
@@ -89,6 +89,7 @@ async fn thread_start_injects_dynamic_tools_into_model_requests() -> Result<()>
let turn_req = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
client_user_message_id: None,
input: vec![V2UserInput::Text {
text: "Hello".to_string(),
text_elements: Vec::new(),
@@ -168,6 +169,7 @@ async fn thread_start_keeps_hidden_dynamic_tools_out_of_model_requests() -> Resu
let turn_req = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id,
client_user_message_id: None,
input: vec![V2UserInput::Text {
text: "Hello".to_string(),
text_elements: Vec::new(),
@@ -347,6 +349,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
let turn_req = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread_id.clone(),
client_user_message_id: None,
input: vec![V2UserInput::Text {
text: "Run the tool".to_string(),
text_elements: Vec::new(),
@@ -521,6 +524,7 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<(
let turn_req = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread_id.clone(),
client_user_message_id: None,
input: vec![V2UserInput::Text {
text: "Run the tool".to_string(),
text_elements: Vec::new(),
@@ -340,6 +340,7 @@ async fn experimental_feature_enablement_set_only_updates_named_features() -> Re
BTreeMap::from([
("memories".to_string(), true),
("plugins".to_string(), true),
("remote_plugin".to_string(), true),
("tool_suggest".to_string(), true),
("tool_call_mcp_elicitation".to_string(), false),
]),
@@ -352,6 +353,7 @@ async fn experimental_feature_enablement_set_only_updates_named_features() -> Re
enablement: BTreeMap::from([
("memories".to_string(), true),
("plugins".to_string(), true),
("remote_plugin".to_string(), true),
("tool_suggest".to_string(), true),
("tool_call_mcp_elicitation".to_string(), false),
]),
@@ -381,6 +383,13 @@ async fn experimental_feature_enablement_set_only_updates_named_features() -> Re
.and_then(|features| features.get("plugins")),
Some(&json!(true))
);
assert_eq!(
config
.additional
.get("features")
.and_then(|features| features.get("remote_plugin")),
Some(&json!(true))
);
assert_eq!(
config
.additional
@@ -474,7 +483,7 @@ async fn experimental_feature_enablement_set_rejects_non_allowlisted_feature() -
);
assert!(
error.message.contains(
"apps, memories, mentions_v2, plugins, remote_control, tool_suggest, tool_call_mcp_elicitation"
"apps, memories, mentions_v2, plugins, remote_control, remote_plugin, tool_suggest, tool_call_mcp_elicitation"
),
"{}",
error.message
@@ -391,6 +391,7 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
let request_id = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
client_user_message_id: None,
input: vec![UserInput::Text {
text: "follow up".to_string(),
text_elements: Vec::new(),
@@ -964,6 +965,7 @@ async fn external_agent_config_import_compacts_huge_session_before_first_follow_
let request_id = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
client_user_message_id: None,
input: vec![UserInput::Text {
text: "follow up".to_string(),
text_elements: Vec::new(),
@@ -662,6 +662,7 @@ command = "python3 {hook_script_path}"
let first_turn_id = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
client_user_message_id: None,
input: vec![V2UserInput::Text {
text: "first turn".to_string(),
text_elements: Vec::new(),
@@ -723,6 +724,7 @@ command = "python3 {hook_script_path}"
let second_turn_id = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
client_user_message_id: None,
input: vec![V2UserInput::Text {
text: "second turn".to_string(),
text_elements: Vec::new(),
@@ -792,6 +794,7 @@ command = "python3 {hook_script_path}"
let third_turn_id = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id,
client_user_message_id: None,
input: vec![V2UserInput::Text {
text: "third turn".to_string(),
text_elements: Vec::new(),
@@ -932,6 +935,7 @@ command = "python3 {hook_script_path}"
let first_turn_id = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
client_user_message_id: None,
input: vec![V2UserInput::Text {
text: "first turn".to_string(),
text_elements: Vec::new(),
@@ -983,6 +987,7 @@ command = "python3 {hook_script_path}"
let second_turn_id = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id,
client_user_message_id: None,
input: vec![V2UserInput::Text {
text: "second turn".to_string(),
text_elements: Vec::new(),

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