From ed1c350c7df9cef727793ca3dafed832134b40d7 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 14 Nov 2026 06:42:22 +0000 Subject: [PATCH 1/3] test(codex): delegate_task children start with the subagent thread source Failing test for #36520: Codex thread/start for a delegate_task child carries no threadSource, so the child's MCP tool calls look like a user thread's. Co-Authored-By: Claude Opus 5.5 --- .../Adapters/CodexAdapterV2.test.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index 97fb058bf092..9a45e5854f84 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -1872,6 +1872,55 @@ describe("CodexAdapterV2 post-settle continuation", () => { ), ); + // regression: #36520 MCP servers can tell a delegate_task child from a user thread + it.effect("starts delegate_task children with the subagent thread source", () => + Effect.gen(function* () { + const scenario = "delegated-thread-source"; + const preamble = codexReplayPreamble({ + nativeThreadId: "delegated-parent-thread", + nativeTurnId: "unused", + prompt: "unused", + }).slice(0, 5); + const childStart = codexReplayPreamble({ + nativeThreadId: "delegated-child-thread", + nativeTurnId: "unused", + prompt: "unused", + }) + .slice(3, 5) + .map((entry) => withReplayRequestId(entry, 3)); + const threadSources: Array = []; + const harness = yield* makeCodexReplayHarness( + makeCodexReplayTranscript({ scenario, entries: [...preamble, ...childStart] }), + () => Effect.void, + (method, params) => + Effect.sync(() => { + if (method === "thread/start" && Predicate.isObject(params)) { + threadSources.push(params["threadSource"]); + } + }), + ); + const childThreadId = (yield* IdAllocator.IdAllocatorV2).derive.delegatedTaskThread({ + commandId: CommandId.make(`mcp:${scenario}:delegate-task:child`), + }); + yield* harness.runtime.ensureThread({ + threadId: childThreadId, + modelSelection: CODEX_TEST_MODEL_SELECTION, + runtimePolicy: CODEX_TEST_RUNTIME_POLICY, + }); + assert.deepEqual(threadSources, [undefined, "subagent"]); + }).pipe( + Effect.scoped, + Effect.provide( + Layer.mergeAll( + IdAllocator.layer, + McpProviderSessions.layer, + NodeServices.layer, + NodeCrypto.layer, + ), + ), + ), + ); + // regression: #15839 launch-arg MCP servers survive the thread's t3-code server it.effect("replays historical and dotted MCP config while enforcing other thread config", () => Effect.gen(function* () { From 6bedaa6510a2e2db007a510aea4f35477c7de43d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 14 Nov 2026 06:48:42 +0000 Subject: [PATCH 2/3] fix(codex): delegate_task children start as Codex subagents Codex delegate_task children were started with thread/start and no threadSource, so their MCP tool calls carried no thread_source and MCP servers could not tell them from threads the user opened. ensureThread now sends threadSource "subagent" when the app thread id is a delegate_task child. Fixes pingdotgg/t3code#36520 Co-Authored-By: Claude Opus 5.5 --- .../Adapters/CodexAdapterV2.test.ts | 15 ++++++++++++++- .../orchestration-v2/Adapters/CodexAdapterV2.ts | 11 +++++++---- packages/provider-core/src/server/IdAllocator.ts | 7 ++++++- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index 9a45e5854f84..d4f7d48c2a5d 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -1887,7 +1887,20 @@ describe("CodexAdapterV2 post-settle continuation", () => { prompt: "unused", }) .slice(3, 5) - .map((entry) => withReplayRequestId(entry, 3)); + .map((entry) => withReplayRequestId(entry, 3)) + .map((entry) => + entry.type === "expect_outbound" && + Predicate.isObject(entry.frame) && + Predicate.isObject(entry.frame["params"]) + ? { + ...entry, + frame: { + ...entry.frame, + params: { ...entry.frame["params"], threadSource: "subagent" }, + }, + } + : entry, + ); const threadSources: Array = []; const harness = yield* makeCodexReplayHarness( makeCodexReplayTranscript({ scenario, entries: [...preamble, ...childStart] }), diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index b6673c0b29ec..95acbf13a3a8 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -7701,15 +7701,18 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( ensureInitialized.pipe( Effect.andThen(mcpSessions.read(threadInput.threadId)), Effect.flatMap((mcpSession) => - client.request( - "thread/start", - codexThreadRuntimeParams({ + client.request("thread/start", { + ...codexThreadRuntimeParams({ threadId: threadInput.threadId, mcpSession, modelSelection: threadInput.modelSelection, runtimePolicy: threadInput.runtimePolicy, }), - ), + // Codex tags the child's MCP calls with thread_source "subagent". + ...(IdAllocator.isDelegatedTaskThread(threadInput.threadId) + ? { threadSource: "subagent" } + : {}), + }), ), Effect.map((response): OrchestrationV2ProviderThread => providerThreadFromCodexThread({ diff --git a/packages/provider-core/src/server/IdAllocator.ts b/packages/provider-core/src/server/IdAllocator.ts index 02ecadba198e..b03a98cc4e86 100644 --- a/packages/provider-core/src/server/IdAllocator.ts +++ b/packages/provider-core/src/server/IdAllocator.ts @@ -271,6 +271,11 @@ const randomId = ), ); +const DELEGATED_TASK_THREAD_PREFIX = joinId("thread", "delegated-task", ""); + +export const isDelegatedTaskThread = (threadId: ThreadId): boolean => + threadId.startsWith(DELEGATED_TASK_THREAD_PREFIX); + export const derive: IdAllocatorV2Derive = { providerSession: (input) => ProviderSessionId.make( @@ -278,7 +283,7 @@ export const derive: IdAllocatorV2Derive = { ), delegatedTaskNode: (input) => NodeId.make(joinId("node", "delegated-task", input.commandId)), delegatedTaskThread: (input) => - ThreadId.make(joinId("thread", "delegated-task", input.commandId)), + ThreadId.make(`${DELEGATED_TASK_THREAD_PREFIX}${encodePart(input.commandId)}`), delegatedTaskMessage: (input) => MessageId.make(joinId("message", "delegated-task", input.commandId)), delegatedTaskTurnItem: (input) => From f8074ae16ad88ec4a48b00af5cd0c6ef3f9ac6ed Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 14 Nov 2026 07:13:38 +0000 Subject: [PATCH 3/3] test(codex): delegated child replay expects the subagent thread source The delegated_task_status Codex transcript recorded the child's thread/start without threadSource; T3 now sends "subagent" for delegate_task children. Co-Authored-By: Claude Opus 5.5 --- .../fixtures/delegated_task_status/codex_transcript.ndjson | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/delegated_task_status/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/delegated_task_status/codex_transcript.ndjson index 6ba29577937a..8f42b9cc89a8 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/delegated_task_status/codex_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/delegated_task_status/codex_transcript.ndjson @@ -11,7 +11,7 @@ {"type":"emit_inbound","label":"parent.turn/start","frame":{"id":4,"result":{"turn":{"id":"019f1000-0000-7000-8000-000000000002","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} {"type":"emit_inbound","label":"parent.thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019f1000-0000-7000-8000-000000000001","status":{"type":"active","activeFlags":[]}}}} {"type":"emit_inbound","label":"parent.turn/started","frame":{"method":"turn/started","params":{"threadId":"019f1000-0000-7000-8000-000000000001","turn":{"id":"019f1000-0000-7000-8000-000000000002","items":[],"status":"inProgress","error":null,"startedAt":1776739467,"completedAt":null,"durationMs":null}}}} -{"type":"expect_outbound","label":"child.thread/start","frame":{"id":5,"method":"thread/start","params":{"config":{"tools.update_plan.enabled":true,"mcp_optional_startup_grace_ms":10000,"features.concurrent_reasoning_summaries":true}}}} +{"type":"expect_outbound","label":"child.thread/start","frame":{"id":5,"method":"thread/start","params":{"config":{"tools.update_plan.enabled":true,"mcp_optional_startup_grace_ms":10000,"features.concurrent_reasoning_summaries":true},"threadSource":"subagent"}}} {"type":"emit_inbound","label":"child.thread/start","frame":{"id":5,"result":{"thread":{"id":"019f1000-0000-7000-8000-000000000003","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776739468,"updatedAt":1776739468,"status":{"type":"idle"},"path":"/home/replay-user/.codex/sessions/delegated-task-status-child.jsonl","cwd":"/home/replay-user/t3code","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]},"model":"gpt-5.4","modelProvider":"openai","serviceTier":"fast","cwd":"/home/replay-user/t3code","approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":[],"readOnlyAccess":{"type":"fullAccess"},"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"reasoningEffort":"xhigh"}}} {"type":"expect_outbound","label":"config/read","frame":{"id":6,"method":"config/read","params":{"cwd":"","includeLayers":false}}} {"type":"emit_inbound","label":"config/read","frame":{"id":6,"result":{"config":{},"origins":{}}}}