Add request_user_input auto-resolution window contract (#27256)

## Why

`request_user_input` is moving beyond its original plan-mode-only
workflow, and future default/goal-mode usage needs a way for the model
to ask helpful but non-blocking questions without forcing the turn to
wait forever. This PR adds an explicit `autoResolutionMs` contract so a
later client/runtime change can auto-resolve unanswered prompts after a
bounded window while leaving truly blocking questions unchanged.

This is contract plumbing only; it does not implement the client-side
timer or auto-selection behavior, and the model-facing description
treats the field as reserved unless the current runtime explicitly
supports auto-resolution.

## What Changed

- Added optional `autoResolutionMs` to the model-facing
`request_user_input` args and core `RequestUserInputEvent`.
- Added model-facing schema text for `autoResolutionMs` while marking it
reserved for runtimes that explicitly support auto-resolution.
- Bounds `autoResolutionMs` to `60_000..=240_000` ms during argument
normalization by clamping out-of-range model-provided values.
- Propagated the field through app-server v2
`ToolRequestUserInputParams`, app-server request forwarding, generated
TypeScript, and JSON schema fixtures.
- Updated app-server, core, protocol, and TUI call sites/tests so
omitted values preserve existing `None`/`null` behavior and coverage
verifies a `Some(60_000)` round trip.

## Verification

- `just test -p codex-app-server-protocol`
- `just test -p codex-core request_user_input`
- `just test -p codex-app-server request_user_input_round_trip`
- `just test -p codex-tui request_user_input`
- `just test -p codex-protocol`
This commit is contained in:
Shijie Rao
2026-06-11 22:30:41 -07:00
committed by GitHub
parent 78bab04116
commit 216ce03031
25 changed files with 243 additions and 16 deletions
@@ -76,10 +76,18 @@ fn call_output_content_and_success(
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn request_user_input_round_trip_resolves_pending() -> anyhow::Result<()> {
request_user_input_round_trip_for_mode(ModeKind::Plan).await
request_user_input_round_trip_for_mode(ModeKind::Plan, /*auto_resolution_ms*/ None).await
}
async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn request_user_input_round_trip_emits_auto_resolution_ms() -> anyhow::Result<()> {
request_user_input_round_trip_for_mode(ModeKind::Plan, Some(60_000)).await
}
async fn request_user_input_round_trip_for_mode(
mode: ModeKind,
auto_resolution_ms: Option<u64>,
) -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
@@ -104,7 +112,7 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul
.await?;
let call_id = "user-input-call";
let request_args = json!({
let mut request_args = json!({
"questions": [{
"id": "confirm_path",
"header": "Confirm",
@@ -117,8 +125,14 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul
"description": "Stop and revisit the approach."
}]
}]
})
.to_string();
});
if let Some(auto_resolution_ms) = auto_resolution_ms {
let Some(request_args) = request_args.as_object_mut() else {
panic!("request_user_input args should be a JSON object");
};
request_args.insert("autoResolutionMs".to_string(), json!(auto_resolution_ms));
}
let request_args = request_args.to_string();
let first_response = sse(vec![
ev_response_created("resp-1"),
@@ -171,6 +185,7 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul
.await;
assert_eq!(request.call_id, call_id);
assert_eq!(request.questions.len(), 1);
assert_eq!(request.auto_resolution_ms, auto_resolution_ms);
assert_eq!(request.questions[0].is_other, true);
assert!(
timeout(Duration::from_millis(200), async {
@@ -445,7 +460,7 @@ async fn request_user_input_rejected_in_default_mode_by_default() -> anyhow::Res
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn request_user_input_round_trip_in_default_mode_with_feature() -> anyhow::Result<()> {
request_user_input_round_trip_for_mode(ModeKind::Default).await
request_user_input_round_trip_for_mode(ModeKind::Default, /*auto_resolution_ms*/ None).await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]