From a786c0605aa19c0bc04d374b1dff9284161dfd85 Mon Sep 17 00:00:00 2001 From: "(CJ) Chukwudi Nwobodo" <142016413+chuks-qua@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:34:33 +0100 Subject: [PATCH 1/2] refactor(delegation): drop the per-turn thread-control eligibility flag TurnRequest.threadControlEligible was the last piece of the wording gate. Since #2271 the server set it from the provider alone, so it never varied for a Codex session, but the Codex adapter still used it to decide whether to attach the Delegation toolkit and kept a session restart for an eligibility change that could no longer happen. Codex now attaches the Delegation toolkit to every session, as the Claude, Cursor and Copilot adapters already do. The steer runtime reads whether a turn holds the Delegation lease from the lease directive itself instead of the request flag. Part of #2218 --- .../__tests__/agent-service-gate.test.ts | 5 +- .../__tests__/turn-steer-delivery.test.ts | 2 +- .../orchestration/turn-runtime-controller.ts | 2 +- .../__tests__/native-plan-questions.test.ts | 2 +- .../steering/turn-steer-runtime-port.ts | 4 +- .../turn-admission-dispatch-coordinator.ts | 1 - .../delegation/coordinator-wake-service.ts | 2 +- .../delegation/delegated-task-service.test.ts | 2 +- .../contracts/src/providers/interfaces.ts | 2 - .../__tests__/codex/codex-native-plan.test.ts | 2 +- .../codex/codex-provider-first-turn.test.ts | 77 ++----------------- .../codex/codex-provider-lifecycle.test.ts | 1 - .../codex/codex-provider-test-fixture.ts | 8 -- .../src/private/codex/codex-provider.ts | 34 ++------ 14 files changed, 19 insertions(+), 125 deletions(-) diff --git a/apps/server/src/features/agents/orchestration/__tests__/agent-service-gate.test.ts b/apps/server/src/features/agents/orchestration/__tests__/agent-service-gate.test.ts index 0aed63b67..4bbe3fee3 100644 --- a/apps/server/src/features/agents/orchestration/__tests__/agent-service-gate.test.ts +++ b/apps/server/src/features/agents/orchestration/__tests__/agent-service-gate.test.ts @@ -401,7 +401,7 @@ describe("AgentService internal MCP provider allowlist", () => { "Call delegate_task exactly once to hand this review to Codex. Do not call any other tool.", "Do not create an Mcode thread", ])("activates thread control whatever the message says: %s", async (content) => { - const { svc, threadControlMcp, providerStub } = buildService(); + const { svc, threadControlMcp } = buildService(); await svc.sendMessage({ threadId: THREAD_ID, @@ -418,9 +418,6 @@ describe("AgentService internal MCP provider allowlist", () => { sourceThreadId: THREAD_ID, permissionMode: "supervised", })); - expect(providerStub.sendTurn).toHaveBeenCalledWith(expect.objectContaining({ - threadControlEligible: true, - })); }); it("activates full thread control for a default command on a persisted full thread", async () => { diff --git a/apps/server/src/features/agents/orchestration/__tests__/turn-steer-delivery.test.ts b/apps/server/src/features/agents/orchestration/__tests__/turn-steer-delivery.test.ts index 021a2c810..c137f7ce5 100644 --- a/apps/server/src/features/agents/orchestration/__tests__/turn-steer-delivery.test.ts +++ b/apps/server/src/features/agents/orchestration/__tests__/turn-steer-delivery.test.ts @@ -50,7 +50,7 @@ class FakeTurnRuntime implements TurnSteerRuntime { turnExecutionId: this.running.turnExecutionId, workspaceId: "workspace", providerId: this.providerId, - threadControlEligible: false, + holdsDelegationLease: false, steer: this.running.steerable ? (input) => { this.sent.push(input); return this.respond(input); } : null, }; } diff --git a/apps/server/src/features/agents/orchestration/turn-runtime-controller.ts b/apps/server/src/features/agents/orchestration/turn-runtime-controller.ts index dad3d2a23..aa2ebe37a 100644 --- a/apps/server/src/features/agents/orchestration/turn-runtime-controller.ts +++ b/apps/server/src/features/agents/orchestration/turn-runtime-controller.ts @@ -329,7 +329,7 @@ export class TurnRuntimeController implements TurnLifecycleControl, TurnRuntimeE turnExecutionId: turn.turnExecutionId, workspaceId: owned.prepared.request.workspaceId, providerId: provider.id, - threadControlEligible: owned.prepared.request.threadControlEligible === true, + holdsDelegationLease: owned.prepared.threadControl !== null, steer: isTurnSteerCapable(provider) ? (input) => provider.steerTurn({ ...turn, ...input }) : null, }; } diff --git a/apps/server/src/features/agents/planning/__tests__/native-plan-questions.test.ts b/apps/server/src/features/agents/planning/__tests__/native-plan-questions.test.ts index 1390d1643..4526efa9a 100644 --- a/apps/server/src/features/agents/planning/__tests__/native-plan-questions.test.ts +++ b/apps/server/src/features/agents/planning/__tests__/native-plan-questions.test.ts @@ -107,7 +107,7 @@ async function fixture(interactionMode: "plan" | "build" = "plan") { const request: TurnRequest<"codex"> = { threadId: thread.id, turnId: "turn", turnExecutionId: "execution", sessionId: `mcode-${thread.id}`, workspaceId: workspace.id, cwd: process.cwd(), message: "Plan a greeting", model: "gpt-6.1-sol", permissionMode: "full", approvalReviewMode: "manual", interactionMode, - providerOptions: {}, threadControlEligible: false, + providerOptions: {}, ...(interactionMode === "plan" ? { planTurn: { kind: "planning", planFilePath: null } } : {}) }; const ask = async () => { await provider.sendTurn(request); diff --git a/apps/server/src/features/agents/steering/turn-steer-runtime-port.ts b/apps/server/src/features/agents/steering/turn-steer-runtime-port.ts index c6cd68eba..ea372fbdb 100644 --- a/apps/server/src/features/agents/steering/turn-steer-runtime-port.ts +++ b/apps/server/src/features/agents/steering/turn-steer-runtime-port.ts @@ -16,8 +16,8 @@ export interface SteerableTurn { /** Where the turn's mentions resolve, as for `agent.send`. */ readonly workspaceId: string; readonly providerId: ProviderId; - /** Whether the turn holds the thread-control lease, so a message steered into it can be acted on with task tools. */ - readonly threadControlEligible: boolean; + /** Whether the turn holds the Delegation lease, so a message steered into it can be acted on with task tools. */ + readonly holdsDelegationLease: boolean; readonly steer: ((input: TurnSteerMessage) => Promise) | null; } diff --git a/apps/server/src/features/agents/turns/turn-admission-dispatch-coordinator.ts b/apps/server/src/features/agents/turns/turn-admission-dispatch-coordinator.ts index 6d5f3a48f..69930e342 100644 --- a/apps/server/src/features/agents/turns/turn-admission-dispatch-coordinator.ts +++ b/apps/server/src/features/agents/turns/turn-admission-dispatch-coordinator.ts @@ -1205,7 +1205,6 @@ export class TurnAdmissionDispatchCoordinator { attachments: attachments.persisted.length > 0 ? attachments.persisted : undefined, reasoningLevel: prepared.command.reasoningLevel, ...guardrails, - threadControlEligible: supportsInternalThreadControl(prepared.providerId), resumeFrom: this.resumeFrom(prepared, attachments), ...(prepared.command.resumeRequired ? { resumeRequired: true } : {}), providerOptions: this.providerOptions(prepared, settings), diff --git a/apps/server/src/features/thread-control/delegation/coordinator-wake-service.ts b/apps/server/src/features/thread-control/delegation/coordinator-wake-service.ts index 5bdcd4389..c1fd76f2f 100644 --- a/apps/server/src/features/thread-control/delegation/coordinator-wake-service.ts +++ b/apps/server/src/features/thread-control/delegation/coordinator-wake-service.ts @@ -110,7 +110,7 @@ export class CoordinatorWakeService { const turn = this.deps.runtime.runningTurn(coordinatorId); if (turn) { // A turn without the lease cannot call task_status for detail, so the result waits for a turn that can. - if (!turn.steer || !turn.threadControlEligible || this.noSteer.get(coordinatorId) === turn.turnExecutionId) return; + if (!turn.steer || !turn.holdsDelegationLease || this.noSteer.get(coordinatorId) === turn.turnExecutionId) return; await this.deliverSteer(coordinatorId, turn.turnExecutionId); return; } diff --git a/apps/server/src/features/thread-control/delegation/delegated-task-service.test.ts b/apps/server/src/features/thread-control/delegation/delegated-task-service.test.ts index 9cf0da96f..c281da363 100644 --- a/apps/server/src/features/thread-control/delegation/delegated-task-service.test.ts +++ b/apps/server/src/features/thread-control/delegation/delegated-task-service.test.ts @@ -257,7 +257,7 @@ async function wakeHarness(f: Fixture) { const turn = f.canonical.loadLatestTurn(threadId); if (!turn?.executionId) return null; return { turnExecutionId: turn.executionId, workspaceId: f.workspace.id, providerId: "codex", - threadControlEligible: state.leased, steer: state.steerable ? async () => state.response : null }; + holdsDelegationLease: state.leased, steer: state.steerable ? async () => state.response : null }; }; const runtime: TurnSteerRuntime = { runningTurn, recordDelivered: async (message) => { await f.messages.create(message.threadId, "user", message.content, 3, undefined, undefined, undefined, diff --git a/packages/contracts/src/providers/interfaces.ts b/packages/contracts/src/providers/interfaces.ts index 6783498ed..e790cd732 100644 --- a/packages/contracts/src/providers/interfaces.ts +++ b/packages/contracts/src/providers/interfaces.ts @@ -129,8 +129,6 @@ export interface TurnRequest

{ maxBudgetUsd?: number; /** Max agent turns for this Turn. Undefined or 0 disables. */ maxTurns?: number; - /** Whether the server grants this turn Mcode thread control. */ - threadControlEligible?: boolean; /** * SDK session id to resume from. When defined the Provider resumes that * session; when undefined it starts fresh. Replaces the previous diff --git a/packages/providers/src/__tests__/codex/codex-native-plan.test.ts b/packages/providers/src/__tests__/codex/codex-native-plan.test.ts index 55ad4d410..31e65c64b 100644 --- a/packages/providers/src/__tests__/codex/codex-native-plan.test.ts +++ b/packages/providers/src/__tests__/codex/codex-native-plan.test.ts @@ -63,7 +63,7 @@ const turn: TurnRequest<"codex"> = { turnId: "turn", turnExecutionId: "execution", sessionId: "mcode-native-plan", threadId: "native-plan", workspaceId: "fixture", cwd: process.cwd(), message: "Plan a greeting", model: "gpt-6.1-sol", reasoningLevel: "low", permissionMode: "full", approvalReviewMode: "manual", interactionMode: "plan", - providerOptions: {}, threadControlEligible: false, planTurn: { kind: "planning", planFilePath: null }, + providerOptions: {}, planTurn: { kind: "planning", planFilePath: null }, }; function fixture( diff --git a/packages/providers/src/__tests__/codex/codex-provider-first-turn.test.ts b/packages/providers/src/__tests__/codex/codex-provider-first-turn.test.ts index 31e82e1c4..a1336a319 100644 --- a/packages/providers/src/__tests__/codex/codex-provider-first-turn.test.ts +++ b/packages/providers/src/__tests__/codex/codex-provider-first-turn.test.ts @@ -496,9 +496,9 @@ describe("CodexProvider first turn on new session", () => { await provider.stopSession(sessionId); }); - it("attaches for Browser alone when the turn is not thread-control eligible", async () => { - const attach = vi.fn().mockResolvedValue(mcodeAttachment({ browser: true, threadControl: false })); - const provider = makeProvider(undefined, configuredLease(), { attach }); + it("attaches the Delegation toolkit whatever the user's wording, without a Browser grant", async () => { + const attach = vi.fn().mockResolvedValue(mcodeAttachment({ browser: false, threadControl: true })); + const provider = makeProvider(undefined, new BrowserAutomationSessionLease(), { attach }); const send = provider.sendTurn({ turnId: "test-turn", @@ -506,19 +506,18 @@ describe("CodexProvider first turn on new session", () => { sessionId, workspaceId: "workspace-test", threadId, - message: "inspect the page", + message: "Farm this out to Claude", cwd: process.cwd(), model: "gpt-5.4", interactionMode: "build", providerOptions: {}, permissionMode: "supervised", - threadControlEligible: false, }); await vi.waitFor(() => expect(appServers.at(-1)).toBeDefined()); emitMcpStartup(appServers.at(-1)!, "mcode", "ready"); await send; - expect(attach).toHaveBeenCalledExactlyOnceWith({ providerId: "codex", sessionId, threadId, threadControl: false }); + expect(attach).toHaveBeenCalledExactlyOnceWith({ providerId: "codex", sessionId, threadId, threadControl: true }); await provider.stopSession(sessionId); }); @@ -622,7 +621,6 @@ describe("CodexProvider first turn on new session", () => { interactionMode: "build", providerOptions: {}, permissionMode: "supervised", - threadControlEligible: false, }); const server = appServers.at(-1)!; @@ -2808,71 +2806,6 @@ describe("CodexProvider first turn on new session", () => { await provider.stopSession(sessionId); }); - it("omits the mcode server for an ineligible turn without a Browser lease", async () => { - const attach = vi.fn().mockResolvedValue(mcodeAttachment()); - const provider = makeProvider(undefined, new BrowserAutomationSessionLease(), { attach }); - - await provider.sendTurn({ - turnId: "test-turn", - turnExecutionId: "ineligible-execution", - sessionId: "mcode-ineligible", - workspaceId: "workspace-test", - threadId: "ineligible", - message: "Spawn a provider-native child", - cwd: process.cwd(), - model: "gpt-5.4", - interactionMode: "build", - providerOptions: {}, - permissionMode: "supervised", - threadControlEligible: false, - }); - - const server = appServers.at(-1)!; - expect(attach).not.toHaveBeenCalled(); - expect(server.options.configOverrides).toEqual([]); - expect(server.spawnedEnv).not.toHaveProperty("MCODE_MCP_TOKEN"); - expect(server.options.instructionSections?.map((section) => section.key)).not.toContain("mcode_thread_control"); - await provider.stopSession("mcode-ineligible"); - }); - - it("restarts a pooled session when Mcode thread-control eligibility changes", async () => { - const attach = vi.fn().mockResolvedValue(mcodeAttachment({ browser: false, threadControl: true })); - const provider = makeProvider(undefined, new BrowserAutomationSessionLease(), { attach }); - - const request = { - turnId: "test-turn", - turnExecutionId: "eligible-execution", - sessionId: "mcode-eligibility-transition", - workspaceId: "workspace-test", - threadId: "eligibility-transition", - message: "Create an Mcode thread named child", - cwd: process.cwd(), - model: "gpt-5.4", - interactionMode: "build" as const, - providerOptions: {}, - permissionMode: "supervised", - }; - const firstSend = provider.sendTurn({ ...request, threadControlEligible: true }); - await vi.waitFor(() => expect(appServers.at(-1)).toBeDefined()); - emitMcpStartup(appServers[0]!, "mcode", "ready"); - await firstSend; - await provider.sendTurn({ - ...request, - turnExecutionId: "ineligible-execution", - message: "Spawn a provider-native child named child", - threadControlEligible: false, - }); - - expect(appServers).toHaveLength(2); - expect(appServers[0]?.options.configOverrides).toEqual([ - 'mcp_servers.mcode.url="http://127.0.0.1:19400/mcp/mcode"', - 'mcp_servers.mcode.bearer_token_env_var="MCODE_MCP_TOKEN"', - ]); - expect(appServers[1]?.options.configOverrides).toEqual([]); - expect(attach).toHaveBeenCalledTimes(1); - await provider.stopSession("mcode-eligibility-transition"); - }); - it("restarts a pooled session and attaches again once its Browser credential expires", async () => { const attach = vi.fn().mockImplementation(async () => mcodeAttachment()); const provider = makeProvider(undefined, configuredLease(), { attach }); diff --git a/packages/providers/src/__tests__/codex/codex-provider-lifecycle.test.ts b/packages/providers/src/__tests__/codex/codex-provider-lifecycle.test.ts index 66fa8ddb5..ea3e59344 100644 --- a/packages/providers/src/__tests__/codex/codex-provider-lifecycle.test.ts +++ b/packages/providers/src/__tests__/codex/codex-provider-lifecycle.test.ts @@ -77,7 +77,6 @@ const request: TurnRequest<"codex"> = { turnId: "turn-1", turnExecutionId: "execution-1", sessionId: "mcode-lifecycle", threadId: "lifecycle", workspaceId: "fixture", cwd: process.cwd(), message: "hello", model: "gpt-5.4", permissionMode: "supervised", approvalReviewMode: "manual", interactionMode: "build", providerOptions: {}, - threadControlEligible: false, }; function createProvider() { diff --git a/packages/providers/src/__tests__/codex/codex-provider-test-fixture.ts b/packages/providers/src/__tests__/codex/codex-provider-test-fixture.ts index 8be540fa3..4929d17ed 100644 --- a/packages/providers/src/__tests__/codex/codex-provider-test-fixture.ts +++ b/packages/providers/src/__tests__/codex/codex-provider-test-fixture.ts @@ -10,7 +10,6 @@ import type { ProviderMcodeAttachment, ProviderMcodeAttachRequest, } from "../../host-ports.js"; -import type { TurnRequest } from "@mcode/contracts"; interface TestBrowserScope extends ProviderBrowserLeaseRequest {} @@ -216,11 +215,4 @@ export class CodexProvider extends PackageCodexProvider { this.settingsService = delegatedSettingsService; } - /** Preserve legacy fixture behavior unless a test explicitly exercises the gate. */ - override sendTurn(request: TurnRequest<"codex">): Promise { - return super.sendTurn({ - ...request, - threadControlEligible: request.threadControlEligible ?? true, - }); - } } diff --git a/packages/providers/src/private/codex/codex-provider.ts b/packages/providers/src/private/codex/codex-provider.ts index ed8e3def4..f954c7005 100644 --- a/packages/providers/src/private/codex/codex-provider.ts +++ b/packages/providers/src/private/codex/codex-provider.ts @@ -320,8 +320,6 @@ interface CodexSessionState { workspaceId: string; /** Browser permission class fixed to the provider process at spawn. */ browserPermissionCapability: "observe" | "interact" | "privileged"; - /** Whether this session was started with the Mcode thread-control toolkit. */ - threadControlEligible: boolean; } /** One pending codex approval bridged into the Phase 1 permission flow. */ @@ -349,7 +347,6 @@ interface PreparedCodexTurn { browserPermissionCapability: "observe" | "interact" | "privileged"; input: CodexTurnInput; turnOptions: CodexTurnOptions; - threadControlEligible: boolean; } interface CodexMcodeSetup { @@ -366,9 +363,8 @@ interface CodexSpawnContext { resumeFrom: string | undefined; sandbox: SandboxMode; approvalPolicy: "never" | "on-request"; - pendingSpawn: { input: CodexTurnInput; turnOptions: CodexTurnOptions; turnExecutionId: string; threadControlEligible: boolean; resumeRequired: boolean } | undefined; + pendingSpawn: { input: CodexTurnInput; turnOptions: CodexTurnOptions; turnExecutionId: string; resumeRequired: boolean } | undefined; stagedExecutionId: string | undefined; - threadControlEligible: boolean; browserAccess: { stage: BrowserAutomationSessionLeaseStage; workspaceId: string; permissionCapability: "observe" | "interact" | "privileged" } | undefined; mcode: ProviderMcodeAttachment | null; mcodeSetupError: string | undefined; @@ -741,7 +737,6 @@ export class CodexProvider extends NodeEvents.EventEmitter implements IAgentProv turnExecutionId: string; turnId: string; deliveryAttempt: number; - threadControlEligible: boolean; resumeRequired: boolean; signal: AbortSignal; } @@ -1182,7 +1177,6 @@ export class CodexProvider extends NodeEvents.EventEmitter implements IAgentProv sandbox: request.permissionMode === "full" ? "danger-full-access" : "workspace-write", browserPermissionCapability: providerBrowserPermissionCapability(request.permissionMode, request.interactionMode), turnOptions: this.codexTurnOptions(request, settings.fastMode, nativePlan), - threadControlEligible: request.threadControlEligible === true, }; } @@ -1255,7 +1249,6 @@ export class CodexProvider extends NodeEvents.EventEmitter implements IAgentProv return undefined; } if (existing && existing.server.isAlive && existing.sandboxMode !== turn.sandbox) this.restartCodexSessionForSandbox(existing, turn); - if (this.requiresThreadControlSessionRestart(existing, turn)) return this.restartCodexSessionForThreadControl(existing, turn); return existing; } @@ -1273,18 +1266,6 @@ export class CodexProvider extends NodeEvents.EventEmitter implements IAgentProv }); } - private requiresThreadControlSessionRestart(existing: CodexSessionState | undefined, turn: PreparedCodexTurn): existing is CodexSessionState { - return Boolean(existing && existing.server.isAlive && existing.threadControlEligible !== turn.threadControlEligible); - } - - private async restartCodexSessionForThreadControl(existing: CodexSessionState, turn: PreparedCodexTurn): Promise { - const sessionId = turn.request.sessionId; - logger.info("Codex session restarted due to Mcode thread-control eligibility change", { sessionId, from: existing.threadControlEligible, to: turn.threadControlEligible }); - this.drainPending((entry) => entry.sessionId === sessionId); - await this.runtime.stop(sessionId); - return undefined; - } - private isReusableCodexSession(existing: CodexSessionState | undefined, sandbox: string): existing is CodexSessionState { return Boolean(existing && existing.server.isAlive && existing.sandboxMode === sandbox); } @@ -1301,7 +1282,7 @@ export class CodexProvider extends NodeEvents.EventEmitter implements IAgentProv private stageCodexTurn(turn: PreparedCodexTurn): void { const { request } = turn; const sessionId = request.sessionId; - this.pendingSpawnTurns.set(sessionId, { input: turn.input, turnOptions: turn.turnOptions, turnExecutionId: request.turnExecutionId, turnId: request.turnId, deliveryAttempt: request.deliveryAttempt ?? 1, threadControlEligible: turn.threadControlEligible, resumeRequired: request.resumeRequired === true, signal: turn.signal }); + this.pendingSpawnTurns.set(sessionId, { input: turn.input, turnOptions: turn.turnOptions, turnExecutionId: request.turnExecutionId, turnId: request.turnId, deliveryAttempt: request.deliveryAttempt ?? 1, resumeRequired: request.resumeRequired === true, signal: turn.signal }); if (!this.host.browser.isConfigured()) return; const stage = this.host.browser.stage({ providerId: this.id, providerSessionId: request.resumeFrom ?? sessionId, mcodeSessionId: sessionId, threadId: request.threadId, workspaceId: request.workspaceId, permissionCapability: turn.browserPermissionCapability }); const previous = this.pendingBrowserAccess.get(sessionId); @@ -1472,12 +1453,11 @@ export class CodexProvider extends NodeEvents.EventEmitter implements IAgentProv private async prepareCodexSpawn(args: SpawnArgs): Promise { const settings = await this.codexPorts.settings.get(); const pendingSpawn = this.pendingSpawnTurns.get(args.sessionId); - const threadControlEligible = pendingSpawn?.threadControlEligible === true; const browserAccess = this.pendingBrowserAccess.get(args.sessionId); const issuedGrant = browserAccess ? this.host.browser.issue(browserAccess.stage) : null; this.pendingBrowserAccess.delete(args.sessionId); const closeMcode = this.codexMcodeSessionCloser(args.sessionId); - const mcodeSetup = await this.attachCodexMcode(args, threadControlEligible, issuedGrant !== null, closeMcode); + const mcodeSetup = await this.attachCodexMcode(args, closeMcode); const browserGrant = this.releaseUnlistedBrowserGrant(issuedGrant, mcodeSetup.mcode); const spawnEnv = { ...args.env }; if (mcodeSetup.mcode) spawnEnv[CODEX_MCODE_TOKEN_ENV] = mcodeSetup.mcode.token; @@ -1492,7 +1472,6 @@ export class CodexProvider extends NodeEvents.EventEmitter implements IAgentProv approvalPolicy: args.permissionMode === "full" ? "never" : "on-request", pendingSpawn, stagedExecutionId: pendingSpawn?.turnExecutionId, - threadControlEligible, browserAccess, mcode: mcodeSetup.mcode, mcodeSetupError: mcodeSetup.setupError, @@ -1509,13 +1488,10 @@ export class CodexProvider extends NodeEvents.EventEmitter implements IAgentProv */ private async attachCodexMcode( args: SpawnArgs, - threadControlEligible: boolean, - hasBrowserGrant: boolean, close: () => Promise, ): Promise { - if (!threadControlEligible && !hasBrowserGrant) return { mcode: null, setupError: undefined }; try { - const mcode = await this.host.mcode.attach({ providerId: this.id, sessionId: args.sessionId, threadId: args.threadId, threadControl: threadControlEligible }); + const mcode = await this.host.mcode.attach({ providerId: this.id, sessionId: args.sessionId, threadId: args.threadId, threadControl: true }); if (!mcode) logger.warn("Codex mcode MCP server is unavailable; continuing without Mcode tools", { threadId: args.threadId }); return { mcode, setupError: undefined }; } catch (error) { @@ -1863,7 +1839,7 @@ export class CodexProvider extends NodeEvents.EventEmitter implements IAgentProv nextChildGeneration: 0, pendingChildEvents: [], deliveredChildEventKeys: new Set(), workspaceId: context.browserAccess?.workspaceId ?? "unknown-workspace", browserPermissionCapability: context.browserAccess?.permissionCapability ?? "interact", - threadControlEligible: context.threadControlEligible, childMetadataFetches: new Set(), + childMetadataFetches: new Set(), }; if (context.browserGrant) { state.browserCredential = { credentialId: context.browserGrant.credentialId, expiresAt: context.browserGrant.expiresAt }; From ec7337d7d2242b9dd44a069bafab14eefeb00a2b Mon Sep 17 00:00:00 2001 From: "(CJ) Chukwudi Nwobodo" <142016413+chuks-qua@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:35:52 +0100 Subject: [PATCH 2/2] docs(providers): describe the Delegation toolkit attach flag without per-turn eligibility Part of #2218 --- .../src/__tests__/codex/codex-provider-test-fixture.ts | 1 - packages/providers/src/host-ports.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/providers/src/__tests__/codex/codex-provider-test-fixture.ts b/packages/providers/src/__tests__/codex/codex-provider-test-fixture.ts index 4929d17ed..817a217f3 100644 --- a/packages/providers/src/__tests__/codex/codex-provider-test-fixture.ts +++ b/packages/providers/src/__tests__/codex/codex-provider-test-fixture.ts @@ -214,5 +214,4 @@ export class CodexProvider extends PackageCodexProvider { super(host, codexPorts, 10 * 60 * 1_000); this.settingsService = delegatedSettingsService; } - } diff --git a/packages/providers/src/host-ports.ts b/packages/providers/src/host-ports.ts index 4fc054796..c0ebe52f2 100644 --- a/packages/providers/src/host-ports.ts +++ b/packages/providers/src/host-ports.ts @@ -92,7 +92,7 @@ export interface ProviderMcodeAttachRequest { /** Must equal the Browser lease `mcodeSessionId` for this session. */ sessionId: string; threadId: string; - /** Whether the thread-control toolkit is listed; Codex passes its per-turn eligibility. */ + /** Whether the Delegation toolkit is listed; every adapter that supports it passes true. */ threadControl: boolean; }