mirror of
https://github.com/openai/codex.git
synced 2026-09-05 15:18:41 +00:00
## Description This PR adds a new `historyMode = "legacy" | "paginated"` to `Thread`. This will be stored in `SessionMeta` in the JSONL rollout file and as a new column in the SQLite thread_metadata table, and exposed on `thread/start` and on the `Thread` object in app-server. ## What changed - Added canonical `ThreadHistoryMode` with `legacy` and `paginated`, defaulting old and new SessionMeta to `legacy`. - Carried `history_mode` through core session config, ThreadStore stored metadata, local/in-memory stores, rollout metadata extraction, and the existing SQLite `threads` table. - Added experimental `historyMode` to app-server v2 `Thread` and `thread/start`. - Made paginated stored threads metadata-discoverable but unsupported for legacy full-history reads, `load_history`, live resume, and create paths. - Regenerated app-server schema fixtures and added protocol/state/thread-store/app-server coverage for persistence and fail-closed behavior. ## Compatibility floor Because users may be running various versions of Codex binaries on the same machine (TUI, Codex App, etc.), we will need to establish a compatibility floor for upcoming paginated threads, which will change how thread storage reads and writes work. The overall plan here: ``` Release N: - Add historyMode to SessionMeta / Thread / SQLite metadata. - Teach binaries to understand paginated threads. - If a binary sees `historyMode="paginated"` but does not support the paginated contract, it refuses to resume/mutate the thread. - Default remains `"legacy"`. Release N+1: - First-party clients start opting into paginated threads where appropriate. - Internal dogfood / staged rollout. - Measure old-client usage and paginated-thread unsupported errors. Release N+2: - Only after Release N+ is overwhelmingly deployed, make paginated the default. - Accept that a small tail of N-1-or-older binaries may not understand paginated threads. ``` The important behavior change is fail-closed handling for a binary that encounters a persisted `paginated` thread before it knows how to fully support paginated history. In app-server, if a thread is `paginated`, we will: - allow metadata-only discovery paths like `thread/list` and `thread/read(includeTurns=false)`, so clients can still see the thread and inspect its `historyMode` - reject legacy full-history/live-thread paths like `thread/read(includeTurns=true)` and `thread/resume` with an unsupported JSON-RPC error - avoid silently treating an unknown or future `historyMode` as `legacy` Under the hood, the ThreadStore layer also rejects legacy operations that would need to load or replay the full thread history for a paginated thread. That gives us the behavior we want for Release N: future paginated threads are visible, but this binary fails closed instead of trying to operate on them as if they were legacy threads.
693 lines
23 KiB
Rust
693 lines
23 KiB
Rust
use crate::agent::AgentStatus;
|
|
use crate::config::ConstraintResult;
|
|
use crate::session::Codex;
|
|
use crate::session::SessionSettingsUpdate;
|
|
use crate::session::SteerInputError;
|
|
use codex_features::Feature;
|
|
use codex_otel::SessionTelemetry;
|
|
use codex_protocol::ThreadId;
|
|
use codex_protocol::config_types::ApprovalsReviewer;
|
|
use codex_protocol::config_types::CollaborationMode;
|
|
use codex_protocol::config_types::Personality;
|
|
use codex_protocol::config_types::ReasoningSummary;
|
|
use codex_protocol::config_types::WindowsSandboxLevel;
|
|
use codex_protocol::error::CodexErr;
|
|
use codex_protocol::error::Result as CodexResult;
|
|
use codex_protocol::mcp::CallToolResult;
|
|
use codex_protocol::models::ActivePermissionProfile;
|
|
use codex_protocol::models::ContentItem;
|
|
use codex_protocol::models::PermissionProfile;
|
|
use codex_protocol::models::ResponseItem;
|
|
use codex_protocol::openai_models::ReasoningEffort;
|
|
use codex_protocol::protocol::AdditionalContextEntry;
|
|
use codex_protocol::protocol::AskForApproval;
|
|
use codex_protocol::protocol::Event;
|
|
use codex_protocol::protocol::MultiAgentVersion;
|
|
use codex_protocol::protocol::Op;
|
|
use codex_protocol::protocol::RolloutItem;
|
|
use codex_protocol::protocol::SandboxPolicy;
|
|
use codex_protocol::protocol::SessionConfiguredEvent;
|
|
use codex_protocol::protocol::SessionSource;
|
|
use codex_protocol::protocol::Submission;
|
|
use codex_protocol::protocol::ThreadHistoryMode;
|
|
use codex_protocol::protocol::ThreadMemoryMode;
|
|
use codex_protocol::protocol::ThreadSource;
|
|
use codex_protocol::protocol::TokenUsageInfo;
|
|
use codex_protocol::protocol::TurnEnvironmentSelection;
|
|
use codex_protocol::protocol::TurnEnvironmentSelections;
|
|
use codex_protocol::protocol::W3cTraceContext;
|
|
use codex_protocol::user_input::UserInput;
|
|
use codex_thread_store::StoredThread;
|
|
use codex_thread_store::StoredThreadHistory;
|
|
use codex_thread_store::ThreadMetadataPatch;
|
|
use codex_thread_store::ThreadStoreError;
|
|
use codex_thread_store::ThreadStoreResult;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use codex_utils_path_uri::LegacyAppPathString;
|
|
use codex_utils_path_uri::PathUri;
|
|
use rmcp::model::ReadResourceRequestParams;
|
|
use std::collections::BTreeMap;
|
|
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use tokio::sync::Mutex;
|
|
use tokio::sync::watch;
|
|
|
|
use codex_rollout::state_db::StateDbHandle;
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct ThreadConfigSnapshot {
|
|
pub model: String,
|
|
pub model_provider_id: String,
|
|
pub service_tier: Option<String>,
|
|
pub approval_policy: AskForApproval,
|
|
pub approvals_reviewer: ApprovalsReviewer,
|
|
pub permission_profile: PermissionProfile,
|
|
pub active_permission_profile: Option<ActivePermissionProfile>,
|
|
pub environments: TurnEnvironmentSelections,
|
|
pub workspace_roots: Vec<AbsolutePathBuf>,
|
|
pub profile_workspace_roots: Vec<AbsolutePathBuf>,
|
|
pub ephemeral: bool,
|
|
pub reasoning_effort: Option<ReasoningEffort>,
|
|
pub reasoning_summary: Option<ReasoningSummary>,
|
|
pub personality: Option<Personality>,
|
|
pub collaboration_mode: CollaborationMode,
|
|
pub session_source: SessionSource,
|
|
pub history_mode: ThreadHistoryMode,
|
|
pub forked_from_thread_id: Option<ThreadId>,
|
|
pub parent_thread_id: Option<ThreadId>,
|
|
pub thread_source: Option<ThreadSource>,
|
|
pub originator: String,
|
|
}
|
|
|
|
/// Explains why `CodexThread::try_start_turn_if_idle` rejected an automatic
|
|
/// idle turn.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum TryStartTurnIfIdleRejectionReason {
|
|
/// User/client-triggered mailbox work is already queued and must take
|
|
/// priority over extension-initiated idle work.
|
|
PendingTriggerTurn,
|
|
/// The thread is in Plan mode, where automatic idle work must not start a
|
|
/// new model turn.
|
|
PlanMode,
|
|
/// Another turn or task is active, or the idle reservation was lost before
|
|
/// the automatic turn could start.
|
|
Busy,
|
|
}
|
|
|
|
/// Rejection returned when an extension asks to start automatic idle work but
|
|
/// the thread is not eligible to run it.
|
|
#[derive(Debug)]
|
|
pub struct TryStartTurnIfIdleError {
|
|
reason: TryStartTurnIfIdleRejectionReason,
|
|
input: Vec<ResponseItem>,
|
|
}
|
|
|
|
impl TryStartTurnIfIdleError {
|
|
pub(crate) fn new(reason: TryStartTurnIfIdleRejectionReason, input: Vec<ResponseItem>) -> Self {
|
|
Self { reason, input }
|
|
}
|
|
|
|
/// Returns the stable reason the automatic idle turn was rejected.
|
|
pub fn reason(&self) -> TryStartTurnIfIdleRejectionReason {
|
|
self.reason
|
|
}
|
|
|
|
/// Consumes the rejection and returns the original model-visible input
|
|
/// unchanged, so callers can retry, drop, or log it explicitly.
|
|
pub fn into_input(self) -> Vec<ResponseItem> {
|
|
self.input
|
|
}
|
|
}
|
|
|
|
impl ThreadConfigSnapshot {
|
|
pub fn cwd(&self) -> &AbsolutePathBuf {
|
|
&self.environments.legacy_fallback_cwd
|
|
}
|
|
|
|
pub fn environment_selections(&self) -> &[TurnEnvironmentSelection] {
|
|
&self.environments.environments
|
|
}
|
|
|
|
pub fn sandbox_policy(&self) -> SandboxPolicy {
|
|
codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
|
|
&self.permission_profile,
|
|
self.cwd().as_path(),
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Thread settings overrides that app-server validates before starting a turn.
|
|
#[derive(Clone, Default)]
|
|
pub struct CodexThreadSettingsOverrides {
|
|
pub environments: Option<TurnEnvironmentSelections>,
|
|
pub workspace_roots: Option<Vec<AbsolutePathBuf>>,
|
|
pub profile_workspace_roots: Option<Vec<AbsolutePathBuf>>,
|
|
pub approval_policy: Option<AskForApproval>,
|
|
pub approvals_reviewer: Option<ApprovalsReviewer>,
|
|
pub sandbox_policy: Option<SandboxPolicy>,
|
|
pub permission_profile: Option<PermissionProfile>,
|
|
pub active_permission_profile: Option<ActivePermissionProfile>,
|
|
pub windows_sandbox_level: Option<WindowsSandboxLevel>,
|
|
pub model: Option<String>,
|
|
pub effort: Option<Option<ReasoningEffort>>,
|
|
pub summary: Option<ReasoningSummary>,
|
|
pub service_tier: Option<Option<String>>,
|
|
pub collaboration_mode: Option<CollaborationMode>,
|
|
pub personality: Option<Personality>,
|
|
}
|
|
|
|
pub struct CodexThread {
|
|
pub(crate) codex: Codex,
|
|
pub(crate) session_source: SessionSource,
|
|
session_configured: SessionConfiguredEvent,
|
|
rollout_path: Option<PathBuf>,
|
|
out_of_band_elicitation_count: Mutex<u64>,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq)]
|
|
pub struct BackgroundTerminalInfo {
|
|
pub item_id: String,
|
|
pub process_id: String,
|
|
pub command: String,
|
|
pub cwd: PathUri,
|
|
}
|
|
|
|
/// Conduit for the bidirectional stream of messages that compose a thread
|
|
/// (formerly called a conversation) in Codex.
|
|
impl CodexThread {
|
|
pub(crate) fn new(
|
|
codex: Codex,
|
|
session_configured: SessionConfiguredEvent,
|
|
rollout_path: Option<PathBuf>,
|
|
session_source: SessionSource,
|
|
) -> Self {
|
|
Self {
|
|
codex,
|
|
session_source,
|
|
session_configured,
|
|
rollout_path,
|
|
out_of_band_elicitation_count: Mutex::new(0),
|
|
}
|
|
}
|
|
|
|
pub async fn submit(&self, op: Op) -> CodexResult<String> {
|
|
self.codex.submit(op).await
|
|
}
|
|
|
|
/// Returns the session telemetry handle for thread-scoped production instrumentation.
|
|
pub fn session_telemetry(&self) -> SessionTelemetry {
|
|
self.codex.session.services.session_telemetry.clone()
|
|
}
|
|
|
|
pub async fn shutdown_and_wait(&self) -> CodexResult<()> {
|
|
self.codex.shutdown_and_wait().await
|
|
}
|
|
|
|
/// Wait until the underlying session loop has terminated.
|
|
pub async fn wait_until_terminated(&self) {
|
|
self.codex.session_loop_termination.clone().await;
|
|
}
|
|
|
|
pub(crate) async fn emit_thread_resume_lifecycle(&self) {
|
|
for contributor in self
|
|
.codex
|
|
.session
|
|
.services
|
|
.extensions
|
|
.thread_lifecycle_contributors()
|
|
{
|
|
contributor
|
|
.on_thread_resume(codex_extension_api::ThreadResumeInput {
|
|
session_store: &self.codex.session.services.session_extension_data,
|
|
thread_store: &self.codex.session.services.thread_extension_data,
|
|
})
|
|
.await;
|
|
}
|
|
}
|
|
|
|
pub async fn emit_thread_idle_lifecycle_if_idle(&self) {
|
|
self.codex
|
|
.session
|
|
.emit_thread_idle_lifecycle_if_idle()
|
|
.await;
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
pub async fn ensure_rollout_materialized(&self) {
|
|
self.codex.session.ensure_rollout_materialized().await;
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
pub async fn flush_rollout(&self) -> std::io::Result<()> {
|
|
self.codex.session.flush_rollout().await
|
|
}
|
|
|
|
pub async fn submit_with_trace(
|
|
&self,
|
|
op: Op,
|
|
trace: Option<W3cTraceContext>,
|
|
) -> CodexResult<String> {
|
|
self.codex.submit_with_trace(op, trace).await
|
|
}
|
|
|
|
pub async fn submit_user_input_with_client_user_message_id(
|
|
&self,
|
|
op: Op,
|
|
trace: Option<W3cTraceContext>,
|
|
client_user_message_id: Option<String>,
|
|
) -> CodexResult<String> {
|
|
self.codex
|
|
.session
|
|
.services
|
|
.agent_control
|
|
.ensure_execution_capacity_for_op(self.session_configured.thread_id, &op)
|
|
.await?;
|
|
self.codex
|
|
.submit_user_input_with_client_user_message_id(op, trace, client_user_message_id)
|
|
.await
|
|
}
|
|
|
|
/// Persist whether this thread is eligible for future memory generation.
|
|
pub async fn set_thread_memory_mode(&self, mode: ThreadMemoryMode) -> anyhow::Result<()> {
|
|
self.codex.set_thread_memory_mode(mode).await
|
|
}
|
|
|
|
pub async fn steer_input(
|
|
&self,
|
|
input: Vec<UserInput>,
|
|
additional_context: BTreeMap<String, AdditionalContextEntry>,
|
|
expected_turn_id: Option<&str>,
|
|
client_user_message_id: Option<String>,
|
|
responsesapi_client_metadata: Option<HashMap<String, String>>,
|
|
) -> Result<String, SteerInputError> {
|
|
self.codex
|
|
.steer_input(
|
|
input,
|
|
additional_context,
|
|
expected_turn_id,
|
|
client_user_message_id,
|
|
responsesapi_client_metadata,
|
|
)
|
|
.await
|
|
}
|
|
|
|
/// Injects model-visible items into the currently active turn.
|
|
///
|
|
/// This is the thread-level bridge to `Session::inject_if_running` for
|
|
/// callers that only hold a `CodexThread`.
|
|
/// It returns the unchanged items when this thread has no active turn.
|
|
pub async fn inject_if_running(
|
|
&self,
|
|
items: Vec<ResponseItem>,
|
|
) -> Result<(), Vec<ResponseItem>> {
|
|
self.codex.session.inject_if_running(items).await
|
|
}
|
|
|
|
/// Starts an automatic regular turn with model-visible items only when idle
|
|
/// work is allowed for this thread.
|
|
///
|
|
/// This is the required entry point for extensions that want to launch
|
|
/// model-visible work from `ThreadLifecycleContributor::on_thread_idle`.
|
|
/// The call succeeds only if no user/client-triggered turn is queued, no
|
|
/// task is currently active, and the thread is not in Plan mode. Active
|
|
/// Review tasks are rejected by the active-task check because Review turns
|
|
/// are not steerable.
|
|
///
|
|
/// On rejection, the returned error includes a stable reason and carries
|
|
/// the original `items` unchanged so the caller can decide whether to drop
|
|
/// them, retry later, or log why no automatic turn was started.
|
|
pub async fn try_start_turn_if_idle(
|
|
&self,
|
|
items: Vec<ResponseItem>,
|
|
) -> Result<(), TryStartTurnIfIdleError> {
|
|
self.codex.session.try_start_turn_if_idle(items).await
|
|
}
|
|
|
|
pub async fn set_app_server_client_info(
|
|
&self,
|
|
app_server_client_name: Option<String>,
|
|
app_server_client_version: Option<String>,
|
|
mcp_elicitations_auto_deny: bool,
|
|
) -> ConstraintResult<()> {
|
|
self.codex
|
|
.set_app_server_client_info(
|
|
app_server_client_name,
|
|
app_server_client_version,
|
|
mcp_elicitations_auto_deny,
|
|
)
|
|
.await
|
|
}
|
|
|
|
pub async fn set_openai_form_elicitation_support(&self, supported: bool) -> anyhow::Result<()> {
|
|
self.codex
|
|
.session
|
|
.set_openai_form_elicitation_support(supported)
|
|
.await
|
|
}
|
|
|
|
/// Preview persistent thread settings overrides without committing them.
|
|
pub async fn preview_thread_settings_overrides(
|
|
&self,
|
|
overrides: CodexThreadSettingsOverrides,
|
|
) -> ConstraintResult<ThreadConfigSnapshot> {
|
|
let updates = self.thread_settings_update(overrides).await;
|
|
self.codex.session.preview_settings(&updates).await
|
|
}
|
|
|
|
async fn thread_settings_update(
|
|
&self,
|
|
overrides: CodexThreadSettingsOverrides,
|
|
) -> SessionSettingsUpdate {
|
|
let CodexThreadSettingsOverrides {
|
|
environments,
|
|
workspace_roots,
|
|
profile_workspace_roots,
|
|
approval_policy,
|
|
approvals_reviewer,
|
|
sandbox_policy,
|
|
permission_profile,
|
|
active_permission_profile,
|
|
windows_sandbox_level,
|
|
model,
|
|
effort,
|
|
summary,
|
|
service_tier,
|
|
collaboration_mode,
|
|
personality,
|
|
} = overrides;
|
|
let collaboration_mode = if let Some(collaboration_mode) = collaboration_mode {
|
|
collaboration_mode
|
|
} else {
|
|
self.codex
|
|
.session
|
|
.collaboration_mode()
|
|
.await
|
|
.with_updates(model, effort, /*developer_instructions*/ None)
|
|
};
|
|
|
|
SessionSettingsUpdate {
|
|
environments,
|
|
workspace_roots,
|
|
profile_workspace_roots,
|
|
approval_policy,
|
|
approvals_reviewer,
|
|
sandbox_policy,
|
|
permission_profile,
|
|
active_permission_profile,
|
|
windows_sandbox_level,
|
|
collaboration_mode: Some(collaboration_mode),
|
|
reasoning_summary: summary,
|
|
service_tier,
|
|
personality,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Use sparingly: this is intended to be removed soon.
|
|
pub async fn submit_with_id(&self, sub: Submission) -> CodexResult<()> {
|
|
self.codex.submit_with_id(sub).await
|
|
}
|
|
|
|
pub async fn next_event(&self) -> CodexResult<Event> {
|
|
self.codex.next_event().await
|
|
}
|
|
|
|
pub async fn agent_status(&self) -> AgentStatus {
|
|
self.codex.agent_status().await
|
|
}
|
|
|
|
pub async fn list_background_terminals(&self) -> Vec<BackgroundTerminalInfo> {
|
|
self.codex.session.list_background_terminals().await
|
|
}
|
|
|
|
pub async fn terminate_background_terminal(&self, process_id: i32) -> bool {
|
|
self.codex
|
|
.session
|
|
.terminate_background_terminal(process_id)
|
|
.await
|
|
}
|
|
|
|
pub(crate) fn subscribe_status(&self) -> watch::Receiver<AgentStatus> {
|
|
self.codex.agent_status.clone()
|
|
}
|
|
|
|
/// Returns the complete token usage snapshot currently cached for this thread.
|
|
///
|
|
/// This accessor is intentionally narrower than direct session access: it lets
|
|
/// app-server lifecycle paths replay restored usage after resume or fork without
|
|
/// exposing broader session mutation authority. A caller that only reads
|
|
/// `total_token_usage` would drop last-turn usage and make the v2
|
|
/// `thread/tokenUsage/updated` payload incomplete.
|
|
pub async fn token_usage_info(&self) -> Option<TokenUsageInfo> {
|
|
self.codex.session.token_usage_info().await
|
|
}
|
|
|
|
/// Records a user-role session-prefix message without creating a new user turn boundary.
|
|
pub(crate) async fn inject_user_message_without_turn(&self, message: String) {
|
|
let item = ResponseItem::Message {
|
|
id: None,
|
|
role: "user".to_string(),
|
|
content: vec![ContentItem::InputText { text: message }],
|
|
phase: None,
|
|
internal_chat_message_metadata_passthrough: None,
|
|
};
|
|
self.codex
|
|
.session
|
|
.inject_no_new_turn(vec![item], /*current_turn_context*/ None)
|
|
.await;
|
|
}
|
|
|
|
/// Record raw Responses API items without starting a new turn.
|
|
pub async fn inject_response_items(&self, items: Vec<ResponseItem>) -> CodexResult<()> {
|
|
if items.is_empty() {
|
|
return Err(CodexErr::InvalidRequest(
|
|
"items must not be empty".to_string(),
|
|
));
|
|
}
|
|
|
|
let turn_context = self.codex.session.new_default_turn().await;
|
|
if self.codex.session.reference_context_item().await.is_none() {
|
|
// This history-only API runs without run_turn, so it owns its initial step.
|
|
let step_context = self
|
|
.codex
|
|
.session
|
|
.capture_step_context(Arc::clone(&turn_context))
|
|
.await;
|
|
self.codex
|
|
.session
|
|
.record_context_updates_and_set_reference_context_item(step_context.as_ref())
|
|
.await;
|
|
}
|
|
self.codex
|
|
.session
|
|
.inject_no_new_turn(items, Some(turn_context.as_ref()))
|
|
.await;
|
|
self.codex.session.flush_rollout().await?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn rollout_path(&self) -> Option<PathBuf> {
|
|
self.rollout_path.clone()
|
|
}
|
|
|
|
pub fn session_configured(&self) -> SessionConfiguredEvent {
|
|
self.session_configured.clone()
|
|
}
|
|
|
|
pub(crate) fn is_running(&self) -> bool {
|
|
!self.codex.tx_sub.is_closed()
|
|
}
|
|
|
|
pub async fn guardian_trunk_rollout_path(&self) -> Option<PathBuf> {
|
|
self.codex
|
|
.session
|
|
.guardian_review_session
|
|
.trunk_rollout_path()
|
|
.await
|
|
}
|
|
|
|
pub async fn load_history(
|
|
&self,
|
|
include_archived: bool,
|
|
) -> ThreadStoreResult<StoredThreadHistory> {
|
|
let live_thread = self
|
|
.codex
|
|
.session
|
|
.live_thread_for_persistence("load history")
|
|
.map_err(|err| ThreadStoreError::Internal {
|
|
message: err.to_string(),
|
|
})?;
|
|
live_thread.load_history(include_archived).await
|
|
}
|
|
|
|
pub async fn read_thread(
|
|
&self,
|
|
include_archived: bool,
|
|
include_history: bool,
|
|
) -> ThreadStoreResult<StoredThread> {
|
|
let live_thread = self
|
|
.codex
|
|
.session
|
|
.live_thread_for_persistence("read thread")
|
|
.map_err(|err| ThreadStoreError::Internal {
|
|
message: err.to_string(),
|
|
})?;
|
|
live_thread
|
|
.read_thread(include_archived, include_history)
|
|
.await
|
|
}
|
|
|
|
pub async fn update_thread_metadata(
|
|
&self,
|
|
patch: ThreadMetadataPatch,
|
|
include_archived: bool,
|
|
) -> ThreadStoreResult<StoredThread> {
|
|
let live_thread = self
|
|
.codex
|
|
.session
|
|
.live_thread_for_persistence("update thread metadata")
|
|
.map_err(|err| ThreadStoreError::Internal {
|
|
message: err.to_string(),
|
|
})?;
|
|
live_thread.update_metadata(patch, include_archived).await
|
|
}
|
|
|
|
/// Appends rollout items through the live thread so derived metadata stays in sync.
|
|
pub async fn append_rollout_items(&self, items: &[RolloutItem]) -> ThreadStoreResult<()> {
|
|
let live_thread = self
|
|
.codex
|
|
.session
|
|
.live_thread_for_persistence("append rollout items")
|
|
.map_err(|err| ThreadStoreError::Internal {
|
|
message: err.to_string(),
|
|
})?;
|
|
live_thread.append_items(items).await
|
|
}
|
|
|
|
pub fn state_db(&self) -> Option<StateDbHandle> {
|
|
self.codex.state_db()
|
|
}
|
|
|
|
pub async fn config_snapshot(&self) -> ThreadConfigSnapshot {
|
|
self.codex.thread_config_snapshot().await
|
|
}
|
|
|
|
/// Returns the files that supplied the thread's loaded model instructions.
|
|
pub async fn instruction_sources(&self) -> Vec<PathUri> {
|
|
self.codex.instruction_sources().await
|
|
}
|
|
|
|
/// Returns loaded instruction sources rendered as legacy app-server path strings.
|
|
pub async fn legacy_instruction_sources(&self) -> Vec<LegacyAppPathString> {
|
|
self.instruction_sources()
|
|
.await
|
|
.into_iter()
|
|
.map(Into::into)
|
|
.collect()
|
|
}
|
|
|
|
pub async fn config(&self) -> Arc<crate::config::Config> {
|
|
self.codex.session.get_config().await
|
|
}
|
|
|
|
/// Resolves the MCP runtime configuration using this thread's extension data.
|
|
pub async fn runtime_mcp_config(&self, config: &crate::config::Config) -> codex_mcp::McpConfig {
|
|
self.codex.session.runtime_mcp_config(config).await
|
|
}
|
|
|
|
/// Returns the exact MCP config, environment bindings, and manager most recently published.
|
|
pub async fn current_mcp_runtime(&self) -> Arc<crate::session::McpRuntimeSnapshot> {
|
|
let turn_context = self.codex.session.new_default_turn().await;
|
|
self.codex
|
|
.session
|
|
.capture_step_context(turn_context)
|
|
.await
|
|
.mcp
|
|
.clone()
|
|
}
|
|
|
|
pub fn multi_agent_version(&self) -> Option<MultiAgentVersion> {
|
|
self.codex.session.multi_agent_version()
|
|
}
|
|
|
|
/// Refresh the thread's layer-backed user config state from a caller-supplied
|
|
/// config snapshot. Thread-scoped layers and session-static settings remain
|
|
/// unchanged.
|
|
pub async fn refresh_runtime_config(&self, next_config: crate::config::Config) {
|
|
self.codex.session.refresh_runtime_config(next_config).await;
|
|
}
|
|
|
|
pub async fn environment_selections(&self) -> Vec<TurnEnvironmentSelection> {
|
|
self.codex.thread_environment_selections().await
|
|
}
|
|
|
|
pub async fn read_mcp_resource(
|
|
&self,
|
|
server: &str,
|
|
uri: &str,
|
|
) -> anyhow::Result<serde_json::Value> {
|
|
let result = self
|
|
.current_mcp_runtime()
|
|
.await
|
|
.manager_arc()
|
|
.read_resource(server, ReadResourceRequestParams::new(uri))
|
|
.await?;
|
|
|
|
Ok(serde_json::to_value(result)?)
|
|
}
|
|
|
|
pub async fn call_mcp_tool(
|
|
&self,
|
|
server: &str,
|
|
tool: &str,
|
|
arguments: Option<serde_json::Value>,
|
|
meta: Option<serde_json::Value>,
|
|
) -> anyhow::Result<CallToolResult> {
|
|
self.current_mcp_runtime()
|
|
.await
|
|
.manager_arc()
|
|
.call_tool(server, tool, arguments, meta)
|
|
.await
|
|
}
|
|
|
|
pub fn enabled(&self, feature: Feature) -> bool {
|
|
self.codex.enabled(feature)
|
|
}
|
|
|
|
pub async fn increment_out_of_band_elicitation_count(&self) -> CodexResult<u64> {
|
|
let mut guard = self.out_of_band_elicitation_count.lock().await;
|
|
let was_zero = *guard == 0;
|
|
*guard = guard.checked_add(1).ok_or_else(|| {
|
|
CodexErr::Fatal("out-of-band elicitation count overflowed".to_string())
|
|
})?;
|
|
|
|
if was_zero {
|
|
self.codex
|
|
.session
|
|
.set_out_of_band_elicitation_pause_state(/*paused*/ true);
|
|
}
|
|
|
|
Ok(*guard)
|
|
}
|
|
|
|
pub async fn decrement_out_of_band_elicitation_count(&self) -> CodexResult<u64> {
|
|
let mut guard = self.out_of_band_elicitation_count.lock().await;
|
|
if *guard == 0 {
|
|
return Err(CodexErr::InvalidRequest(
|
|
"out-of-band elicitation count is already zero".to_string(),
|
|
));
|
|
}
|
|
|
|
*guard -= 1;
|
|
let now_zero = *guard == 0;
|
|
if now_zero {
|
|
self.codex
|
|
.session
|
|
.set_out_of_band_elicitation_pause_state(/*paused*/ false);
|
|
}
|
|
|
|
Ok(*guard)
|
|
}
|
|
}
|