Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TurnSteerOutcome>) | null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 0 additions & 2 deletions packages/contracts/src/providers/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,6 @@ export interface TurnRequest<P extends ProviderId = ProviderId> {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -496,29 +496,28 @@ 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",
turnExecutionId: "test-execution",
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);
});

Expand Down Expand Up @@ -622,7 +621,6 @@ describe("CodexProvider first turn on new session", () => {
interactionMode: "build",
providerOptions: {},
permissionMode: "supervised",
threadControlEligible: false,
});

const server = appServers.at(-1)!;
Expand Down Expand Up @@ -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 });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import type {
ProviderMcodeAttachment,
ProviderMcodeAttachRequest,
} from "../../host-ports.js";
import type { TurnRequest } from "@mcode/contracts";

interface TestBrowserScope extends ProviderBrowserLeaseRequest {}

Expand Down Expand Up @@ -215,12 +214,4 @@ export class CodexProvider extends PackageCodexProvider {
super(host, codexPorts, 10 * 60 * 1_000);
this.settingsService = delegatedSettingsService;
}

/** Preserve legacy fixture behavior unless a test explicitly exercises the gate. */
override sendTurn(request: TurnRequest<"codex">): Promise<void> {
return super.sendTurn({
...request,
threadControlEligible: request.threadControlEligible ?? true,
});
}
}
2 changes: 1 addition & 1 deletion packages/providers/src/host-ports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Loading
Loading