Gate automatic idle turns in Plan mode (#26147)

## Why

Goal idle continuation is extension-triggered model-visible work, so it
should follow one core-owned rule for when automatic work may start. In
particular, it should not jump ahead of queued user/client work, start
while another task is active, or inject a continuation turn while the
thread is in Plan mode.

Keeping this policy in `try_start_turn_if_idle` avoids passing
`collaboration_mode` or review-specific state through
`ThreadLifecycleContributor::on_thread_idle`. Active `/review` is
covered by the same active-task gate because Review turns are not
steerable.

## What Changed

- Teach `Session::try_start_turn_if_idle` to reject automatic idle turns
in Plan mode, both before reserving an idle turn and after building the
turn context.
- Document `CodexThread::try_start_turn_if_idle` as the extension-facing
gate for automatic idle work, including Plan-mode and active Review-task
behavior.
- Add focused coverage for Plan-mode rejection and active Review-task
rejection without queuing synthetic input.

## Testing

- `just test -p codex-core try_start_turn_if_idle`
This commit is contained in:
jif
2026-06-04 14:44:45 +02:00
committed by GitHub
Unverified
parent 16d02ec77c
commit d297616d3e
5 changed files with 192 additions and 12 deletions
+6 -2
View File
@@ -307,8 +307,12 @@ impl GoalRuntimeHandle {
return Ok(());
};
if thread.try_start_turn_if_idle(vec![item]).await.is_err() {
tracing::debug!("skipping goal continuation because the thread is no longer idle");
if let Err(err) = thread.try_start_turn_if_idle(vec![item]).await {
let reason = err.reason();
tracing::debug!(
?reason,
"skipping goal continuation because automatic idle work was rejected"
);
}
let current_turn_is_goal_active = self