[codex] improve Codex plan mode compatibility (#538)

* fix: improve codex plan mode compatibility

* fix: tighten codex collaboration retry detection

---------

Co-authored-by: weishu <twsxtd@gmail.com>
This commit is contained in:
EthanWang
2026-05-15 22:46:35 +08:00
committed by GitHub
co-authored by weishu
parent be0a41172a
commit f1accabb29
3 changed files with 38 additions and 14 deletions
+4 -1
View File
@@ -53,7 +53,10 @@ export interface CollaborationModeListItem {
} }
export interface CollaborationModeListResponse { export interface CollaborationModeListResponse {
data?: CollaborationModeListItem[]; data?: Array<CollaborationModeListItem | string>;
modes?: Array<CollaborationModeListItem | string>;
collaborationModes?: Array<CollaborationModeListItem | string>;
items?: Array<CollaborationModeListItem | string>;
[key: string]: unknown; [key: string]: unknown;
} }
+22 -4
View File
@@ -969,6 +969,23 @@ describe('codexRemoteLauncher', () => {
}); });
}); });
it('recognizes alternate collaboration mode list envelopes', async () => {
harness.collaborationModeResponse = { collaborationModes: [{ id: 'plan' }] };
const { session } = createSessionStub(['plan this'], {
permissionMode: 'default',
collaborationMode: 'plan',
model: 'gpt-5.4'
});
const exitReason = await codexRemoteLauncher(session as never);
expect(exitReason).toBe('exit');
expect(harness.startTurnParams).toHaveLength(1);
expect(harness.startTurnParams[0]?.collaborationMode).toMatchObject({
mode: 'plan'
});
});
it('retries plan turns without collaborationMode when the runtime rejects the field', async () => { it('retries plan turns without collaborationMode when the runtime rejects the field', async () => {
harness.startTurnErrors.push(new Error('unknown field collaborationMode')); harness.startTurnErrors.push(new Error('unknown field collaborationMode'));
const { session, sessionEvents } = createSessionStub(['plan this'], { const { session, sessionEvents } = createSessionStub(['plan this'], {
@@ -1036,7 +1053,7 @@ describe('codexRemoteLauncher', () => {
}); });
}); });
it('falls back to a normal turn when collaborationMode/list omits plan', async () => { it('still attempts plan mode when collaborationMode/list omits plan', async () => {
harness.collaborationModeResponse = { data: [{ mode: 'default' }] }; harness.collaborationModeResponse = { data: [{ mode: 'default' }] };
const { session, sessionEvents } = createSessionStub(['plan this'], { const { session, sessionEvents } = createSessionStub(['plan this'], {
permissionMode: 'default', permissionMode: 'default',
@@ -1048,9 +1065,10 @@ describe('codexRemoteLauncher', () => {
expect(exitReason).toBe('exit'); expect(exitReason).toBe('exit');
expect(harness.startTurnParams).toHaveLength(1); expect(harness.startTurnParams).toHaveLength(1);
expect(harness.startTurnParams[0]?.collaborationMode).toBeUndefined(); expect(harness.startTurnParams[0]?.collaborationMode).toMatchObject({
expect(harness.startTurnParams[0]?.model).toBe('gpt-5.4'); mode: 'plan'
expect(sessionEvents).toContainEqual({ });
expect(sessionEvents).not.toContainEqual({
type: 'message', type: 'message',
message: 'Plan mode is not supported by this Codex runtime. Sent as a normal turn instead.' message: 'Plan mode is not supported by this Codex runtime. Sent as a normal turn instead.'
}); });
+12 -9
View File
@@ -266,9 +266,9 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
return false; return false;
} }
return message.includes('requires experimentalapi') return message.includes('experimentalapi')
|| message.includes('unknown field')
|| message.includes('unsupported') || message.includes('unsupported')
|| message.includes('unknown')
|| message.includes('unrecognized') || message.includes('unrecognized')
|| message.includes('unexpected') || message.includes('unexpected')
|| message.includes('invalid field'); || message.includes('invalid field');
@@ -278,7 +278,10 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
const record = asRecord(response); const record = asRecord(response);
const candidates = [ const candidates = [
Array.isArray(response) ? response : undefined, Array.isArray(response) ? response : undefined,
Array.isArray(record?.data) ? record.data : undefined Array.isArray(record?.data) ? record.data : undefined,
Array.isArray(record?.modes) ? record.modes : undefined,
Array.isArray(record?.collaborationModes) ? record.collaborationModes : undefined,
Array.isArray(record?.items) ? record.items : undefined
]; ];
for (const candidate of candidates) { for (const candidate of candidates) {
@@ -288,7 +291,9 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
return true; return true;
} }
const entryRecord = asRecord(entry); const entryRecord = asRecord(entry);
const mode = asString(entryRecord?.mode) ?? asString(entryRecord?.name); const mode = asString(entryRecord?.mode)
?? asString(entryRecord?.name)
?? asString(entryRecord?.id);
if (mode === 'plan') { if (mode === 'plan') {
return true; return true;
} }
@@ -2275,7 +2280,6 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
} }
}); });
let supportsTurnCollaborationMode = true; let supportsTurnCollaborationMode = true;
let supportsPlanCollaborationMode = true;
let supportsGoals = true; let supportsGoals = true;
try { try {
await appServerClient.setExperimentalFeatureEnablement({ enablement: { goals: true } }); await appServerClient.setExperimentalFeatureEnablement({ enablement: { goals: true } });
@@ -2289,7 +2293,7 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
const hasPlanMode = responseContainsPlanCollaborationMode(response); const hasPlanMode = responseContainsPlanCollaborationMode(response);
logger.debug(`[Codex] collaborationMode/list plan=${hasPlanMode}`); logger.debug(`[Codex] collaborationMode/list plan=${hasPlanMode}`);
if (!hasPlanMode) { if (!hasPlanMode) {
supportsPlanCollaborationMode = false; logger.debug('[Codex] collaborationMode/list did not report plan; will still attempt collaborationMode until rejected');
} }
} catch (error) { } catch (error) {
logger.debug(`[Codex] collaborationMode/list failed: ${errorMessage(error)}`); logger.debug(`[Codex] collaborationMode/list failed: ${errorMessage(error)}`);
@@ -2735,8 +2739,7 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
model: session.getModel() ?? message.mode.model model: session.getModel() ?? message.mode.model
}; };
const shouldSendCollaborationMode = supportsTurnCollaborationMode const shouldSendCollaborationMode = supportsTurnCollaborationMode
&& Boolean(mode.collaborationMode) && Boolean(mode.collaborationMode);
&& (mode.collaborationMode !== 'plan' || supportsPlanCollaborationMode);
const buildParams = (suppressCollaborationMode: boolean) => buildTurnStartParams({ const buildParams = (suppressCollaborationMode: boolean) => buildTurnStartParams({
threadId: this.currentThreadId!, threadId: this.currentThreadId!,
message: message.message, message: message.message,
@@ -2749,7 +2752,7 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
}); });
if ( if (
mode.collaborationMode === 'plan' mode.collaborationMode === 'plan'
&& (!supportsTurnCollaborationMode || !supportsPlanCollaborationMode) && !supportsTurnCollaborationMode
) { ) {
session.sendSessionEvent({ session.sendSessionEvent({
type: 'message', type: 'message',