mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(codex): preserve reasoning effort across mode switches
This commit is contained in:
@@ -80,6 +80,8 @@ function createSessionStub(
|
||||
let localLaunchFailure: { message: string; exitReason: 'switch' | 'exit' } | null = null;
|
||||
let sessionId: string | null = null;
|
||||
let transcriptPath: string | null = initialTranscriptPath;
|
||||
let modelReasoningEffort: string | null = null;
|
||||
const modelReasoningEffortUpdates: Array<string | null> = [];
|
||||
const transcriptPathCallbacks: Array<(path: string) => void> = [];
|
||||
|
||||
return {
|
||||
@@ -102,7 +104,11 @@ function createSessionStub(
|
||||
}
|
||||
},
|
||||
getPermissionMode: () => permissionMode,
|
||||
getModelReasoningEffort: () => null,
|
||||
getModelReasoningEffort: () => modelReasoningEffort,
|
||||
setModelReasoningEffort: (effort: string | null) => {
|
||||
modelReasoningEffort = effort;
|
||||
modelReasoningEffortUpdates.push(effort);
|
||||
},
|
||||
onSessionFound: (value: string) => {
|
||||
sessionId = value;
|
||||
},
|
||||
@@ -145,7 +151,9 @@ function createSessionStub(
|
||||
userMessages,
|
||||
agentMessages,
|
||||
getUserActivityCount: () => userActivityCount,
|
||||
getLocalLaunchFailure: () => localLaunchFailure
|
||||
getLocalLaunchFailure: () => localLaunchFailure,
|
||||
getModelReasoningEffort: () => modelReasoningEffort,
|
||||
getModelReasoningEffortUpdates: () => modelReasoningEffortUpdates
|
||||
};
|
||||
}
|
||||
|
||||
@@ -361,6 +369,44 @@ describe('codexLocalLauncher', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('tracks explicit and default reasoning effort from local turn context', async () => {
|
||||
const transcriptPath = await writeTranscriptMeta('codex-turn-context.jsonl', 'codex-thread-effort');
|
||||
const { session, getModelReasoningEffort, getModelReasoningEffortUpdates } = createSessionStub('default');
|
||||
let releaseRunBarrier: (() => void) | undefined;
|
||||
harness.runBarrier = new Promise((resolve) => {
|
||||
releaseRunBarrier = resolve;
|
||||
});
|
||||
|
||||
const launcherPromise = codexLocalLauncher(session as never);
|
||||
await wait(50);
|
||||
harness.sessionHookHandlers[0]?.('codex-thread-effort', {
|
||||
transcript_path: transcriptPath
|
||||
});
|
||||
await wait(100);
|
||||
|
||||
await appendFile(transcriptPath, [
|
||||
JSON.stringify({
|
||||
type: 'turn_context',
|
||||
payload: { effort: 'max' }
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'event_msg',
|
||||
payload: { type: 'token_count', info: {} }
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'turn_context',
|
||||
payload: { model: 'gpt-5.4' }
|
||||
})
|
||||
].join('\n') + '\n');
|
||||
await wait(700);
|
||||
|
||||
releaseRunBarrier?.();
|
||||
await launcherPromise;
|
||||
|
||||
expect(getModelReasoningEffortUpdates()).toEqual(['max', null]);
|
||||
expect(getModelReasoningEffort()).toBeNull();
|
||||
});
|
||||
|
||||
it('renders nested Code Mode plans and commands without their covered exec wrapper', async () => {
|
||||
const transcriptPath = join(tempDir, 'codex-hook-transcript.jsonl');
|
||||
const { session, agentMessages } = createSessionStub('default');
|
||||
|
||||
@@ -5,7 +5,7 @@ import { codexLocal } from './codexLocal';
|
||||
import type { ReasoningEffort } from './appServerTypes';
|
||||
import { CodexSession } from './session';
|
||||
import { createCodexSessionScanner, type CodexSessionScanner } from './utils/codexSessionScanner';
|
||||
import { convertCodexEvent, type CodexMessage } from './utils/codexEventConverter';
|
||||
import { convertCodexEvent, type CodexMessage, type CodexSessionEvent } from './utils/codexEventConverter';
|
||||
import { buildHapiMcpBridge } from './utils/buildHapiMcpBridge';
|
||||
import { parseCodexCliOverrides, stripCodexCliOverrides } from './utils/codexCliOverrides';
|
||||
import { buildCodexPermissionModeCliArgs } from './utils/permissionModeConfig';
|
||||
@@ -22,6 +22,20 @@ type PendingExecWrapper = {
|
||||
turnId?: string;
|
||||
};
|
||||
|
||||
function extractTurnContextReasoningEffort(event: CodexSessionEvent): ReasoningEffort | null | undefined {
|
||||
if (event.type !== 'turn_context') {
|
||||
return undefined;
|
||||
}
|
||||
if (!event.payload || typeof event.payload !== 'object') {
|
||||
return null;
|
||||
}
|
||||
const effort = (event.payload as Record<string, unknown>).effort;
|
||||
if (typeof effort !== 'string' || !effort.trim()) {
|
||||
return null;
|
||||
}
|
||||
return effort.trim().toLowerCase();
|
||||
}
|
||||
|
||||
export async function codexLocalLauncher(session: CodexSession): Promise<'switch' | 'exit'> {
|
||||
const resumeSessionId = session.sessionId;
|
||||
let primarySessionId = resumeSessionId;
|
||||
@@ -172,6 +186,10 @@ export async function codexLocalLauncher(session: CodexSession): Promise<'switch
|
||||
session.onSessionFound(sessionId);
|
||||
},
|
||||
onEvent: (event) => {
|
||||
const observedReasoningEffort = extractTurnContextReasoningEffort(event);
|
||||
if (observedReasoningEffort !== undefined) {
|
||||
session.setModelReasoningEffort(observedReasoningEffort);
|
||||
}
|
||||
const converted = convertCodexEvent(event);
|
||||
if (converted?.sessionId) {
|
||||
if (!isPrimarySessionId(converted.sessionId)) {
|
||||
|
||||
@@ -211,6 +211,13 @@ describe('runCodex', () => {
|
||||
expect(mockCodexSession.setServiceTier).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not collapse inherited Codex reasoning effort into explicit default on startup', async () => {
|
||||
await runCodexImpl({ workingDirectory: '/tmp/project' })
|
||||
|
||||
expect(mockCodexSession.setModelReasoningEffort).not.toHaveBeenCalled()
|
||||
expect(harness.loopArgs[0]?.modelReasoningEffort).toBeUndefined()
|
||||
})
|
||||
|
||||
it('uses lazy bootstrap for a fresh terminal launch', async () => {
|
||||
await runCodexImpl({ workingDirectory: '/tmp/project' })
|
||||
|
||||
@@ -256,7 +263,23 @@ describe('runCodex', () => {
|
||||
await handler?.({ modelReasoningEffort: 'max' })
|
||||
await handler?.({ modelReasoningEffort: ' EXTREME ' })
|
||||
|
||||
expect(mockCodexSession.setModelReasoningEffort).toHaveBeenNthCalledWith(2, 'max')
|
||||
expect(mockCodexSession.setModelReasoningEffort).toHaveBeenNthCalledWith(3, 'extreme')
|
||||
expect(mockCodexSession.setModelReasoningEffort).toHaveBeenNthCalledWith(1, 'max')
|
||||
expect(mockCodexSession.setModelReasoningEffort).toHaveBeenNthCalledWith(2, 'extreme')
|
||||
})
|
||||
|
||||
it('still persists an explicit reasoning effort reset as null', async () => {
|
||||
await runCodexImpl({ workingDirectory: '/tmp/project' })
|
||||
|
||||
const registration = harness.session.rpcHandlerManager.registerHandler.mock.calls.find(
|
||||
([method]) => method === RPC_METHODS.SetSessionConfig
|
||||
)
|
||||
const handler = registration?.[1] as ((payload: unknown) => Promise<unknown>) | undefined
|
||||
const result = await handler?.({ modelReasoningEffort: null })
|
||||
|
||||
expect(mockCodexSession.setModelReasoningEffort).toHaveBeenCalledTimes(1)
|
||||
expect(mockCodexSession.setModelReasoningEffort).toHaveBeenCalledWith(null)
|
||||
expect(result).toEqual({
|
||||
applied: expect.objectContaining({ modelReasoningEffort: null })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+16
-10
@@ -88,7 +88,9 @@ export async function runCodex(opts: {
|
||||
?? (persistedPermissionMode && isPermissionModeAllowedForFlavor(persistedPermissionMode, 'codex') ? persistedPermissionMode as PermissionMode : undefined)
|
||||
?? 'default';
|
||||
let currentModel = opts.model;
|
||||
let currentModelReasoningEffort: ReasoningEffort | undefined = opts.modelReasoningEffort;
|
||||
// Three states matter here: `undefined` inherits Codex configuration,
|
||||
// `null` explicitly clears a HAPI override, and a string explicitly sets it.
|
||||
let currentModelReasoningEffort: ReasoningEffort | null | undefined = opts.modelReasoningEffort;
|
||||
let currentCollaborationMode: EnhancedMode['collaborationMode'] = opts.collaborationMode ?? 'default';
|
||||
let currentProactiveMultiAgent: boolean | undefined;
|
||||
// Service tier (Fast mode), stored representation: `'fast'` and
|
||||
@@ -118,7 +120,11 @@ export async function runCodex(opts: {
|
||||
if (options?.syncModel !== false) {
|
||||
sessionInstance.setModel(currentModel ?? null);
|
||||
}
|
||||
sessionInstance.setModelReasoningEffort(currentModelReasoningEffort ?? null);
|
||||
// Do not collapse inherited Codex config into an explicit default.
|
||||
// Explicit clears remain `null` and must still be synchronized.
|
||||
if (currentModelReasoningEffort !== undefined) {
|
||||
sessionInstance.setModelReasoningEffort(currentModelReasoningEffort);
|
||||
}
|
||||
// Preserve the third state: only sync when the user/persisted session
|
||||
// has an explicit tier. `undefined` means "omit" so the keepalive does
|
||||
// not overwrite the account-default or persisted Fast tier with null.
|
||||
@@ -149,7 +155,7 @@ export async function runCodex(opts: {
|
||||
currentModel = updates.model ?? undefined;
|
||||
}
|
||||
if (updates.modelReasoningEffort !== undefined) {
|
||||
currentModelReasoningEffort = updates.modelReasoningEffort ?? undefined;
|
||||
currentModelReasoningEffort = updates.modelReasoningEffort;
|
||||
}
|
||||
if (updates.collaborationMode !== undefined) {
|
||||
currentCollaborationMode = updates.collaborationMode;
|
||||
@@ -174,7 +180,7 @@ export async function runCodex(opts: {
|
||||
}
|
||||
const sessionModelReasoningEffort = sessionWrapperRef.current?.getModelReasoningEffort();
|
||||
if (sessionModelReasoningEffort !== undefined) {
|
||||
currentModelReasoningEffort = (sessionModelReasoningEffort ?? undefined) as ReasoningEffort | undefined;
|
||||
currentModelReasoningEffort = sessionModelReasoningEffort as ReasoningEffort | null;
|
||||
}
|
||||
const sessionCollaborationMode = sessionWrapperRef.current?.getCollaborationMode();
|
||||
if (sessionCollaborationMode) {
|
||||
@@ -199,7 +205,7 @@ export async function runCodex(opts: {
|
||||
permissionMode: currentPermissionMode,
|
||||
collaborationMode: currentCollaborationMode,
|
||||
model: currentModel,
|
||||
modelReasoningEffort: currentModelReasoningEffort,
|
||||
modelReasoningEffort: currentModelReasoningEffort ?? undefined,
|
||||
serviceTier: currentServiceTier,
|
||||
proactiveMultiAgent: currentProactiveMultiAgent
|
||||
});
|
||||
@@ -219,7 +225,7 @@ export async function runCodex(opts: {
|
||||
messageQueue.pushIsolateAndClear(goalCommand, {
|
||||
permissionMode: currentPermissionMode ?? 'default',
|
||||
model: currentModel,
|
||||
modelReasoningEffort: currentModelReasoningEffort,
|
||||
modelReasoningEffort: currentModelReasoningEffort ?? undefined,
|
||||
collaborationMode: currentCollaborationMode,
|
||||
serviceTier: currentServiceTier
|
||||
}, localId);
|
||||
@@ -258,7 +264,7 @@ export async function runCodex(opts: {
|
||||
const enhancedMode: EnhancedMode = {
|
||||
permissionMode: messagePermissionMode ?? 'default',
|
||||
model: currentModel,
|
||||
modelReasoningEffort: currentModelReasoningEffort,
|
||||
modelReasoningEffort: currentModelReasoningEffort ?? undefined,
|
||||
collaborationMode: currentCollaborationMode,
|
||||
proactiveMultiAgent: currentProactiveMultiAgent,
|
||||
serviceTier: currentServiceTier
|
||||
@@ -273,7 +279,7 @@ export async function runCodex(opts: {
|
||||
const enhancedMode: EnhancedMode = {
|
||||
permissionMode: currentPermissionMode ?? 'default',
|
||||
model: currentModel,
|
||||
modelReasoningEffort: currentModelReasoningEffort,
|
||||
modelReasoningEffort: currentModelReasoningEffort ?? undefined,
|
||||
collaborationMode: currentCollaborationMode,
|
||||
proactiveMultiAgent: currentProactiveMultiAgent,
|
||||
serviceTier: currentServiceTier
|
||||
@@ -367,7 +373,7 @@ export async function runCodex(opts: {
|
||||
}
|
||||
|
||||
if (config.modelReasoningEffort !== undefined) {
|
||||
currentModelReasoningEffort = parseReasoningEffortValue(config.modelReasoningEffort);
|
||||
currentModelReasoningEffort = parseReasoningEffortValue(config.modelReasoningEffort) ?? null;
|
||||
}
|
||||
|
||||
if (config.collaborationMode !== undefined) {
|
||||
@@ -413,7 +419,7 @@ export async function runCodex(opts: {
|
||||
startedBy,
|
||||
permissionMode: currentPermissionMode,
|
||||
model: currentModel,
|
||||
modelReasoningEffort: currentModelReasoningEffort,
|
||||
modelReasoningEffort: currentModelReasoningEffort ?? undefined,
|
||||
collaborationMode: currentCollaborationMode,
|
||||
resumeSessionId: opts.resumeSessionId,
|
||||
sourceSessionId: codexSourceSessionId,
|
||||
|
||||
@@ -401,10 +401,10 @@ describe('appServerConfig', () => {
|
||||
mode: 'plan',
|
||||
settings: {
|
||||
model: 'o3',
|
||||
reasoning_effort: null,
|
||||
developer_instructions: withCollaborationInstructions(`${codexSystemPrompt}\n\nOnly respond in Chinese.`)
|
||||
}
|
||||
});
|
||||
expect(params.collaborationMode?.settings).not.toHaveProperty('reasoning_effort');
|
||||
});
|
||||
|
||||
it('injects spawn_agent argument rules into collaboration mode instructions', () => {
|
||||
@@ -463,7 +463,6 @@ describe('appServerConfig', () => {
|
||||
mode: 'default',
|
||||
settings: {
|
||||
model: 'o3',
|
||||
reasoning_effort: null,
|
||||
developer_instructions: withCollaborationInstructions(codexSystemPrompt)
|
||||
}
|
||||
});
|
||||
@@ -484,7 +483,6 @@ describe('appServerConfig', () => {
|
||||
mode: 'default',
|
||||
settings: {
|
||||
model: 'o3',
|
||||
reasoning_effort: null,
|
||||
developer_instructions: withCollaborationInstructions(codexSystemPrompt)
|
||||
}
|
||||
});
|
||||
@@ -504,7 +502,6 @@ describe('appServerConfig', () => {
|
||||
mode: 'default',
|
||||
settings: {
|
||||
model: 'gpt-5',
|
||||
reasoning_effort: null,
|
||||
developer_instructions: withCollaborationInstructions(codexSystemPrompt)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -280,7 +280,7 @@ export function buildTurnStartParams(args: {
|
||||
mode: collaborationMode,
|
||||
settings: {
|
||||
model,
|
||||
reasoning_effort: modelReasoningEffort ?? null,
|
||||
...(modelReasoningEffort !== undefined ? { reasoning_effort: modelReasoningEffort } : {}),
|
||||
developer_instructions: appendCollaborationInstructions(developerInstructions, args.mode?.proactiveMultiAgent)
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user