Compare commits

...

1 Commits

Author SHA1 Message Date
Abhinav Vedmala
48659e216a Support clear SessionStart source 2026-04-07 19:54:55 -07:00
17 changed files with 167 additions and 22 deletions

View File

@@ -3186,10 +3186,27 @@
"type": "null"
}
]
},
"sessionStartSource": {
"anyOf": [
{
"$ref": "#/definitions/ThreadStartSource"
},
{
"type": "null"
}
]
}
},
"type": "object"
},
"ThreadStartSource": {
"enum": [
"startup",
"clear"
],
"type": "string"
},
"ThreadUnarchiveParams": {
"properties": {
"threadId": {

View File

@@ -14167,6 +14167,16 @@
"type": "null"
}
]
},
"sessionStartSource": {
"anyOf": [
{
"$ref": "#/definitions/v2/ThreadStartSource"
},
{
"type": "null"
}
]
}
},
"title": "ThreadStartParams",
@@ -14234,6 +14244,13 @@
"title": "ThreadStartResponse",
"type": "object"
},
"ThreadStartSource": {
"enum": [
"startup",
"clear"
],
"type": "string"
},
"ThreadStartedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {

View File

@@ -12022,6 +12022,16 @@
"type": "null"
}
]
},
"sessionStartSource": {
"anyOf": [
{
"$ref": "#/definitions/ThreadStartSource"
},
{
"type": "null"
}
]
}
},
"title": "ThreadStartParams",
@@ -12089,6 +12099,13 @@
"title": "ThreadStartResponse",
"type": "object"
},
"ThreadStartSource": {
"enum": [
"startup",
"clear"
],
"type": "string"
},
"ThreadStartedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {

View File

@@ -101,6 +101,13 @@
"flex"
],
"type": "string"
},
"ThreadStartSource": {
"enum": [
"startup",
"clear"
],
"type": "string"
}
},
"properties": {
@@ -210,6 +217,16 @@
"type": "null"
}
]
},
"sessionStartSource": {
"anyOf": [
{
"$ref": "#/definitions/ThreadStartSource"
},
{
"type": "null"
}
]
}
},
"title": "ThreadStartParams",

View File

@@ -7,12 +7,13 @@ import type { JsonValue } from "../serde_json/JsonValue";
import type { ApprovalsReviewer } from "./ApprovalsReviewer";
import type { AskForApproval } from "./AskForApproval";
import type { SandboxMode } from "./SandboxMode";
import type { ThreadStartSource } from "./ThreadStartSource";
export type ThreadStartParams = {model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier | null | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, /**
* Override where approval requests are routed for review on this thread
* and subsequent turns.
*/
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, /**
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, sessionStartSource?: ThreadStartSource | null, /**
* If true, opt into emitting raw Responses API items on the event stream.
* This is for internal use only (e.g. Codex Cloud).
*/

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ThreadStartSource = "startup" | "clear";

View File

@@ -310,6 +310,7 @@ export type { ThreadSortKey } from "./ThreadSortKey";
export type { ThreadSourceKind } from "./ThreadSourceKind";
export type { ThreadStartParams } from "./ThreadStartParams";
export type { ThreadStartResponse } from "./ThreadStartResponse";
export type { ThreadStartSource } from "./ThreadStartSource";
export type { ThreadStartedNotification } from "./ThreadStartedNotification";
export type { ThreadStatus } from "./ThreadStatus";
export type { ThreadStatusChangedNotification } from "./ThreadStatusChangedNotification";

View File

@@ -409,6 +409,14 @@ v2_enum_from_core!(
}
);
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase", export_to = "v2/")]
pub enum ThreadStartSource {
Startup,
Clear,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -2611,6 +2619,8 @@ pub struct ThreadStartParams {
pub personality: Option<Personality>,
#[ts(optional = nullable)]
pub ephemeral: Option<bool>,
#[ts(optional = nullable)]
pub session_start_source: Option<ThreadStartSource>,
#[experimental("thread/start.dynamicTools")]
#[ts(optional = nullable)]
pub dynamic_tools: Option<Vec<DynamicToolSpec>>,

View File

@@ -133,7 +133,7 @@ Example with notification opt-out:
## API Overview
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`.
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`.
- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it.
- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread.
- `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
@@ -212,6 +212,7 @@ Start a fresh thread when you need a new Codex conversation.
"sandbox": "workspaceWrite",
"personality": "friendly",
"serviceName": "my_app_server_client", // optional metrics tag (`service_name`)
"sessionStartSource": "startup", // optional: "startup" (default) or "clear"
// Experimental: requires opt-in
"dynamicTools": [
{

View File

@@ -2083,6 +2083,7 @@ impl CodexMessageProcessor {
experimental_raw_events,
personality,
ephemeral,
session_start_source,
persist_extended_history,
} = params;
let mut typesafe_overrides = self.build_thread_config_overrides(
@@ -2124,6 +2125,7 @@ impl CodexMessageProcessor {
config,
typesafe_overrides,
dynamic_tools,
session_start_source,
persist_extended_history,
service_name,
experimental_raw_events,
@@ -2199,6 +2201,7 @@ impl CodexMessageProcessor {
config_overrides: Option<HashMap<String, serde_json::Value>>,
typesafe_overrides: ConfigOverrides,
dynamic_tools: Option<Vec<ApiDynamicToolSpec>>,
session_start_source: Option<codex_app_server_protocol::ThreadStartSource>,
persist_extended_history: bool,
service_name: Option<String>,
experimental_raw_events: bool,
@@ -2322,6 +2325,12 @@ impl CodexMessageProcessor {
.thread_manager
.start_thread_with_tools_and_service_name(
config,
match session_start_source
.unwrap_or(codex_app_server_protocol::ThreadStartSource::Startup)
{
codex_app_server_protocol::ThreadStartSource::Startup => InitialHistory::New,
codex_app_server_protocol::ThreadStartSource::Clear => InitialHistory::Cleared,
},
core_dynamic_tools,
persist_extended_history,
service_name,
@@ -4239,7 +4248,7 @@ impl CodexMessageProcessor {
thread.preview = preview_from_rollout_items(items);
Ok(thread)
}
InitialHistory::New => Err(format!(
InitialHistory::New | InitialHistory::Cleared => Err(format!(
"failed to build resume response for thread {thread_id}: initial history missing"
)),
};
@@ -8856,7 +8865,7 @@ pub(crate) async fn read_rollout_items_from_rollout(
path: &Path,
) -> std::io::Result<Vec<RolloutItem>> {
let items = match RolloutRecorder::get_rollout_history(path).await? {
InitialHistory::New => Vec::new(),
InitialHistory::New | InitialHistory::Cleared => Vec::new(),
InitialHistory::Forked(items) => items,
InitialHistory::Resumed(resumed) => resumed.history,
};

View File

@@ -241,6 +241,7 @@ async fn skills_changed_notification_is_emitted_after_skill_change() -> Result<(
developer_instructions: None,
personality: None,
ephemeral: None,
session_start_source: None,
dynamic_tools: None,
mock_experimental_field: None,
experimental_raw_events: false,

View File

@@ -591,7 +591,7 @@ impl Codex {
let thread_id = match &conversation_history {
InitialHistory::Resumed(resumed) => Some(resumed.conversation_id),
InitialHistory::Forked(_) => conversation_history.forked_from_id(),
InitialHistory::New => None,
InitialHistory::New | InitialHistory::Cleared => None,
};
match thread_id {
Some(thread_id) => {
@@ -1530,7 +1530,7 @@ impl Session {
let forked_from_id = initial_history.forked_from_id();
let (conversation_id, rollout_params) = match &initial_history {
InitialHistory::New | InitialHistory::Forked(_) => {
InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_) => {
let conversation_id = ThreadId::default();
(
conversation_id,
@@ -1571,14 +1571,14 @@ impl Session {
.count(),
)
.unwrap_or(u64::MAX),
InitialHistory::New | InitialHistory::Forked(_) => 0,
InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_) => 0,
};
let state_builder = match &initial_history {
InitialHistory::Resumed(resumed) => metadata::builder_from_items(
resumed.history.as_slice(),
resumed.rollout_path.as_path(),
),
InitialHistory::New | InitialHistory::Forked(_) => None,
InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_) => None,
};
// Kick off independent async setup tasks in parallel to reduce startup latency.
@@ -2111,6 +2111,7 @@ impl Session {
InitialHistory::New | InitialHistory::Forked(_) => {
codex_hooks::SessionStartSource::Startup
}
InitialHistory::Cleared => codex_hooks::SessionStartSource::Clear,
};
// record_initial_history can emit events. We record only after the SessionConfiguredEvent is emitted.
@@ -2245,7 +2246,7 @@ impl Session {
)
};
match conversation_history {
InitialHistory::New => {
InitialHistory::New | InitialHistory::Cleared => {
// Defer initial context insertion until the first real turn starts so
// turn/start overrides can be merged before we write model-visible context.
self.set_previous_turn_settings(/*previous_turn_settings*/ None)

View File

@@ -469,6 +469,7 @@ impl ThreadManager {
) -> CodexResult<NewThread> {
Box::pin(self.start_thread_with_tools_and_service_name(
config,
InitialHistory::New,
dynamic_tools,
persist_extended_history,
/*metrics_service_name*/ None,
@@ -480,6 +481,7 @@ impl ThreadManager {
pub async fn start_thread_with_tools_and_service_name(
&self,
config: Config,
initial_history: InitialHistory,
dynamic_tools: Vec<codex_protocol::dynamic_tools::DynamicToolSpec>,
persist_extended_history: bool,
metrics_service_name: Option<String>,
@@ -487,7 +489,7 @@ impl ThreadManager {
) -> CodexResult<NewThread> {
Box::pin(self.state.spawn_thread(
config,
InitialHistory::New,
initial_history,
Arc::clone(&self.state.auth_manager),
self.agent_control(),
dynamic_tools,
@@ -663,6 +665,7 @@ impl ThreadManager {
ForkSnapshot::Interrupted => {
let history = match history {
InitialHistory::New => InitialHistory::New,
InitialHistory::Cleared => InitialHistory::Cleared,
InitialHistory::Forked(history) => InitialHistory::Forked(history),
InitialHistory::Resumed(resumed) => InitialHistory::Forked(resumed.history),
};
@@ -1064,7 +1067,7 @@ fn append_interrupted_boundary(history: InitialHistory, turn_id: Option<String>)
}));
match history {
InitialHistory::New => InitialHistory::Forked(vec![
InitialHistory::New | InitialHistory::Cleared => InitialHistory::Forked(vec![
RolloutItem::ResponseItem(interrupted_turn_history_marker()),
aborted_event,
]),

View File

@@ -20,6 +20,7 @@ use crate::schema::SessionStartCommandInput;
pub enum SessionStartSource {
Startup,
Resume,
Clear,
}
impl SessionStartSource {
@@ -27,6 +28,7 @@ impl SessionStartSource {
match self {
Self::Startup => "startup",
Self::Resume => "resume",
Self::Clear => "clear",
}
}
}

View File

@@ -2274,6 +2274,7 @@ pub struct ResumedHistory {
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub enum InitialHistory {
New,
Cleared,
Resumed(ResumedHistory),
Forked(Vec<RolloutItem>),
}
@@ -2281,7 +2282,7 @@ pub enum InitialHistory {
impl InitialHistory {
pub fn forked_from_id(&self) -> Option<ThreadId> {
match self {
InitialHistory::New => None,
InitialHistory::New | InitialHistory::Cleared => None,
InitialHistory::Resumed(resumed) => {
resumed.history.iter().find_map(|item| match item {
RolloutItem::SessionMeta(meta_line) => meta_line.meta.forked_from_id,
@@ -2297,7 +2298,7 @@ impl InitialHistory {
pub fn session_cwd(&self) -> Option<PathBuf> {
match self {
InitialHistory::New => None,
InitialHistory::New | InitialHistory::Cleared => None,
InitialHistory::Resumed(resumed) => session_cwd_from_items(&resumed.history),
InitialHistory::Forked(items) => session_cwd_from_items(items),
}
@@ -2305,7 +2306,7 @@ impl InitialHistory {
pub fn get_rollout_items(&self) -> Vec<RolloutItem> {
match self {
InitialHistory::New => Vec::new(),
InitialHistory::New | InitialHistory::Cleared => Vec::new(),
InitialHistory::Resumed(resumed) => resumed.history.clone(),
InitialHistory::Forked(items) => items.clone(),
}
@@ -2313,7 +2314,7 @@ impl InitialHistory {
pub fn get_event_msgs(&self) -> Option<Vec<EventMsg>> {
match self {
InitialHistory::New => None,
InitialHistory::New | InitialHistory::Cleared => None,
InitialHistory::Resumed(resumed) => Some(
resumed
.history
@@ -2339,7 +2340,7 @@ impl InitialHistory {
pub fn get_base_instructions(&self) -> Option<BaseInstructions> {
// TODO: SessionMeta should (in theory) always be first in the history, so we can probably only check the first item?
match self {
InitialHistory::New => None,
InitialHistory::New | InitialHistory::Cleared => None,
InitialHistory::Resumed(resumed) => {
resumed.history.iter().find_map(|item| match item {
RolloutItem::SessionMeta(meta_line) => meta_line.meta.base_instructions.clone(),
@@ -2355,7 +2356,7 @@ impl InitialHistory {
pub fn get_dynamic_tools(&self) -> Option<Vec<DynamicToolSpec>> {
match self {
InitialHistory::New => None,
InitialHistory::New | InitialHistory::Cleared => None,
InitialHistory::Resumed(resumed) => {
resumed.history.iter().find_map(|item| match item {
RolloutItem::SessionMeta(meta_line) => meta_line.meta.dynamic_tools.clone(),

View File

@@ -80,6 +80,7 @@ use codex_app_server_protocol::SkillsListResponse;
use codex_app_server_protocol::ThreadItem;
use codex_app_server_protocol::ThreadLoadedListParams;
use codex_app_server_protocol::ThreadRollbackResponse;
use codex_app_server_protocol::ThreadStartSource;
use codex_app_server_protocol::Turn;
use codex_app_server_protocol::TurnError as AppServerTurnError;
use codex_app_server_protocol::TurnStatus;
@@ -3271,6 +3272,7 @@ impl App {
&mut self,
tui: &mut tui::Tui,
app_server: &mut AppServerSession,
session_start_source: Option<ThreadStartSource>,
) {
// Start a fresh in-memory session while preserving resumability via persisted rollout
// history.
@@ -3292,7 +3294,10 @@ impl App {
}
}
self.config = config.clone();
match app_server.start_thread(&config).await {
match app_server
.start_thread_with_session_start_source(&config, session_start_source)
.await
{
Ok(started) => {
if let Err(err) = self
.replace_chat_widget_with_app_server_thread(tui, app_server, started)
@@ -4019,15 +4024,21 @@ impl App {
) -> Result<AppRunControl> {
match event {
AppEvent::NewSession => {
self.start_fresh_session_with_summary_hint(tui, app_server)
.await;
self.start_fresh_session_with_summary_hint(
tui, app_server, /*session_start_source*/ None,
)
.await;
}
AppEvent::ClearUi => {
self.clear_terminal_ui(tui, /*redraw_header*/ false)?;
self.reset_app_ui_state_after_clear();
self.start_fresh_session_with_summary_hint(tui, app_server)
.await;
self.start_fresh_session_with_summary_hint(
tui,
app_server,
Some(ThreadStartSource::Clear),
)
.await;
}
AppEvent::OpenResumePicker => {
let picker_app_server = match crate::start_app_server_for_picker(

View File

@@ -55,6 +55,7 @@ use codex_app_server_protocol::ThreadShellCommandParams;
use codex_app_server_protocol::ThreadShellCommandResponse;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
use codex_app_server_protocol::ThreadStartSource;
use codex_app_server_protocol::ThreadUnsubscribeParams;
use codex_app_server_protocol::ThreadUnsubscribeResponse;
use codex_app_server_protocol::Turn;
@@ -300,6 +301,15 @@ impl AppServerSession {
}
pub(crate) async fn start_thread(&mut self, config: &Config) -> Result<AppServerStartedThread> {
self.start_thread_with_session_start_source(config, /*session_start_source*/ None)
.await
}
pub(crate) async fn start_thread_with_session_start_source(
&mut self,
config: &Config,
session_start_source: Option<ThreadStartSource>,
) -> Result<AppServerStartedThread> {
let request_id = self.next_request_id();
let response: ThreadStartResponse = self
.client
@@ -309,6 +319,7 @@ impl AppServerSession {
config,
self.thread_params_mode(),
self.remote_cwd_override.as_deref(),
session_start_source,
),
})
.await
@@ -869,6 +880,7 @@ fn thread_start_params_from_config(
config: &Config,
thread_params_mode: ThreadParamsMode,
remote_cwd_override: Option<&std::path::Path>,
session_start_source: Option<ThreadStartSource>,
) -> ThreadStartParams {
ThreadStartParams {
model: config.model.clone(),
@@ -879,6 +891,7 @@ fn thread_start_params_from_config(
sandbox: sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone()),
config: config_request_overrides_from_config(config),
ephemeral: Some(config.ephemeral),
session_start_source,
persist_extended_history: true,
..ThreadStartParams::default()
}
@@ -1185,12 +1198,28 @@ mod tests {
&config,
ThreadParamsMode::Embedded,
/*remote_cwd_override*/ None,
/*session_start_source*/ None,
);
assert_eq!(params.cwd, Some(config.cwd.to_string_lossy().to_string()));
assert_eq!(params.model_provider, Some(config.model_provider_id));
}
#[tokio::test]
async fn thread_start_params_can_mark_clear_source() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let config = build_config(&temp_dir).await;
let params = thread_start_params_from_config(
&config,
ThreadParamsMode::Embedded,
/*remote_cwd_override*/ None,
Some(ThreadStartSource::Clear),
);
assert_eq!(params.session_start_source, Some(ThreadStartSource::Clear));
}
#[tokio::test]
async fn thread_lifecycle_params_omit_cwd_without_remote_override_for_remote_sessions() {
let temp_dir = tempfile::tempdir().expect("tempdir");
@@ -1201,6 +1230,7 @@ mod tests {
&config,
ThreadParamsMode::Remote,
/*remote_cwd_override*/ None,
/*session_start_source*/ None,
);
let resume = thread_resume_params_from_config(
config.clone(),
@@ -1234,6 +1264,7 @@ mod tests {
&config,
ThreadParamsMode::Remote,
Some(remote_cwd.as_path()),
/*session_start_source*/ None,
);
let resume = thread_resume_params_from_config(
config.clone(),