Add acknowledged user message submission to core (#36385)

## What changed

- Add `CodexThread::submit_user_input_and_wait_for_admission` and export the
  `UserMessageAdmission` result through `codex-core-api`.
- Resolve submissions only after they start a new turn or steer the active
  turn, returning the accepting turn ID in either case.
- Return errors for invalid operations, rejected thread settings, and session
  termination instead of leaving admission waiters unresolved.

## Testing

Add integration coverage for concurrent start-and-steer submissions, settings
rejection and recovery, non-user operations, and submission after shutdown.

GitOrigin-RevId: 21f1ede38d399baa9836d61a59b9f9af2dbf83b8
This commit is contained in:
keith thornhill
2026-07-31 20:17:25 +00:00
committed by copyberry
parent 332eac4b85
commit bf7804c254
10 changed files with 500 additions and 10 deletions
+39
View File
@@ -5,6 +5,7 @@ use crate::session::SessionIo;
use crate::session::SessionSettingsUpdate;
use crate::session::SteerInputError;
use crate::session::session::Session;
use crate::user_message_admission::UserMessageAdmission;
use codex_exec_server::SelectedCapabilityRootsStatus;
use codex_features::Feature;
use codex_otel::SessionTelemetry;
@@ -296,6 +297,44 @@ impl CodexThread {
.await
}
/// Waits until Core has actually started a turn or steered the active turn.
pub async fn submit_user_input_and_wait_for_admission(
&self,
op: Op,
trace: Option<W3cTraceContext>,
client_user_message_id: Option<String>,
) -> CodexResult<UserMessageAdmission> {
if !matches!(op, Op::UserInput { .. }) {
return Err(CodexErr::InvalidRequest(
"user message admission requires user input".to_string(),
));
}
self.session
.services
.agent_control
.ensure_execution_capacity_for_op(self.session.thread_id(), &op)
.await?;
let submission_id = crate::session::new_submission_id();
let (_pending_admission, admission) = self
.session
.pending_user_message_admissions
.register(submission_id.clone());
self.io
.submit_with_id(Submission {
id: submission_id.clone(),
op,
client_user_message_id,
trace,
parent_turn_id: None,
})
.await?;
tokio::select! {
biased;
result = admission => result.map_err(|_| CodexErr::InternalAgentDied)?,
() = self.io.session_loop_termination.clone() => Err(CodexErr::InternalAgentDied),
}
}
/// Persist whether this thread is eligible for future memory generation.
pub async fn set_thread_memory_mode(&self, mode: ThreadMemoryMode) -> anyhow::Result<()> {
self.session.set_thread_memory_mode(mode).await