mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
app-server: Add ephemeral field to Thread object (#13084)
Currently there is no alternative way to know that thread is ephemeral, only client which did create it has the knowledge.
This commit is contained in:
committed by
GitHub
Unverified
parent
1a8d930267
commit
8c1e3f3e64
@@ -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": [
|
||||
{
|
||||
@@ -1724,6 +1728,7 @@
|
||||
"cliVersion",
|
||||
"createdAt",
|
||||
"cwd",
|
||||
"ephemeral",
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
@@ -12567,6 +12571,7 @@
|
||||
"cliVersion",
|
||||
"createdAt",
|
||||
"cwd",
|
||||
"ephemeral",
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
@@ -951,6 +955,7 @@
|
||||
"cliVersion",
|
||||
"createdAt",
|
||||
"cwd",
|
||||
"ephemeral",
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
@@ -724,6 +728,7 @@
|
||||
"cliVersion",
|
||||
"createdAt",
|
||||
"cwd",
|
||||
"ephemeral",
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
@@ -724,6 +728,7 @@
|
||||
"cliVersion",
|
||||
"createdAt",
|
||||
"cwd",
|
||||
"ephemeral",
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
@@ -951,6 +955,7 @@
|
||||
"cliVersion",
|
||||
"createdAt",
|
||||
"cwd",
|
||||
"ephemeral",
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
@@ -724,6 +728,7 @@
|
||||
"cliVersion",
|
||||
"createdAt",
|
||||
"cwd",
|
||||
"ephemeral",
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
@@ -951,6 +955,7 @@
|
||||
"cliVersion",
|
||||
"createdAt",
|
||||
"cwd",
|
||||
"ephemeral",
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
@@ -724,6 +728,7 @@
|
||||
"cliVersion",
|
||||
"createdAt",
|
||||
"cwd",
|
||||
"ephemeral",
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
@@ -724,6 +728,7 @@
|
||||
"cliVersion",
|
||||
"createdAt",
|
||||
"cwd",
|
||||
"ephemeral",
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
|
||||
@@ -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').
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -7869,6 +7869,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,
|
||||
@@ -7910,6 +7911,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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -851,6 +851,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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user