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:
efrazer-oai
2026-08-06 03:11:42 +00:00
committed by copyberry
parent 70b4653232
commit bc8b25ea02
42 changed files with 2114 additions and 140 deletions
+12 -1
View File
@@ -66,6 +66,7 @@ use codex_chatgpt::workspace_settings;
use codex_code_mode::CodeModeSessionProvider;
use codex_core::ThreadManager;
use codex_core::config::Config;
use codex_core::config::ThreadStoreConfig;
use codex_exec_server::EnvironmentManager;
use codex_feedback::CodexFeedback;
use codex_goal_extension::GoalService;
@@ -77,6 +78,8 @@ use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::W3cTraceContext;
use codex_rollout::StateDbHandle;
use codex_state::log_db::LogDbLayer;
use codex_thread_store::LocalQueueStore;
use codex_thread_store::QueueStore;
use tokio::sync::Mutex;
use tokio::sync::Semaphore;
use tokio::sync::broadcast;
@@ -247,6 +250,14 @@ impl MessageProcessor {
// affect per-thread behavior, but they must not move newly started,
// resumed, or forked threads to a different persistence backend/root.
let thread_store = codex_core::thread_store_from_config(config.as_ref(), state_db.clone());
// Queue persistence requires SQLite, so in-memory thread stores and
// app servers without a state database do not have a queue backend.
let queue_store: Option<Arc<dyn QueueStore>> = match &config.experimental_thread_store {
ThreadStoreConfig::Local => state_db.as_ref().map(|state_db| {
Arc::new(LocalQueueStore::new(Arc::clone(state_db))) as Arc<dyn QueueStore>
}),
ThreadStoreConfig::InMemory { .. } => None,
};
let environment_manager_for_requests = Arc::clone(&environment_manager);
let environment_manager_for_extensions = Arc::clone(&environment_manager);
let restriction_product = session_source.restriction_product();
@@ -281,7 +292,7 @@ impl MessageProcessor {
executor_skill_provider: Arc::clone(&executor_skill_provider),
git_attribution_base_url: config.chatgpt_base_url.clone(),
http_client_factory: config.http_client_factory(),
thread_store: Arc::clone(&thread_store),
queue_store,
},
),
Arc::new(CodexHomeUserInstructionsProvider::new(