fix(server): preserve Codex fast mode after plan approval
Codex was clearing fast mode after approving a plan. This keeps fast mode and plan mode independent. Approving a plan still exits plan collaboration mode, and the follow-up implementation turn now keeps serviceTier=fast when fast mode was already enabled. Verified locally: - node node_modules/vitest/vitest.mjs run packages/server/src/server/agent/providers/codex-app-server-agent.test.ts --bail=1 - node node_modules/vitest/vitest.mjs run packages/server/src/server/agent/agent-manager.test.ts --bail=1 - npm run typecheck
This commit is contained in:
@@ -3719,7 +3719,7 @@ describe("AgentManager", () => {
|
||||
this.modeId = "auto";
|
||||
this.pending = [];
|
||||
this.featureState = [
|
||||
createFeature({ id: "fast_mode", label: "Fast", value: false }),
|
||||
createFeature({ id: "fast_mode", label: "Fast", value: true }),
|
||||
createFeature({ id: "plan_mode", label: "Plan", value: false }),
|
||||
];
|
||||
}
|
||||
@@ -3765,7 +3765,7 @@ describe("AgentManager", () => {
|
||||
const updated = manager.getAgent(snapshot.id);
|
||||
expect(updated?.pendingPermissions.size).toBe(0);
|
||||
expect(updated?.features).toEqual([
|
||||
createFeature({ id: "fast_mode", label: "Fast", value: false }),
|
||||
createFeature({ id: "fast_mode", label: "Fast", value: true }),
|
||||
createFeature({ id: "plan_mode", label: "Plan", value: false }),
|
||||
]);
|
||||
expect(updated?.runtimeInfo).toMatchObject({
|
||||
@@ -3775,7 +3775,7 @@ describe("AgentManager", () => {
|
||||
|
||||
const persisted = await storage.get(snapshot.id);
|
||||
expect(persisted?.features).toEqual([
|
||||
createFeature({ id: "fast_mode", label: "Fast", value: false }),
|
||||
createFeature({ id: "fast_mode", label: "Fast", value: true }),
|
||||
createFeature({ id: "plan_mode", label: "Plan", value: false }),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -700,7 +700,7 @@ describe("Codex app-server provider", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("approving a synthetic Codex plan permission disables plan and fast mode and returns follow-up prompt", async () => {
|
||||
test("approving a synthetic Codex plan permission disables plan mode, preserves fast mode, and returns follow-up prompt", async () => {
|
||||
const session = createSession({
|
||||
featureValues: { plan_mode: true, fast_mode: true },
|
||||
});
|
||||
@@ -731,11 +731,11 @@ describe("Codex app-server provider", () => {
|
||||
selectedActionId: "implement",
|
||||
});
|
||||
|
||||
expect((session as any).serviceTier).toBeNull();
|
||||
expect((session as any).serviceTier).toBe("fast");
|
||||
expect((session as any).planModeEnabled).toBe(false);
|
||||
expect((session as any).config.featureValues).toEqual({
|
||||
plan_mode: false,
|
||||
fast_mode: false,
|
||||
fast_mode: true,
|
||||
});
|
||||
// The session returns the follow-up prompt instead of calling startTurn directly.
|
||||
// The caller (session/agent-manager) is responsible for sending it through streamAgent.
|
||||
@@ -753,4 +753,117 @@ describe("Codex app-server provider", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("approving a synthetic Codex plan permission keeps fast mode disabled when it started disabled", async () => {
|
||||
const session = createSession({
|
||||
featureValues: { plan_mode: true, fast_mode: false },
|
||||
});
|
||||
const events: AgentStreamEvent[] = [];
|
||||
session.subscribe((event) => events.push(event));
|
||||
|
||||
(session as any).handleNotification("turn/started", {
|
||||
turn: { id: "turn-plan-3" },
|
||||
});
|
||||
(session as any).handleNotification("turn/plan/updated", {
|
||||
plan: [{ step: "Implement the safe flow", status: "pending" }],
|
||||
});
|
||||
(session as any).handleNotification("turn/completed", {
|
||||
turn: { status: "completed", error: null },
|
||||
});
|
||||
|
||||
const request = events.find(
|
||||
(event): event is Extract<AgentStreamEvent, { type: "permission_requested" }> =>
|
||||
event.type === "permission_requested" && event.request.kind === "plan",
|
||||
);
|
||||
expect(request).toBeDefined();
|
||||
if (!request) {
|
||||
throw new Error("Expected synthetic plan approval permission");
|
||||
}
|
||||
|
||||
const result = await session.respondToPermission(request.request.id, {
|
||||
behavior: "allow",
|
||||
selectedActionId: "implement",
|
||||
});
|
||||
|
||||
expect((session as any).serviceTier).toBeNull();
|
||||
expect((session as any).planModeEnabled).toBe(false);
|
||||
expect((session as any).config.featureValues).toEqual({
|
||||
plan_mode: false,
|
||||
fast_mode: false,
|
||||
});
|
||||
expect(result?.followUpPrompt).toEqual(
|
||||
expect.stringContaining("The user approved the plan. Implement it now."),
|
||||
);
|
||||
});
|
||||
|
||||
test("follow-up implementation turn keeps fast service tier and switches back to code collaboration mode", async () => {
|
||||
const session = createSession({
|
||||
featureValues: { plan_mode: true, fast_mode: true },
|
||||
});
|
||||
(session as any).collaborationModes = [
|
||||
{
|
||||
name: "Code",
|
||||
mode: "code",
|
||||
developer_instructions: "Built-in code mode",
|
||||
},
|
||||
{
|
||||
name: "Plan",
|
||||
mode: "plan",
|
||||
developer_instructions: "Built-in plan mode",
|
||||
},
|
||||
];
|
||||
(session as any).refreshResolvedCollaborationMode();
|
||||
const request = vi.fn(async (method: string) => {
|
||||
if (method === "thread/loaded/list") {
|
||||
return { data: ["test-thread"] };
|
||||
}
|
||||
if (method === "turn/start") {
|
||||
return {};
|
||||
}
|
||||
throw new Error(`Unexpected request: ${method}`);
|
||||
});
|
||||
|
||||
session.activeForegroundTurnId = null;
|
||||
session.client = { request } as any;
|
||||
|
||||
const events: AgentStreamEvent[] = [];
|
||||
session.subscribe((event) => events.push(event));
|
||||
|
||||
(session as any).handleNotification("turn/started", {
|
||||
turn: { id: "turn-plan-4" },
|
||||
});
|
||||
(session as any).handleNotification("turn/plan/updated", {
|
||||
plan: [{ step: "Implement the fast flow", status: "pending" }],
|
||||
});
|
||||
(session as any).handleNotification("turn/completed", {
|
||||
turn: { status: "completed", error: null },
|
||||
});
|
||||
|
||||
const permissionRequest = events.find(
|
||||
(event): event is Extract<AgentStreamEvent, { type: "permission_requested" }> =>
|
||||
event.type === "permission_requested" && event.request.kind === "plan",
|
||||
);
|
||||
expect(permissionRequest).toBeDefined();
|
||||
if (!permissionRequest) {
|
||||
throw new Error("Expected synthetic plan approval permission");
|
||||
}
|
||||
|
||||
const result = await session.respondToPermission(permissionRequest.request.id, {
|
||||
behavior: "allow",
|
||||
selectedActionId: "implement",
|
||||
});
|
||||
expect(result?.followUpPrompt).toEqual(expect.any(String));
|
||||
|
||||
await session.startTurn(result!.followUpPrompt!);
|
||||
|
||||
const turnStartCall = request.mock.calls.find(([method]) => method === "turn/start");
|
||||
expect(turnStartCall?.[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
serviceTier: "fast",
|
||||
collaborationMode: expect.objectContaining({
|
||||
mode: "code",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2721,7 +2721,7 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the session for plan implementation by disabling plan/fast mode
|
||||
* Prepare the session for plan implementation by disabling plan mode
|
||||
* and returning the implementation prompt. The caller is responsible for
|
||||
* starting the turn through the normal streamAgent path.
|
||||
*/
|
||||
@@ -2730,7 +2730,6 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
typeof params.planText === "string" ? normalizePlanMarkdown(params.planText) : "";
|
||||
|
||||
this.applyFeatureValue("plan_mode", false);
|
||||
this.applyFeatureValue("fast_mode", false);
|
||||
|
||||
return buildCodexPlanImplementationPrompt(planText);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user