mirror of
https://github.com/openai/codex.git
synced 2026-11-09 08:40:33 +00:00
Add durable user-message queue dispatch (#37204)
## What changed - Add a storage-neutral queue extension for listing, adding, editing, reordering, deleting, and explicitly starting queued user messages. - Dispatch queued messages in FIFO order when a thread becomes idle after a completed turn, while leaving them queued after interruptions or failures. - Wait for user input to be persisted before removing its queue item, and report hook rejection, task termination, and persistence failures distinctly. - Snapshot local image and audio attachments into validated data URLs before storing them, enforce input limits, and emit `ThreadQueueChanged` events after mutations. ## Testing - Cover queue mutations, automatic and explicit dispatch, interruption and failure behavior, hook rejection, invalid records, input limits, and local attachment snapshotting. GitOrigin-RevId: 14f39d7c8cbaccb767a6da84b82cced0edfcb4b9
This commit is contained in:
1 parent
70b4653232
commit
bc8b25ea02
42 files changed
+2114
-140
No files matched your search
@@ -8,6 +8,7 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use codex_extension_api::ThreadIdleCause;
|
||||
use futures::future::BoxFuture;
|
||||
use tokio::select;
|
||||
use tokio::sync::Notify;
|
||||
@@ -22,13 +23,12 @@ use tracing::trace_span;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::codex_thread::BackgroundTerminalInfo;
|
||||
use crate::codex_thread::TryStartTurnIfIdleRejectionReason;
|
||||
use crate::config::Config;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::hook_runtime::inspect_pending_input;
|
||||
use crate::hook_runtime::record_additional_contexts;
|
||||
use crate::hook_runtime::record_pending_input;
|
||||
use crate::session::TurnInput;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn::run_hooks_and_record_inputs;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::state::ActiveTurn;
|
||||
use crate::state::RunningTask;
|
||||
@@ -281,8 +281,14 @@ impl Session {
|
||||
) {
|
||||
self.abort_all_tasks(TurnAbortReason::Replaced).await;
|
||||
self.clear_connector_selection().await;
|
||||
self.start_task(turn_context, input, task, MailboxParentProvenance::Ignore)
|
||||
.await;
|
||||
self.start_task(
|
||||
turn_context,
|
||||
input,
|
||||
task,
|
||||
/*input_persisted*/ None,
|
||||
MailboxParentProvenance::Ignore,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) async fn start_task<T: SessionTask>(
|
||||
@@ -290,6 +296,9 @@ impl Session {
|
||||
turn_context: Arc<TurnContext>,
|
||||
input: Vec<TurnInput>,
|
||||
task: T,
|
||||
input_persisted: Option<
|
||||
tokio::sync::oneshot::Sender<Result<(), TryStartTurnIfIdleRejectionReason>>,
|
||||
>,
|
||||
mailbox_parent_provenance: MailboxParentProvenance,
|
||||
) {
|
||||
let task: Arc<dyn AnySessionTask> = Arc::new(task);
|
||||
@@ -408,6 +417,7 @@ impl Session {
|
||||
handle: AbortOnDropHandle::new(handle),
|
||||
kind: task_kind,
|
||||
task,
|
||||
input_persisted,
|
||||
cancellation_token,
|
||||
turn_context: Arc::clone(&turn_context),
|
||||
_agent_execution_guard: agent_execution_guard,
|
||||
@@ -471,6 +481,7 @@ impl Session {
|
||||
turn_context,
|
||||
Vec::new(),
|
||||
RegularTask::new(),
|
||||
/*input_persisted*/ None,
|
||||
MailboxParentProvenance::Attribute,
|
||||
)
|
||||
.await;
|
||||
@@ -580,12 +591,24 @@ impl Session {
|
||||
let turn_state = {
|
||||
let mut active = self.active_turn.lock().await;
|
||||
active.as_mut().and_then(|active_turn| {
|
||||
let task = active_turn.task.take()?;
|
||||
let mut task = active_turn.task.take()?;
|
||||
let task_ended_before_persistence =
|
||||
if let Some(sender) = task.input_persisted.take() {
|
||||
let _ = sender.send(Err(
|
||||
TryStartTurnIfIdleRejectionReason::TaskEndedBeforePersistence,
|
||||
));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
task.handle.detach();
|
||||
Some(Arc::clone(&active_turn.turn_state))
|
||||
Some((
|
||||
Arc::clone(&active_turn.turn_state),
|
||||
task_ended_before_persistence,
|
||||
))
|
||||
})
|
||||
};
|
||||
let Some(turn_state) = turn_state else {
|
||||
let Some((turn_state, mut task_ended_before_persistence)) = turn_state else {
|
||||
return;
|
||||
};
|
||||
let pending_input = self
|
||||
@@ -600,28 +623,10 @@ impl Session {
|
||||
ts.token_usage_at_turn_start.clone(),
|
||||
)
|
||||
};
|
||||
if !pending_input.is_empty() {
|
||||
for pending_input_item in pending_input {
|
||||
let hook_outcome =
|
||||
inspect_pending_input(self, &turn_context, &pending_input_item).await;
|
||||
if hook_outcome.should_stop {
|
||||
record_additional_contexts(
|
||||
self,
|
||||
&turn_context,
|
||||
hook_outcome.additional_contexts,
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
record_pending_input(
|
||||
self,
|
||||
&turn_context,
|
||||
pending_input_item,
|
||||
hook_outcome.additional_contexts,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
run_hooks_and_record_inputs(self, &turn_context, &pending_input).await;
|
||||
task_ended_before_persistence |= self
|
||||
.pending_user_message_admissions
|
||||
.complete_task_end(&turn_context.sub_id);
|
||||
// Emit token usage metrics.
|
||||
{
|
||||
// TODO(jif): drop this
|
||||
@@ -764,6 +769,15 @@ impl Session {
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
profile,
|
||||
});
|
||||
let idle_cause = if matches!(abort_reason.as_ref(), Some(TurnAbortReason::Interrupted)) {
|
||||
ThreadIdleCause::Interrupted
|
||||
} else if task_ended_before_persistence
|
||||
|| (abort_reason.is_none() && turn_context.terminal_error.lock().await.is_some())
|
||||
{
|
||||
ThreadIdleCause::Failed
|
||||
} else {
|
||||
ThreadIdleCause::Completed
|
||||
};
|
||||
let event = if let Some(reason) = abort_reason {
|
||||
self.emit_turn_abort_lifecycle(reason.clone(), turn_context.extension_data.as_ref())
|
||||
.await;
|
||||
@@ -812,7 +826,7 @@ impl Session {
|
||||
}
|
||||
};
|
||||
if cleared_active_turn {
|
||||
self.emit_thread_idle_lifecycle_if_idle().await;
|
||||
self.emit_thread_idle_lifecycle_if_idle(idle_cause).await;
|
||||
}
|
||||
// Regular items were flushed before this terminal event was appended; buffering
|
||||
// thread writers may not flush it without another explicit barrier.
|
||||
@@ -847,12 +861,19 @@ impl Session {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn handle_task_abort(self: &Arc<Self>, task: RunningTask, reason: TurnAbortReason) {
|
||||
async fn handle_task_abort(self: &Arc<Self>, mut task: RunningTask, reason: TurnAbortReason) {
|
||||
let sub_id = task.turn_context.sub_id.clone();
|
||||
if task.cancellation_token.is_cancelled() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(sender) = task.input_persisted.take() {
|
||||
let _ = sender.send(Err(
|
||||
TryStartTurnIfIdleRejectionReason::TaskEndedBeforePersistence,
|
||||
));
|
||||
}
|
||||
self.pending_user_message_admissions
|
||||
.complete_task_end(&sub_id);
|
||||
trace!(task_kind = ?task.kind, sub_id, "aborting running task");
|
||||
task.cancellation_token.cancel();
|
||||
task.turn_context
|
||||
|
||||
Reference in new issue
Block a user