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.modeId = "auto";
|
||||||
this.pending = [];
|
this.pending = [];
|
||||||
this.featureState = [
|
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 }),
|
createFeature({ id: "plan_mode", label: "Plan", value: false }),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -3765,7 +3765,7 @@ describe("AgentManager", () => {
|
|||||||
const updated = manager.getAgent(snapshot.id);
|
const updated = manager.getAgent(snapshot.id);
|
||||||
expect(updated?.pendingPermissions.size).toBe(0);
|
expect(updated?.pendingPermissions.size).toBe(0);
|
||||||
expect(updated?.features).toEqual([
|
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 }),
|
createFeature({ id: "plan_mode", label: "Plan", value: false }),
|
||||||
]);
|
]);
|
||||||
expect(updated?.runtimeInfo).toMatchObject({
|
expect(updated?.runtimeInfo).toMatchObject({
|
||||||
@@ -3775,7 +3775,7 @@ describe("AgentManager", () => {
|
|||||||
|
|
||||||
const persisted = await storage.get(snapshot.id);
|
const persisted = await storage.get(snapshot.id);
|
||||||
expect(persisted?.features).toEqual([
|
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 }),
|
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({
|
const session = createSession({
|
||||||
featureValues: { plan_mode: true, fast_mode: true },
|
featureValues: { plan_mode: true, fast_mode: true },
|
||||||
});
|
});
|
||||||
@@ -731,11 +731,11 @@ describe("Codex app-server provider", () => {
|
|||||||
selectedActionId: "implement",
|
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).planModeEnabled).toBe(false);
|
||||||
expect((session as any).config.featureValues).toEqual({
|
expect((session as any).config.featureValues).toEqual({
|
||||||
plan_mode: false,
|
plan_mode: false,
|
||||||
fast_mode: false,
|
fast_mode: true,
|
||||||
});
|
});
|
||||||
// The session returns the follow-up prompt instead of calling startTurn directly.
|
// The session returns the follow-up prompt instead of calling startTurn directly.
|
||||||
// The caller (session/agent-manager) is responsible for sending it through streamAgent.
|
// 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
|
* and returning the implementation prompt. The caller is responsible for
|
||||||
* starting the turn through the normal streamAgent path.
|
* starting the turn through the normal streamAgent path.
|
||||||
*/
|
*/
|
||||||
@@ -2730,7 +2730,6 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
typeof params.planText === "string" ? normalizePlanMarkdown(params.planText) : "";
|
typeof params.planText === "string" ? normalizePlanMarkdown(params.planText) : "";
|
||||||
|
|
||||||
this.applyFeatureValue("plan_mode", false);
|
this.applyFeatureValue("plan_mode", false);
|
||||||
this.applyFeatureValue("fast_mode", false);
|
|
||||||
|
|
||||||
return buildCodexPlanImplementationPrompt(planText);
|
return buildCodexPlanImplementationPrompt(planText);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user