From 68738579eb33d60be97219ca481a41e74df76be8 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Tue, 9 Jun 2026 13:24:53 +0100 Subject: [PATCH] Disable multi-agent tools in side conversations --- .../schema/json/ClientRequest.json | 5 +- .../schema/json/ServerNotification.json | 3 +- .../codex_app_server_protocol.schemas.json | 7 +- .../codex_app_server_protocol.v2.schemas.json | 7 +- .../schema/json/v2/ThreadForkParams.json | 3 +- .../schema/json/v2/ThreadForkResponse.json | 3 +- .../schema/json/v2/ThreadListResponse.json | 3 +- .../json/v2/ThreadMetadataUpdateResponse.json | 3 +- .../schema/json/v2/ThreadReadResponse.json | 3 +- .../schema/json/v2/ThreadResumeResponse.json | 3 +- .../json/v2/ThreadRollbackResponse.json | 3 +- .../schema/json/v2/ThreadStartParams.json | 3 +- .../schema/json/v2/ThreadStartResponse.json | 3 +- .../json/v2/ThreadStartedNotification.json | 3 +- .../json/v2/ThreadUnarchiveResponse.json | 3 +- .../schema/typescript/v2/Thread.ts | 3 +- .../schema/typescript/v2/ThreadForkParams.ts | 3 +- .../schema/typescript/v2/ThreadSource.ts | 2 +- .../schema/typescript/v2/ThreadStartParams.ts | 3 +- .../src/protocol/v2/thread.rs | 6 +- .../src/protocol/v2/thread_data.rs | 6 +- codex-rs/app-server/README.md | 2 +- codex-rs/core/src/tools/spec_plan.rs | 4 + codex-rs/core/src/tools/spec_plan_tests.rs | 37 ++++++++ codex-rs/core/tests/suite/tools.rs | 87 +++++++++++++++++++ codex-rs/protocol/src/protocol.rs | 3 + codex-rs/tui/src/app.rs | 3 +- codex-rs/tui/src/app/event_dispatch.rs | 5 +- codex-rs/tui/src/app/side.rs | 5 +- codex-rs/tui/src/app_server_session.rs | 12 ++- 30 files changed, 204 insertions(+), 32 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 6d9be4ec02..fa51451f29 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -3237,7 +3237,7 @@ "type": "null" } ], - "description": "Optional client-supplied analytics source classification for this forked thread." + "description": "Optional client-supplied source classification for this forked thread. Threads marked as `side` do not receive multi-agent collaboration tools." } }, "required": [ @@ -3797,6 +3797,7 @@ "ThreadSource": { "enum": [ "user", + "side", "subagent", "memory_consolidation" ], @@ -3934,7 +3935,7 @@ "type": "null" } ], - "description": "Optional client-supplied analytics source classification for this thread." + "description": "Optional client-supplied source classification for this thread. Threads marked as `side` do not receive multi-agent collaboration tools." } }, "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 01f6f58b09..3e19cded00 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -3463,7 +3463,7 @@ "type": "null" } ], - "description": "Optional analytics source classification for this thread." + "description": "Optional source classification for this thread. `side` threads do not receive multi-agent collaboration tools." }, "turns": { "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", @@ -4617,6 +4617,7 @@ "ThreadSource": { "enum": [ "user", + "side", "subagent", "memory_consolidation" ], diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 7abc5af7d0..76e8dd1fa8 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -15879,7 +15879,7 @@ "type": "null" } ], - "description": "Optional analytics source classification for this thread." + "description": "Optional source classification for this thread. `side` threads do not receive multi-agent collaboration tools." }, "turns": { "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", @@ -16095,7 +16095,7 @@ "type": "null" } ], - "description": "Optional client-supplied analytics source classification for this forked thread." + "description": "Optional client-supplied source classification for this forked thread. Threads marked as `side` do not receive multi-agent collaboration tools." } }, "required": [ @@ -18059,6 +18059,7 @@ "ThreadSource": { "enum": [ "user", + "side", "subagent", "memory_consolidation" ], @@ -18197,7 +18198,7 @@ "type": "null" } ], - "description": "Optional client-supplied analytics source classification for this thread." + "description": "Optional client-supplied source classification for this thread. Threads marked as `side` do not receive multi-agent collaboration tools." } }, "title": "ThreadStartParams", diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index e5ac77c088..588351d476 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -13696,7 +13696,7 @@ "type": "null" } ], - "description": "Optional analytics source classification for this thread." + "description": "Optional source classification for this thread. `side` threads do not receive multi-agent collaboration tools." }, "turns": { "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", @@ -13912,7 +13912,7 @@ "type": "null" } ], - "description": "Optional client-supplied analytics source classification for this forked thread." + "description": "Optional client-supplied source classification for this forked thread. Threads marked as `side` do not receive multi-agent collaboration tools." } }, "required": [ @@ -15876,6 +15876,7 @@ "ThreadSource": { "enum": [ "user", + "side", "subagent", "memory_consolidation" ], @@ -16014,7 +16015,7 @@ "type": "null" } ], - "description": "Optional client-supplied analytics source classification for this thread." + "description": "Optional client-supplied source classification for this thread. Threads marked as `side` do not receive multi-agent collaboration tools." } }, "title": "ThreadStartParams", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json index d9a543e939..91fab2fb4f 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json @@ -75,6 +75,7 @@ "ThreadSource": { "enum": [ "user", + "side", "subagent", "memory_consolidation" ], @@ -173,7 +174,7 @@ "type": "null" } ], - "description": "Optional client-supplied analytics source classification for this forked thread." + "description": "Optional client-supplied source classification for this forked thread. Threads marked as `side` do not receive multi-agent collaboration tools." } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 0cea90749b..c71e4bd195 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -1084,7 +1084,7 @@ "type": "null" } ], - "description": "Optional analytics source classification for this thread." + "description": "Optional source classification for this thread. `side` threads do not receive multi-agent collaboration tools." }, "turns": { "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", @@ -1832,6 +1832,7 @@ "ThreadSource": { "enum": [ "user", + "side", "subagent", "memory_consolidation" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index d0b3351a4e..f1555d856e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -899,7 +899,7 @@ "type": "null" } ], - "description": "Optional analytics source classification for this thread." + "description": "Optional source classification for this thread. `side` threads do not receive multi-agent collaboration tools." }, "turns": { "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", @@ -1647,6 +1647,7 @@ "ThreadSource": { "enum": [ "user", + "side", "subagent", "memory_consolidation" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json index e2850b3dd9..f7fcde886f 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json @@ -899,7 +899,7 @@ "type": "null" } ], - "description": "Optional analytics source classification for this thread." + "description": "Optional source classification for this thread. `side` threads do not receive multi-agent collaboration tools." }, "turns": { "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", @@ -1647,6 +1647,7 @@ "ThreadSource": { "enum": [ "user", + "side", "subagent", "memory_consolidation" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index 01da3165b1..7b27d7db84 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -899,7 +899,7 @@ "type": "null" } ], - "description": "Optional analytics source classification for this thread." + "description": "Optional source classification for this thread. `side` threads do not receive multi-agent collaboration tools." }, "turns": { "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", @@ -1647,6 +1647,7 @@ "ThreadSource": { "enum": [ "user", + "side", "subagent", "memory_consolidation" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index d766d7a024..c5a0fb0b05 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -1084,7 +1084,7 @@ "type": "null" } ], - "description": "Optional analytics source classification for this thread." + "description": "Optional source classification for this thread. `side` threads do not receive multi-agent collaboration tools." }, "turns": { "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", @@ -1832,6 +1832,7 @@ "ThreadSource": { "enum": [ "user", + "side", "subagent", "memory_consolidation" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json index 6448ca6517..841478ec03 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -899,7 +899,7 @@ "type": "null" } ], - "description": "Optional analytics source classification for this thread." + "description": "Optional source classification for this thread. `side` threads do not receive multi-agent collaboration tools." }, "turns": { "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", @@ -1647,6 +1647,7 @@ "ThreadSource": { "enum": [ "user", + "side", "subagent", "memory_consolidation" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json index 99b25490ab..84ef43e66e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json @@ -109,6 +109,7 @@ "ThreadSource": { "enum": [ "user", + "side", "subagent", "memory_consolidation" ], @@ -253,7 +254,7 @@ "type": "null" } ], - "description": "Optional client-supplied analytics source classification for this thread." + "description": "Optional client-supplied source classification for this thread. Threads marked as `side` do not receive multi-agent collaboration tools." } }, "title": "ThreadStartParams", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index a19a40f522..e53a27f1ba 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -1084,7 +1084,7 @@ "type": "null" } ], - "description": "Optional analytics source classification for this thread." + "description": "Optional source classification for this thread. `side` threads do not receive multi-agent collaboration tools." }, "turns": { "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", @@ -1832,6 +1832,7 @@ "ThreadSource": { "enum": [ "user", + "side", "subagent", "memory_consolidation" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index 0eb79075f6..de390408ec 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -899,7 +899,7 @@ "type": "null" } ], - "description": "Optional analytics source classification for this thread." + "description": "Optional source classification for this thread. `side` threads do not receive multi-agent collaboration tools." }, "turns": { "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", @@ -1647,6 +1647,7 @@ "ThreadSource": { "enum": [ "user", + "side", "subagent", "memory_consolidation" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index 71064d5ea2..79e91b2cd9 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -899,7 +899,7 @@ "type": "null" } ], - "description": "Optional analytics source classification for this thread." + "description": "Optional source classification for this thread. `side` threads do not receive multi-agent collaboration tools." }, "turns": { "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", @@ -1647,6 +1647,7 @@ "ThreadSource": { "enum": [ "user", + "side", "subagent", "memory_consolidation" ], diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/Thread.ts b/codex-rs/app-server-protocol/schema/typescript/v2/Thread.ts index 5fa30b64e3..d060ada864 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/Thread.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/Thread.ts @@ -62,7 +62,8 @@ cliVersion: string, */ source: SessionSource, /** - * Optional analytics source classification for this thread. + * Optional source classification for this thread. `side` threads do not receive multi-agent + * collaboration tools. */ threadSource: ThreadSource | null, /** diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts index c5109b2c7d..55220339a7 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts @@ -25,6 +25,7 @@ model?: string | null, modelProvider?: string | null, serviceTier?: string | nul * and subsequent turns. */ approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, ephemeral?: boolean, /** - * Optional client-supplied analytics source classification for this forked thread. + * Optional client-supplied source classification for this forked thread. Threads marked as + * `side` do not receive multi-agent collaboration tools. */ threadSource?: ThreadSource | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadSource.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadSource.ts index 8f55524801..5cd1ddb344 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadSource.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadSource.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type ThreadSource = "user" | "subagent" | "memory_consolidation"; +export type ThreadSource = "user" | "side" | "subagent" | "memory_consolidation"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts index 30509ef6cb..e17ab43a78 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts @@ -14,6 +14,7 @@ export type ThreadStartParams = {model?: string | null, modelProvider?: string | * 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, sessionStartSource?: ThreadStartSource | null, /** - * Optional client-supplied analytics source classification for this thread. + * Optional client-supplied source classification for this thread. Threads marked as `side` + * do not receive multi-agent collaboration tools. */ threadSource?: ThreadSource | null}; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs index 486e121cf7..961e13192e 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs @@ -138,7 +138,8 @@ pub struct ThreadStartParams { pub ephemeral: Option, #[ts(optional = nullable)] pub session_start_source: Option, - /// Optional client-supplied analytics source classification for this thread. + /// Optional client-supplied source classification for this thread. Threads marked as `side` + /// do not receive multi-agent collaboration tools. #[ts(optional = nullable)] pub thread_source: Option, /// Optional sticky environments for this thread. @@ -533,7 +534,8 @@ pub struct ThreadForkParams { pub developer_instructions: Option, #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub ephemeral: bool, - /// Optional client-supplied analytics source classification for this forked thread. + /// Optional client-supplied source classification for this forked thread. Threads marked as + /// `side` do not receive multi-agent collaboration tools. #[ts(optional = nullable)] pub thread_source: Option, /// When true, return only thread metadata and live fork state without diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs index 35b6183871..8cbc3d7eb9 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs @@ -66,6 +66,7 @@ impl From for CoreSessionSource { #[ts(rename_all = "snake_case", export_to = "v2/")] pub enum ThreadSource { User, + Side, Subagent, MemoryConsolidation, } @@ -74,6 +75,7 @@ impl From for ThreadSource { fn from(value: CoreThreadSource) -> Self { match value { CoreThreadSource::User => ThreadSource::User, + CoreThreadSource::Side => ThreadSource::Side, CoreThreadSource::Subagent => ThreadSource::Subagent, CoreThreadSource::MemoryConsolidation => ThreadSource::MemoryConsolidation, } @@ -84,6 +86,7 @@ impl From for CoreThreadSource { fn from(value: ThreadSource) -> Self { match value { ThreadSource::User => CoreThreadSource::User, + ThreadSource::Side => CoreThreadSource::Side, ThreadSource::Subagent => CoreThreadSource::Subagent, ThreadSource::MemoryConsolidation => CoreThreadSource::MemoryConsolidation, } @@ -132,7 +135,8 @@ pub struct Thread { pub cli_version: String, /// Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.). pub source: SessionSource, - /// Optional analytics source classification for this thread. + /// Optional source classification for this thread. `side` threads do not receive multi-agent + /// collaboration tools. pub thread_source: Option, /// Optional random unique nickname assigned to an AgentControl-spawned sub-agent. pub agent_nickname: Option, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index a06adf03e7..1881c818d1 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -132,7 +132,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"`. 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/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/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. Set `threadSource` to `side` for an isolated side conversation; side threads do not receive multi-agent collaboration tools. 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. - `thread/loaded/list` — list the thread ids currently loaded in memory. diff --git a/codex-rs/core/src/tools/spec_plan.rs b/codex-rs/core/src/tools/spec_plan.rs index fed62a0ee1..438bd64f1f 100644 --- a/codex-rs/core/src/tools/spec_plan.rs +++ b/codex-rs/core/src/tools/spec_plan.rs @@ -64,6 +64,7 @@ use codex_protocol::openai_models::ToolMode; use codex_protocol::protocol::MultiAgentVersion; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; +use codex_protocol::protocol::ThreadSource; use codex_tools::DiscoverableTool; use codex_tools::ResponsesApiNamespace; use codex_tools::ResponsesApiNamespaceTool; @@ -302,6 +303,9 @@ fn multi_agent_v2_enabled(turn_context: &TurnContext) -> bool { } fn collab_tools_enabled(turn_context: &TurnContext) -> bool { + if turn_context.thread_source == Some(ThreadSource::Side) { + return false; + } match turn_context.multi_agent_version { MultiAgentVersion::Disabled => false, MultiAgentVersion::V1 => !exceeds_thread_spawn_depth_limit( diff --git a/codex-rs/core/src/tools/spec_plan_tests.rs b/codex-rs/core/src/tools/spec_plan_tests.rs index f96f41f0f2..f33be34bcb 100644 --- a/codex-rs/core/src/tools/spec_plan_tests.rs +++ b/codex-rs/core/src/tools/spec_plan_tests.rs @@ -17,6 +17,7 @@ use codex_protocol::openai_models::ToolMode; use codex_protocol::openai_models::WebSearchToolType; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; +use codex_protocol::protocol::ThreadSource; use codex_tools::DiscoverablePluginInfo; use codex_tools::DiscoverableTool; use codex_tools::ResponsesApiNamespaceTool; @@ -1043,6 +1044,42 @@ async fn multi_agent_feature_selects_one_agent_tool_family() { ); } +#[tokio::test] +async fn side_threads_do_not_expose_multi_agent_tools() { + let v1 = probe(|turn| { + set_features(turn, &[Feature::Collab, Feature::SpawnCsv]); + set_feature(turn, Feature::MultiAgentV2, /*enabled*/ false); + turn.thread_source = Some(ThreadSource::Side); + }) + .await; + v1.assert_visible_lacks(&[MULTI_AGENT_V1_NAMESPACE, "spawn_agents_on_csv"]); + v1.assert_registered_lacks(&[ + &ToolName::namespaced(MULTI_AGENT_V1_NAMESPACE, "spawn_agent").to_string(), + &ToolName::namespaced(MULTI_AGENT_V1_NAMESPACE, "send_input").to_string(), + &ToolName::namespaced(MULTI_AGENT_V1_NAMESPACE, "resume_agent").to_string(), + &ToolName::namespaced(MULTI_AGENT_V1_NAMESPACE, "wait_agent").to_string(), + &ToolName::namespaced(MULTI_AGENT_V1_NAMESPACE, "close_agent").to_string(), + "spawn_agents_on_csv", + ]); + + let v2 = probe(|turn| { + set_features(turn, &[Feature::MultiAgentV2, Feature::SpawnCsv]); + turn.thread_source = Some(ThreadSource::Side); + }) + .await; + let v2_tools = [ + "spawn_agent", + "send_message", + "followup_task", + "wait_agent", + "interrupt_agent", + "list_agents", + "spawn_agents_on_csv", + ]; + v2.assert_visible_lacks(&v2_tools); + v2.assert_registered_lacks(&v2_tools); +} + #[tokio::test] async fn multi_agent_v2_message_schemas_are_encrypted() { let plan = probe(|turn| { diff --git a/codex-rs/core/tests/suite/tools.rs b/codex-rs/core/tests/suite/tools.rs index fd19a18c76..03c3899dc5 100644 --- a/codex-rs/core/tests/suite/tools.rs +++ b/codex-rs/core/tests/suite/tools.rs @@ -7,6 +7,7 @@ use std::time::Instant; use anyhow::Context; use anyhow::Result; +use codex_core::StartThreadOptions; use codex_core::sandboxing::SandboxPermissions; use codex_features::Feature; use codex_protocol::models::PermissionProfile; @@ -16,6 +17,11 @@ use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::EventMsg; +use codex_protocol::protocol::InitialHistory; +use codex_protocol::protocol::Op; +use codex_protocol::protocol::ThreadSource; +use codex_protocol::user_input::UserInput; use core_test_support::assert_regex_match; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -30,6 +36,8 @@ use core_test_support::skip_if_no_network; use core_test_support::skip_if_sandbox; use core_test_support::test_codex::local; use core_test_support::test_codex::test_codex; +use core_test_support::wait_for_event; +use pretty_assertions::assert_eq; use regex_lite::Regex; use serde_json::Value; use serde_json::json; @@ -51,6 +59,85 @@ fn tool_names(body: &Value) -> Vec { .unwrap_or_default() } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn side_threads_omit_multi_agent_tools_from_requests() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + let response_mock = mount_sse_once( + &server, + sse(vec![ + ev_response_created("resp-1"), + ev_assistant_message("msg-1", "done"), + ev_completed("resp-1"), + ]), + ) + .await; + let mut builder = test_codex().with_config(|config| { + config + .features + .enable(Feature::MultiAgentV2) + .expect("multi-agent v2 should enable for test"); + config + .features + .enable(Feature::SpawnCsv) + .expect("spawn CSV should enable for test"); + }); + let test = builder.build(&server).await?; + let side_thread = test + .thread_manager + .start_thread_with_options(StartThreadOptions { + config: test.config.clone(), + initial_history: InitialHistory::New, + session_source: None, + thread_source: Some(ThreadSource::Side), + dynamic_tools: Vec::new(), + metrics_service_name: None, + parent_trace: None, + environments: Vec::new(), + }) + .await?; + + side_thread + .thread + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: "which tools are available?".to_string(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + responsesapi_client_metadata: None, + additional_context: Default::default(), + thread_settings: Default::default(), + }) + .await?; + wait_for_event(&side_thread.thread, |event| { + matches!(event, EventMsg::TurnComplete(_)) + }) + .await; + + let tools = tool_names(&response_mock.single_request().body_json()); + assert!(tools.contains(&"update_plan".to_string())); + let present_multi_agent_tools = tools + .into_iter() + .filter(|tool| { + matches!( + tool.as_str(), + "spawn_agent" + | "send_message" + | "followup_task" + | "wait_agent" + | "interrupt_agent" + | "list_agents" + | "spawn_agents_on_csv" + ) + }) + .collect::>(); + assert_eq!(present_multi_agent_tools, Vec::::new()); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn empty_turn_environments_omits_environment_backed_tools() -> Result<()> { skip_if_no_network!(Ok(())); diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index b141298288..fdd405af35 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -2534,6 +2534,7 @@ pub enum SessionSource { #[ts(rename_all = "snake_case")] pub enum ThreadSource { User, + Side, Subagent, MemoryConsolidation, } @@ -2542,6 +2543,7 @@ impl ThreadSource { pub fn as_str(self) -> &'static str { match self { ThreadSource::User => "user", + ThreadSource::Side => "side", ThreadSource::Subagent => "subagent", ThreadSource::MemoryConsolidation => "memory_consolidation", } @@ -2560,6 +2562,7 @@ impl FromStr for ThreadSource { fn from_str(value: &str) -> Result { match value { "user" => Ok(ThreadSource::User), + "side" => Ok(ThreadSource::Side), "subagent" => Ok(ThreadSource::Subagent), "memory_consolidation" => Ok(ThreadSource::MemoryConsolidation), other => Err(format!("unknown thread source: {other}")), diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 63a5d4a1c0..d57a7b75ab 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -126,6 +126,7 @@ use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::ThreadLoadedListParams; use codex_app_server_protocol::ThreadMemoryMode; use codex_app_server_protocol::ThreadRollbackResponse; +use codex_app_server_protocol::ThreadSource; use codex_app_server_protocol::ThreadStartSource; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnError as AppServerTurnError; @@ -960,7 +961,7 @@ impl App { &[("source", "cli_subcommand")], ); let forked = app_server - .fork_thread(config.clone(), target_session.thread_id) + .fork_thread(config.clone(), target_session.thread_id, ThreadSource::User) .await .map_err(|err| session_start_error("fork", &target_session, err))?; let init = crate::chatwidget::ChatWidgetInit { diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index 548aa54427..4699c8f03a 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -144,7 +144,10 @@ impl App { if let Some(thread_id) = self.chat_widget.thread_id() { self.refresh_in_memory_config_from_disk_best_effort("forking the thread") .await; - match app_server.fork_thread(self.config.clone(), thread_id).await { + match app_server + .fork_thread(self.config.clone(), thread_id, ThreadSource::User) + .await + { Ok(forked) => { self.shutdown_current_thread(app_server).await; match self diff --git a/codex-rs/tui/src/app/side.rs b/codex-rs/tui/src/app/side.rs index 04831e16e4..86e51d18d6 100644 --- a/codex-rs/tui/src/app/side.rs +++ b/codex-rs/tui/src/app/side.rs @@ -564,7 +564,10 @@ impl App { .await; let fork_config = self.side_fork_config(); - match app_server.fork_thread(fork_config, parent_thread_id).await { + match app_server + .fork_thread(fork_config, parent_thread_id, ThreadSource::Side) + .await + { Ok(forked) => { let child_thread_id = forked.session.thread_id; let channel = self.ensure_thread_channel(child_thread_id); diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 32db3d2cf7..11826eef8d 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -445,6 +445,7 @@ impl AppServerSession { &mut self, config: Config, thread_id: ThreadId, + thread_source: ThreadSource, ) -> Result { let request_id = self.next_request_id(); let session_config = self.session_config_with_effective_service_tier(&config); @@ -457,6 +458,7 @@ impl AppServerSession { thread_id, self.thread_params_mode(), self.remote_cwd_override.as_deref(), + thread_source, ), }) .await @@ -1461,6 +1463,7 @@ fn thread_fork_params_from_config( thread_id: ThreadId, thread_params_mode: ThreadParamsMode, remote_cwd_override: Option<&std::path::Path>, + thread_source: ThreadSource, ) -> ThreadForkParams { let permissions = permissions_selection_from_config(&config, thread_params_mode); let sandbox = permissions @@ -1490,7 +1493,7 @@ fn thread_fork_params_from_config( config.developer_instructions.clone(), ), ephemeral: config.ephemeral, - thread_source: Some(ThreadSource::User), + thread_source: Some(thread_source), ..ThreadForkParams::default() } } @@ -2016,6 +2019,7 @@ mod tests { thread_id, ThreadParamsMode::Remote, /*remote_cwd_override*/ None, + ThreadSource::User, ); assert_eq!(start.cwd, None); @@ -2133,6 +2137,7 @@ mod tests { thread_id, ThreadParamsMode::Remote, Some(remote_cwd.as_path()), + ThreadSource::User, ); assert_eq!(start.cwd.as_deref(), Some("repo/on/server")); @@ -2184,6 +2189,7 @@ mod tests { thread_id, ThreadParamsMode::Embedded, /*remote_cwd_override*/ None, + ThreadSource::User, ); let expected_service_tier = Some(Some(ServiceTier::Fast.request_value().to_string())); @@ -2238,6 +2244,7 @@ mod tests { thread_id, ThreadParamsMode::Embedded, /*remote_cwd_override*/ None, + ThreadSource::Side, ); assert_eq!(params.base_instructions.as_deref(), Some("Base override.")); @@ -2245,6 +2252,7 @@ mod tests { params.developer_instructions.as_deref(), Some("Developer override.") ); + assert_eq!(params.thread_source, Some(ThreadSource::Side)); } #[tokio::test] @@ -2271,6 +2279,7 @@ mod tests { thread_id, ThreadParamsMode::Embedded, /*remote_cwd_override*/ None, + ThreadSource::User, ); assert_eq!(control_start.developer_instructions, None); @@ -2300,6 +2309,7 @@ mod tests { thread_id, ThreadParamsMode::Embedded, /*remote_cwd_override*/ None, + ThreadSource::User, ); let expected = format!( "Developer override.\n\n{}",