Skip to content
Open
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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions codex-rs/app-server-protocol/schema/typescript/v2/index.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,11 @@ client_request_definitions! {
serialization: thread_id(params.thread_id),
response: v2::ThreadRecapResponse,
},
ThreadContinue => "thread/continue" {
params: v2::ThreadContinueParams,
serialization: thread_id(params.destination_thread_id),
response: v2::ThreadContinueResponse,
},
ThreadShellCommand => "thread/shellCommand" {
params: v2::ThreadShellCommandParams,
serialization: thread_id(params.thread_id),
Expand Down Expand Up @@ -2282,6 +2287,20 @@ mod tests {
})
);

let thread_continue = ClientRequest::ThreadContinue {
request_id: request_id(),
params: v2::ThreadContinueParams {
destination_thread_id: thread_id.clone(),
source_thread_id: "thread-source".to_string(),
},
};
assert_eq!(
thread_continue.serialization_scope(),
Some(ClientRequestSerializationScope::Thread {
thread_id: thread_id.clone()
})
);

let thread_fork = ClientRequest::ThreadFork {
request_id: request_id(),
params: v2::ThreadForkParams {
Expand Down
17 changes: 17 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/v2/thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1897,6 +1897,23 @@ pub struct ThreadRecapResponse {
pub summary: String,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadContinueParams {
/// Loaded destination thread that will receive the generated continuation recap.
pub destination_thread_id: String,
/// Persisted source thread whose effective history should be summarized.
pub source_thread_id: String,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadContinueResponse {
pub summary: String,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
Expand Down
13 changes: 13 additions & 0 deletions codex-rs/app-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ Example with notification opt-out:

- `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"`. To create a fresh child agent thread, pass `threadSource: "subagent"` with `parentThreadId`; the returned thread and `thread/started` notification include the subagent source and parent id. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. For permissions, prefer experimental `permissions` profile selection by id; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd`.
- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`.
- `thread/continue` — summarize the effective persisted history of a source thread into a different loaded, idle destination thread without resuming, forking, loading, or switching to the source. The destination's current model/provider/auth generates the recap, which is recorded once as model-visible assistant context and a replay-visible agent message.
- `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. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately. Accepts the same permission override rules as `thread/start`.
- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known.
- `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. Subagent threads also include `parentThreadId` when the immediate control/spawn parent is known.
Expand Down Expand Up @@ -435,6 +436,18 @@ Example:
} }
```

To bring another stored session's work into the currently loaded session without switching threads, call `thread/continue`. `destinationThreadId` must identify a loaded, idle thread; `sourceThreadId` may identify an archived source. App-server reads the source directly from the thread store with history enabled, reconstructs compaction and rollback markers, asks the destination's current model/auth context for a concise handoff recap, and appends that recap to the captured destination. If the destination becomes active before insertion, the request fails without injecting the recap.

```json
{ "method": "thread/continue", "id": 14, "params": {
"destinationThreadId": "thr_current",
"sourceThreadId": "thr_previous"
} }
{ "id": 14, "result": {
"summary": "The previous session implemented the parser and left the integration test failing on archived input; next, update the fixture and rerun the focused gate."
} }
```

To branch from a stored session, call `thread/fork` with the `thread.id`. This creates a new thread id and emits a `thread/started` notification for it. The returned `thread.sessionId` identifies the current live session tree root. Root threads use their own `thread.id` as `thread.sessionId`; stored threads that are not loaded also report their own `thread.id`, because resuming one makes it the root of a new live session tree. When the source history includes persisted token usage, the server also emits `thread/tokenUsage/updated` for the new thread immediately after the response. If the source thread is actively running, the fork snapshots it as if the current turn had been interrupted first. Pass `ephemeral: true` when the fork should stay in-memory only:

```json
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/app-server/src/message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1579,6 +1579,9 @@ impl MessageProcessor {
ClientRequest::ThreadRecap { params, .. } => {
self.thread_processor.thread_recap(params).await
}
ClientRequest::ThreadContinue { params, .. } => {
self.thread_processor.thread_continue(params).await
}
ClientRequest::ThreadBackgroundTerminalsClean { params, .. } => {
self.thread_processor
.thread_background_terminals_clean(&request_id, params)
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/app-server/src/request_processors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ use codex_app_server_protocol::ThreadBackgroundTerminalsCleanResponse;
use codex_app_server_protocol::ThreadClosedNotification;
use codex_app_server_protocol::ThreadCompactStartParams;
use codex_app_server_protocol::ThreadCompactStartResponse;
use codex_app_server_protocol::ThreadContinueParams;
use codex_app_server_protocol::ThreadContinueResponse;
use codex_app_server_protocol::ThreadDecrementElicitationParams;
use codex_app_server_protocol::ThreadDecrementElicitationResponse;
use codex_app_server_protocol::ThreadExternalAgentCancelParams;
Expand Down
60 changes: 60 additions & 0 deletions codex-rs/app-server/src/request_processors/thread_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,15 @@ impl ThreadRequestProcessor {
.map(|response| Some(response.into()))
}

pub(crate) async fn thread_continue(
&self,
params: ThreadContinueParams,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
self.thread_continue_inner(params)
.await
.map(|response| Some(response.into()))
}

pub(crate) async fn thread_background_terminals_clean(
&self,
request_id: &ConnectionRequestId,
Expand Down Expand Up @@ -2063,6 +2072,57 @@ impl ThreadRequestProcessor {
Ok(ThreadRecapResponse { summary })
}

async fn thread_continue_inner(
&self,
params: ThreadContinueParams,
) -> Result<ThreadContinueResponse, JSONRPCErrorError> {
let ThreadContinueParams {
destination_thread_id,
source_thread_id,
} = params;
let (destination_thread_id, destination_thread) =
self.load_thread(&destination_thread_id).await?;
let source_thread_id = ThreadId::from_string(&source_thread_id)
.map_err(|err| invalid_request(format!("invalid source thread id: {err}")))?;
if source_thread_id == destination_thread_id {
return Err(invalid_request(
"source and destination thread ids must differ".to_string(),
));
}
if matches!(destination_thread.agent_status().await, AgentStatus::Running) {
return Err(invalid_request(format!(
"destination thread is active: {destination_thread_id}"
)));
}

let stored_source = self
.thread_store
.read_thread(StoreReadThreadParams {
thread_id: source_thread_id,
include_archived: true,
include_history: true,
})
.await
.map_err(thread_store_resume_read_error)?;
let source_history = stored_source.history.ok_or_else(|| {
internal_error(format!(
"thread store did not return history for source thread {source_thread_id}"
))
})?;
let summary = destination_thread
.generate_session_continuation(&source_history.items)
.await
.map_err(|err| internal_error(format!("failed to generate continuation recap: {err}")))?;
destination_thread
.record_session_continuation_if_idle(summary.clone())
.await
.map_err(|err| match err {
CodexErr::InvalidRequest(message) => invalid_request(message),
err => internal_error(format!("failed to record continuation recap: {err}")),
})?;
Ok(ThreadContinueResponse { summary })
}

async fn thread_background_terminals_clean_inner(
&self,
request_id: &ConnectionRequestId,
Expand Down
10 changes: 10 additions & 0 deletions codex-rs/app-server/tests/common/test_app_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ use codex_app_server_protocol::ThreadExternalAgentPermissionRespondParams;
use codex_app_server_protocol::ThreadExternalAgentStartParams;
use codex_app_server_protocol::ThreadForkParams;
use codex_app_server_protocol::ThreadInjectItemsParams;
use codex_app_server_protocol::ThreadContinueParams;
use codex_app_server_protocol::ThreadListParams;
use codex_app_server_protocol::ThreadLoadedListParams;
use codex_app_server_protocol::ThreadMailboxAckParams;
Expand Down Expand Up @@ -1242,6 +1243,15 @@ impl TestAppServer {
self.send_request("thread/inject_items", params).await
}

/// Send a `thread/continue` JSON-RPC request (v2).
pub async fn send_thread_continue_request(
&mut self,
params: ThreadContinueParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("thread/continue", params).await
}

/// Send a `command/exec` JSON-RPC request (v2).
pub async fn send_command_exec_request(
&mut self,
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/tests/suite/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ mod review;
mod safety_check_downgrade;
mod skills_list;
mod thread_archive;
mod thread_continue;
mod thread_external_agent;
mod thread_fork;
mod thread_inject_items;
Expand Down
Loading
Loading