Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1655,6 +1655,10 @@
"description": "Working directory captured for the thread.",
"type": "string"
},
"ephemeral": {
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
"type": "boolean"
},
"gitInfo": {
"anyOf": [
{
Expand Down Expand Up @@ -1724,6 +1728,7 @@
"cliVersion",
"createdAt",
"cwd",
"ephemeral",
"id",
"modelProvider",
"preview",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12498,6 +12498,10 @@
"description": "Working directory captured for the thread.",
"type": "string"
},
"ephemeral": {
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
"type": "boolean"
},
"gitInfo": {
"anyOf": [
{
Expand Down Expand Up @@ -12567,6 +12571,7 @@
"cliVersion",
"createdAt",
"cwd",
"ephemeral",
"id",
"modelProvider",
"preview",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,10 @@
"description": "Working directory captured for the thread.",
"type": "string"
},
"ephemeral": {
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
"type": "boolean"
},
"gitInfo": {
"anyOf": [
{
Expand Down Expand Up @@ -951,6 +955,7 @@
"cliVersion",
"createdAt",
"cwd",
"ephemeral",
"id",
"modelProvider",
"preview",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,10 @@
"description": "Working directory captured for the thread.",
"type": "string"
},
"ephemeral": {
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
"type": "boolean"
},
"gitInfo": {
"anyOf": [
{
Expand Down Expand Up @@ -724,6 +728,7 @@
"cliVersion",
"createdAt",
"cwd",
"ephemeral",
"id",
"modelProvider",
"preview",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,10 @@
"description": "Working directory captured for the thread.",
"type": "string"
},
"ephemeral": {
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
"type": "boolean"
},
"gitInfo": {
"anyOf": [
{
Expand Down Expand Up @@ -724,6 +728,7 @@
"cliVersion",
"createdAt",
"cwd",
"ephemeral",
"id",
"modelProvider",
"preview",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,10 @@
"description": "Working directory captured for the thread.",
"type": "string"
},
"ephemeral": {
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
"type": "boolean"
},
"gitInfo": {
"anyOf": [
{
Expand Down Expand Up @@ -951,6 +955,7 @@
"cliVersion",
"createdAt",
"cwd",
"ephemeral",
"id",
"modelProvider",
"preview",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,10 @@
"description": "Working directory captured for the thread.",
"type": "string"
},
"ephemeral": {
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
"type": "boolean"
},
"gitInfo": {
"anyOf": [
{
Expand Down Expand Up @@ -724,6 +728,7 @@
"cliVersion",
"createdAt",
"cwd",
"ephemeral",
"id",
"modelProvider",
"preview",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,10 @@
"description": "Working directory captured for the thread.",
"type": "string"
},
"ephemeral": {
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
"type": "boolean"
},
"gitInfo": {
"anyOf": [
{
Expand Down Expand Up @@ -951,6 +955,7 @@
"cliVersion",
"createdAt",
"cwd",
"ephemeral",
"id",
"modelProvider",
"preview",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,10 @@
"description": "Working directory captured for the thread.",
"type": "string"
},
"ephemeral": {
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
"type": "boolean"
},
"gitInfo": {
"anyOf": [
{
Expand Down Expand Up @@ -724,6 +728,7 @@
"cliVersion",
"createdAt",
"cwd",
"ephemeral",
"id",
"modelProvider",
"preview",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,10 @@
"description": "Working directory captured for the thread.",
"type": "string"
},
"ephemeral": {
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
"type": "boolean"
},
"gitInfo": {
"anyOf": [
{
Expand Down Expand Up @@ -724,6 +728,7 @@
"cliVersion",
"createdAt",
"cwd",
"ephemeral",
"id",
"modelProvider",
"preview",
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/app-server-protocol/schema/typescript/v2/Thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export type Thread = { id: string,
* Usually the first user message in the thread, if available.
*/
preview: string,
/**
* Whether the thread is ephemeral and should not be materialized on disk.
*/
ephemeral: boolean,
/**
* Model provider used for this thread (for example, 'openai').
*/
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2517,6 +2517,8 @@ pub struct Thread {
pub id: String,
/// Usually the first user message in the thread, if available.
pub preview: String,
/// Whether the thread is ephemeral and should not be materialized on disk.
pub ephemeral: bool,
/// Model provider used for this thread (for example, 'openai').
pub model_provider: String,
/// Unix timestamp (in seconds) when the thread was created.
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Use the thread APIs to create, list, or archive conversations. Drive a conversat

- Initialize once per connection: Immediately after opening a transport connection, send an `initialize` request with your client metadata, then emit an `initialized` notification. Any other request on that connection before this handshake gets rejected.
- Start (or resume) a thread: Call `thread/start` to open a fresh conversation. The response returns the thread object and you’ll also get a `thread/started` notification. If you’re continuing an existing conversation, call `thread/resume` with its ID instead. If you want to branch from an existing conversation, call `thread/fork` to create a new thread id with copied history.
The returned `thread.ephemeral` flag tells you whether the session is intentionally in-memory only; when it is `true`, `thread.path` is `null`.
- Begin a turn: To send user input, call `turn/start` with the target `threadId` and the user's input. Optional fields let you override model, cwd, sandbox policy, etc. This immediately returns the new turn object and triggers a `turn/started` notification.
- Stream events: After `turn/start`, keep reading JSON-RPC notifications on stdout. You’ll see `item/started`, `item/completed`, deltas like `item/agentMessage/delta`, tool progress, etc. These represent streaming model output plus any side effects (commands, tool calls, reasoning notes).
- Finish the turn: When the model is done (or the turn is interrupted via making the `turn/interrupt` call), the server sends `turn/completed` with the final turn state and token usage.
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/app-server/src/codex_message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7670,6 +7670,7 @@ fn build_thread_from_snapshot(
Thread {
id: thread_id.to_string(),
preview: String::new(),
ephemeral: config_snapshot.ephemeral,
model_provider: config_snapshot.model_provider_id.clone(),
created_at: now,
updated_at: now,
Expand Down Expand Up @@ -7711,6 +7712,7 @@ pub(crate) fn summary_to_thread(summary: ConversationSummary) -> Thread {
Thread {
id: conversation_id.to_string(),
preview,
ephemeral: false,
model_provider,
created_at: created_at.map(|dt| dt.timestamp()).unwrap_or(0),
updated_at: updated_at.map(|dt| dt.timestamp()).unwrap_or(0),
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/src/thread_status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,7 @@ mod tests {
Thread {
id: thread_id.to_string(),
preview: String::new(),
ephemeral: false,
model_provider: "mock-provider".to_string(),
created_at: 0,
updated_at: 0,
Expand Down
16 changes: 16 additions & 0 deletions codex-rs/app-server/tests/suite/v2/thread_read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ async fn thread_read_returns_summary_without_turns() -> Result<()> {
assert_eq!(thread.id, conversation_id);
assert_eq!(thread.preview, preview);
assert_eq!(thread.model_provider, "mock_provider");
assert!(!thread.ephemeral, "stored rollouts should not be ephemeral");
assert!(thread.path.as_ref().expect("thread path").is_absolute());
assert_eq!(thread.cwd, PathBuf::from("/"));
assert_eq!(thread.cli_version, "0.0.0");
Expand Down Expand Up @@ -278,6 +279,11 @@ async fn thread_name_set_is_reflected_in_read_list_and_resume() -> Result<()> {
Some(new_name),
"thread/read must serialize `thread.name` on the wire"
);
assert_eq!(
thread_json.get("ephemeral").and_then(Value::as_bool),
Some(false),
"thread/read must serialize `thread.ephemeral` on the wire"
);

// List should also surface the name.
let list_id = mcp
Expand Down Expand Up @@ -317,6 +323,11 @@ async fn thread_name_set_is_reflected_in_read_list_and_resume() -> Result<()> {
Some(new_name),
"thread/list must serialize `thread.name` on the wire"
);
assert_eq!(
listed_json.get("ephemeral").and_then(Value::as_bool),
Some(false),
"thread/list must serialize `thread.ephemeral` on the wire"
);

// Resume should also surface the name.
let resume_id = mcp
Expand Down Expand Up @@ -345,6 +356,11 @@ async fn thread_name_set_is_reflected_in_read_list_and_resume() -> Result<()> {
Some(new_name),
"thread/resume must serialize `thread.name` on the wire"
);
assert_eq!(
resumed_json.get("ephemeral").and_then(Value::as_bool),
Some(false),
"thread/resume must serialize `thread.ephemeral` on the wire"
);

Ok(())
}
Expand Down
30 changes: 30 additions & 0 deletions codex-rs/app-server/tests/suite/v2/thread_start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ async fn thread_start_creates_thread_and_emits_started() -> Result<()> {
thread.created_at > 0,
"created_at should be a positive UNIX timestamp"
);
assert!(
!thread.ephemeral,
"new persistent threads should not be ephemeral"
);
assert_eq!(thread.status, ThreadStatus::Idle);
let thread_path = thread.path.clone().expect("thread path should be present");
assert!(thread_path.is_absolute(), "thread path should be absolute");
Expand All @@ -80,6 +84,11 @@ async fn thread_start_creates_thread_and_emits_started() -> Result<()> {
Some(&Value::Null),
"new threads should serialize `name: null`"
);
assert_eq!(
thread_json.get("ephemeral").and_then(Value::as_bool),
Some(false),
"new persistent threads should serialize `ephemeral: false`"
);
assert_eq!(thread.name, None);

// A corresponding thread/started notification should arrive.
Expand All @@ -98,6 +107,13 @@ async fn thread_start_creates_thread_and_emits_started() -> Result<()> {
Some(&Value::Null),
"thread/started should serialize `name: null` for new threads"
);
assert_eq!(
started_thread_json
.get("ephemeral")
.and_then(Value::as_bool),
Some(false),
"thread/started should serialize `ephemeral: false` for new persistent threads"
);
let started: ThreadStartedNotification =
serde_json::from_value(notif.params.expect("params must be present"))?;
assert_eq!(started.thread, thread);
Expand Down Expand Up @@ -196,11 +212,25 @@ async fn thread_start_ephemeral_remains_pathless() -> Result<()> {
mcp.read_stream_until_response_message(RequestId::Integer(req_id)),
)
.await??;
let resp_result = resp.result.clone();
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(resp)?;
assert!(
thread.ephemeral,
"ephemeral threads should be marked explicitly"
);
assert_eq!(
thread.path, None,
"ephemeral threads should not expose a path"
);
let thread_json = resp_result
.get("thread")
.and_then(Value::as_object)
.expect("thread/start result.thread must be an object");
assert_eq!(
thread_json.get("ephemeral").and_then(Value::as_bool),
Some(true),
"ephemeral threads should serialize `ephemeral: true`"
);

Ok(())
}
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,7 @@ impl SessionConfiguration {
approval_policy: self.approval_policy.value(),
sandbox_policy: self.sandbox_policy.get().clone(),
cwd: self.cwd.clone(),
ephemeral: self.original_config_do_not_use.ephemeral,
reasoning_effort: self.collaboration_mode.reasoning_effort(),
personality: self.personality,
session_source: self.session_source.clone(),
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/codex_thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub struct ThreadConfigSnapshot {
pub approval_policy: AskForApproval,
pub sandbox_policy: SandboxPolicy,
pub cwd: PathBuf,
pub ephemeral: bool,
pub reasoning_effort: Option<ReasoningEffort>,
pub personality: Option<Personality>,
pub session_source: SessionSource,
Expand Down
Loading