fix(coding-agent): persist API mode per session

This commit is contained in:
x1051445024
2026-07-05 17:17:50 +08:00
parent 785bc79685
commit 4cb55e607e
19 changed files with 175 additions and 24 deletions
+17 -1
View File
@@ -9267,6 +9267,22 @@
},
"provider": {
"type": "string"
},
"apiMode": {
"type": "string",
"enum": [
"chat_completions",
"codex_responses",
"anthropic_messages"
]
},
"api_mode": {
"type": "string",
"enum": [
"chat_completions",
"codex_responses",
"anthropic_messages"
]
}
},
"required": [
@@ -11690,4 +11706,4 @@
}
}
}
}
}
+1
View File
@@ -106,6 +106,7 @@ export interface CodingAgentLaunchResult {
profile: string
provider: string
model: string
apiMode?: CodingAgentApiMode
rootDir: string
workspaceDir: string
command: string
@@ -11,6 +11,7 @@ export interface SessionSummary {
agent_native_session_id?: string
model: string
provider?: string
api_mode?: ProviderApiMode
title: string | null
parent_session_id?: string | null
fork_point_message_id?: string | null
@@ -1078,7 +1078,9 @@ async function selectSessionModel(model: string, provider: string) {
if (meta?.disabled || !sessionModelSessionId.value) return;
if (isSessionModelExternalCodingAgent.value) {
pendingSessionModelSwitch.value = { model, provider };
sessionModelApiMode.value = defaultSessionModelApiMode(provider);
sessionModelApiMode.value = sessionModelSession.value?.provider === provider && sessionModelSession.value.apiMode
? normalizeCodingAgentApiMode(sessionModelSession.value.apiMode, defaultSessionModelApiMode(provider))
: defaultSessionModelApiMode(provider);
showSessionModelModeModal.value = true;
return;
}
+16 -3
View File
@@ -562,6 +562,7 @@ function mapHermesSession(s: SessionSummary): Session {
updatedAt: Math.round(activitySeconds * 1000),
model: s.model,
provider: s.provider || (s as any).billing_provider || '',
apiMode: s.api_mode,
messageCount: s.message_count,
messageTotal: s.message_count,
loadedMessageCount: 0,
@@ -615,6 +616,10 @@ function clearCodingAgentRuntimeCredentials(session?: Session | null) {
session.apiKey = undefined
}
function shouldPreserveRuntimeApiMode(session?: Session | null): boolean {
return isCodingAgentLikeSession(session) && session?.codingAgentMode !== 'global'
}
function isQuotaExceededError(error: unknown): boolean {
if (!error || typeof error !== 'object') return false
const e = error as { name?: string, code?: number }
@@ -982,6 +987,7 @@ export const useChatStore = defineStore('chat', () => {
...mapped,
messages: existing.messages,
contextTokens: existing.contextTokens,
apiMode: mapped.apiMode || existing.apiMode,
loadedMessageCount: existing.loadedMessageCount,
hasMoreBefore: existing.hasMoreBefore,
})
@@ -1001,11 +1007,13 @@ export const useChatStore = defineStore('chat', () => {
const runtimeByIdBefore = new Map(sessions.value.map(s => [s.id, {
messages: s.messages,
contextTokens: s.contextTokens,
apiMode: s.apiMode,
}]))
for (const s of fresh) {
const prev = runtimeByIdBefore.get(s.id)
if (prev?.messages?.length) s.messages = prev.messages
if (prev?.contextTokens != null) s.contextTokens = prev.contextTokens
if (!s.apiMode && prev?.apiMode) s.apiMode = prev.apiMode
}
sessions.value = fresh
pruneCompletedUnreadSessions(new Set(sessions.value.map(s => s.id)))
@@ -1073,6 +1081,7 @@ export const useChatStore = defineStore('chat', () => {
existing.endedAt = fresh.endedAt
existing.model = fresh.model
existing.provider = fresh.provider
existing.apiMode = fresh.apiMode || existing.apiMode
existing.messageCount = fresh.messageCount
existing.inputTokens = fresh.inputTokens
existing.outputTokens = fresh.outputTokens
@@ -1468,18 +1477,22 @@ export const useChatStore = defineStore('chat', () => {
const shouldClearRuntimeCredentials = previousProvider !== nextProvider && (
isCodingAgentLikeSession(target) || isCodingAgentLikeSession(activeTarget)
)
const ok = await setSessionModel(targetId, modelId, provider || '', apiMode)
const preservedApiMode = apiMode || (previousProvider === nextProvider
? (shouldPreserveRuntimeApiMode(target) ? target?.apiMode : undefined) ||
(shouldPreserveRuntimeApiMode(activeTarget) ? activeTarget?.apiMode : undefined)
: undefined)
const ok = await setSessionModel(targetId, modelId, provider || '', preservedApiMode)
if (!ok) return false
if (target) {
target.model = modelId
target.provider = provider || ''
if (apiMode) target.apiMode = apiMode
target.apiMode = preservedApiMode
if (shouldClearRuntimeCredentials) clearCodingAgentRuntimeCredentials(target)
}
if (activeTarget) {
activeTarget.model = modelId
activeTarget.provider = provider || ''
if (apiMode) activeTarget.apiMode = apiMode
activeTarget.apiMode = preservedApiMode
if (shouldClearRuntimeCredentials) clearCodingAgentRuntimeCredentials(activeTarget)
}
return true
@@ -323,6 +323,7 @@ export async function listConversations(ctx: any) {
agent_native_session_id: s.agent_native_session_id,
model: s.model,
provider: s.provider,
api_mode: s.api_mode,
title: s.title,
started_at: s.started_at,
ended_at: s.ended_at,
@@ -985,8 +986,17 @@ export async function setWorkspace(ctx: any) {
ctx.body = { ok: true }
}
type SessionProviderApiMode = 'chat_completions' | 'codex_responses' | 'anthropic_messages'
function normalizeSessionApiMode(value: unknown): SessionProviderApiMode | undefined {
const mode = typeof value === 'string' ? value.trim() : ''
return mode === 'chat_completions' || mode === 'codex_responses' || mode === 'anthropic_messages'
? mode
: undefined
}
export async function setModel(ctx: any) {
const { model, provider } = ctx.request.body as { model?: string; provider?: string }
const { model, provider, apiMode, api_mode } = ctx.request.body as { model?: string; provider?: string; apiMode?: SessionProviderApiMode; api_mode?: SessionProviderApiMode }
if (!model || typeof model !== 'string') {
ctx.status = 400
ctx.body = { error: 'model is required' }
@@ -1004,19 +1014,22 @@ export async function setModel(ctx: any) {
const profile = existing?.profile || requestedProfile(ctx) || 'default'
const cleanModel = model.trim()
const cleanProvider = (provider || '').trim()
const cleanApiMode = normalizeSessionApiMode(apiMode ?? api_mode)
const codingAgentSession = isCodingAgentSession(existing)
const workspace = !codingAgentSession
? await ensureHermesRunWorkspace(profile, existing?.workspace)
: undefined
if (!existing) {
createSession({ id, profile, title: '', model: cleanModel, provider: cleanProvider, workspace })
createSession({ id, profile, title: '', model: cleanModel, provider: cleanProvider, api_mode: cleanApiMode || '', workspace })
}
const updates: Record<string, string> = { model: cleanModel, provider: cleanProvider }
if (cleanApiMode) updates.api_mode = cleanApiMode
else if (codingAgentSession && existing && existing.provider !== cleanProvider) updates.api_mode = ''
if (!codingAgentSession && existing && !existing.workspace && workspace) updates.workspace = workspace
if (
codingAgentSession &&
existing &&
(existing.model !== cleanModel || existing.provider !== cleanProvider)
(existing.model !== cleanModel || existing.provider !== cleanProvider || (cleanApiMode && existing.api_mode !== cleanApiMode))
) {
updates.agent_native_session_id = ''
}
+1
View File
@@ -39,6 +39,7 @@ export const SESSIONS_SCHEMA: Record<string, string> = {
user_id: 'TEXT',
model: 'TEXT NOT NULL DEFAULT \'\'',
provider: 'TEXT NOT NULL DEFAULT \'\'',
api_mode: 'TEXT NOT NULL DEFAULT \'\'',
title: 'TEXT',
parent_session_id: 'TEXT',
fork_point_message_id: 'TEXT',
+11 -5
View File
@@ -19,6 +19,7 @@ export interface HermesSessionRow {
user_id: string | null
model: string
provider: string
api_mode: string
title: string | null
parent_session_id: string | null
fork_point_message_id: string | null
@@ -105,6 +106,7 @@ function mapSessionRow(row: Record<string, unknown>): HermesSessionRow {
user_id: row.user_id != null ? String(row.user_id) : null,
model: String(row.model || ''),
provider: String(row.provider || ''),
api_mode: String(row.api_mode || ''),
title,
parent_session_id: row.parent_session_id != null ? String(row.parent_session_id) : null,
fork_point_message_id: row.fork_point_message_id != null ? String(row.fork_point_message_id) : null,
@@ -165,6 +167,7 @@ export function createSession(data: {
agent_native_session_id?: string
model?: string
provider?: string
api_mode?: string
title?: string
parent_session_id?: string | null
workspace?: string
@@ -177,7 +180,7 @@ export function createSession(data: {
id: data.id, profile: data.profile || 'default', source, agent,
agent_mode: data.agent_mode || '',
agent_session_id: data.agent_session_id || '', agent_native_session_id: data.agent_native_session_id || '',
user_id: null, model: data.model || '', provider: data.provider || '', title: data.title || null,
user_id: null, model: data.model || '', provider: data.provider || '', api_mode: data.api_mode || '', title: data.title || null,
parent_session_id: data.parent_session_id || null,
fork_point_message_id: null,
started_at: now, ended_at: null, end_reason: null,
@@ -189,8 +192,8 @@ export function createSession(data: {
}
const db = getDb()!
db.prepare(
`INSERT INTO ${SESSIONS_TABLE} (id, profile, source, agent, agent_mode, agent_session_id, agent_native_session_id, model, provider, title, parent_session_id, started_at, last_active, workspace)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
`INSERT INTO ${SESSIONS_TABLE} (id, profile, source, agent, agent_mode, agent_session_id, agent_native_session_id, model, provider, api_mode, title, parent_session_id, started_at, last_active, workspace)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
).run(
data.id,
data.profile || 'default',
@@ -201,6 +204,7 @@ export function createSession(data: {
data.agent_native_session_id || '',
data.model || '',
data.provider || '',
data.api_mode || '',
data.title || null,
data.parent_session_id || null,
now,
@@ -221,6 +225,7 @@ export function createBranchedSession(data: {
agent_native_session_id?: string
model?: string
provider?: string
api_mode?: string
title?: string
workspace?: string | null
ended_at: number
@@ -257,8 +262,8 @@ export function createBranchedSession(data: {
).run(data.ended_at, 'branched', data.parent_session_id)
db.prepare(
`INSERT INTO ${SESSIONS_TABLE} (id, profile, source, agent, agent_mode, agent_session_id, agent_native_session_id, model, provider, title, parent_session_id, started_at, last_active, workspace, message_count)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
`INSERT INTO ${SESSIONS_TABLE} (id, profile, source, agent, agent_mode, agent_session_id, agent_native_session_id, model, provider, api_mode, title, parent_session_id, started_at, last_active, workspace, message_count)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
).run(
data.id,
data.profile || 'default',
@@ -269,6 +274,7 @@ export function createBranchedSession(data: {
data.agent_native_session_id || '',
data.model || '',
data.provider || '',
data.api_mode || '',
data.title || null,
data.parent_session_id,
data.ended_at,
@@ -3,6 +3,7 @@ import { existsSync, accessSync, chmodSync, constants as fsConstants, readFileSy
import { homedir } from 'os'
import { spawn, type ChildProcess } from 'child_process'
import { createSession, addMessage, getSession, updateSession, updateSessionStats } from '../../db/hermes/session-store'
import type { ApiMode } from './types'
import { logger } from '../logger'
import { applyResponseStreamEvent, flushResponseRunToDb } from '../hermes/run-chat/response-stream'
import { calcAndUpdateUsage, updateMessageContextTokenUsage } from '../hermes/run-chat/usage'
@@ -62,6 +63,7 @@ export interface CodingAgentRunLaunch {
profile: string
provider: string
model: string
apiMode?: ApiMode
sessionId: string
agentNativeSessionId?: string
nativeResume?: boolean
@@ -417,6 +419,7 @@ export class CodingAgentRunManager {
provider?: string
model?: string
reasoningEffort?: string
apiMode?: ApiMode
}): boolean {
const run = this.getBySession(sessionId)
if (!run || run.exited) return false
@@ -428,6 +431,8 @@ export class CodingAgentRunManager {
const model = String(launch.model || '').trim()
if (provider && run.launch.provider !== provider) return false
if (model && run.launch.model !== model) return false
const apiMode = String(launch.apiMode || '').trim()
if (apiMode && String(run.launch.apiMode || '').trim() !== apiMode) return false
}
if (String(run.launch.reasoningEffort || '').trim() !== String(launch.reasoningEffort || '').trim()) return false
if (!hasManagedHermesMcpConfig(run)) return false
@@ -697,12 +702,13 @@ export class CodingAgentRunManager {
createSession({
id: run.launch.sessionId,
profile: run.launch.profile,
source,
agent: run.launch.agentId === 'codex' ? 'codex' : 'claude',
agent_session_id: run.id,
agent_native_session_id: run.launch.agentNativeSessionId,
model: run.launch.model,
source,
agent: run.launch.agentId === 'codex' ? 'codex' : 'claude',
agent_session_id: run.id,
agent_native_session_id: run.launch.agentNativeSessionId,
model: run.launch.model,
provider: run.launch.provider,
api_mode: run.launch.apiMode || '',
title: '',
workspace: run.launch.workspaceDir,
})
+22 -3
View File
@@ -124,6 +124,7 @@ export interface CodingAgentLaunchResult {
profile: string
provider: string
model: string
apiMode?: ApiMode
rootDir: string
workspaceDir: string
command: string
@@ -500,12 +501,17 @@ async function resolveStoredProviderLaunchInput(
if (input.mode === 'global') return input
const profile = String(input.profile || existingSession?.profile || 'default').trim() || 'default'
const provider = String(input.provider || existingSession?.provider || '').trim()
const inputProvider = String(input.provider || '').trim()
const storedProvider = String(existingSession?.provider || '').trim()
const provider = String(inputProvider || storedProvider).trim()
const model = String(input.model || existingSession?.model || '').trim()
const workspace = input.workspace || existingSession?.workspace || undefined
let baseUrl = String(input.baseUrl || '').trim()
let apiKey = String(input.apiKey || '').trim()
let apiMode = input.apiMode
const storedApiMode = !inputProvider || inputProvider === storedProvider
? normalizeStoredLaunchApiMode(existingSession?.api_mode)
: undefined
let apiMode = input.apiMode || storedApiMode
let canonicalProvider = provider
const ignoredStaleProviderRuntime = belongsToDifferentBuiltinProvider(provider, baseUrl)
if (ignoredStaleProviderRuntime) {
@@ -577,6 +583,15 @@ async function resolveStoredProviderLaunchInput(
}
}
function normalizeStoredLaunchApiMode(value: unknown): ApiMode | undefined {
if (!value) return undefined
try {
return normalizeLaunchApiMode(value, 'chat_completions')
} catch {
return undefined
}
}
function normalizeLaunchApiMode(value: unknown, fallback: ApiMode): ApiMode {
if (!value) return fallback
const mode = String(value).trim() as ApiMode
@@ -1726,6 +1741,7 @@ export async function prepareCodingAgentLaunch(id: string, input: CodingAgentLau
profile: scope.profile,
provider: scope.provider,
model,
apiMode,
rootDir,
workspaceDir,
command: tool.command,
@@ -1773,7 +1789,8 @@ export async function startCodingAgentRun(
? storedCodingAgentMode(existingSession) === requestedMode &&
(existingSession.agent === (id === 'codex' ? 'codex' : 'claude') || !existingSession.agent) &&
String(existingSession.provider || '').trim() === String(resolvedInput.provider || '').trim() &&
String(existingSession.model || '').trim() === String(resolvedInput.model || '').trim()
String(existingSession.model || '').trim() === String(resolvedInput.model || '').trim() &&
(!String(existingSession.api_mode || '').trim() || String(existingSession.api_mode || '').trim() === String(resolvedInput.apiMode || '').trim())
: false
const existingNativeSessionId = canResumeNativeSession ? existingSession?.agent_native_session_id || '' : ''
const agentNativeSessionId = resolvedInput.agentNativeSessionId || existingNativeSessionId || (id === 'claude-code' ? randomUUID() : '')
@@ -1800,6 +1817,7 @@ export async function startCodingAgentRun(
profile: launch.profile,
provider: persistedProvider,
model: launch.model,
apiMode: launch.apiMode,
sessionId,
agentNativeSessionId,
nativeResume: Boolean(existingNativeSessionId),
@@ -1820,6 +1838,7 @@ export async function startCodingAgentRun(
agent_native_session_id: agentNativeSessionId,
model: launch.model,
provider: persistedProvider,
api_mode: launch.apiMode || '',
workspace: launch.workspaceDir,
})
return {
@@ -42,6 +42,7 @@ export interface ConversationSummary {
agent_native_session_id?: string
model: string
provider?: string
api_mode?: string
title: string | null
started_at: number
ended_at: number | null
@@ -64,11 +64,13 @@ export async function handleCodingAgentRun(
const storedSession = getSession(sessionId)
const launchProvider = data.provider || (mode === 'scoped' ? storedSession?.provider || undefined : undefined)
const launchModel = data.model || (mode === 'scoped' ? storedSession?.model || undefined : undefined)
const launchApiMode = data.apiMode || data.api_mode || (mode === 'scoped' ? storedSession?.api_mode || undefined : undefined)
if (runId && !codingAgentRunManager.isSessionLaunchCompatible(sessionId, {
agentId,
mode,
provider: launchProvider,
model: launchModel,
apiMode: launchApiMode,
reasoningEffort: data.reasoning_effort,
})) {
codingAgentRunManager.stop(sessionId, { reportClosed: false })
@@ -84,7 +86,7 @@ export async function handleCodingAgentRun(
workspace: data.workspace,
baseUrl: data.baseUrl || data.base_url,
apiKey: data.apiKey || data.api_key,
apiMode: data.apiMode || data.api_mode,
apiMode: launchApiMode,
reasoningEffort: data.reasoning_effort,
sessionSource: data.session_source,
}, state)
@@ -1117,6 +1117,7 @@ function createBranchSession(parentSessionId: string, requestedTitle: string, ct
agent_native_session_id: parent.agent_native_session_id || '',
model: parent.model || ctx.model || '',
provider: parent.provider || ctx.provider || '',
api_mode: parent.api_mode || '',
title,
parent_session_id: parentSessionId,
workspace: parent.workspace || undefined,
+3
View File
@@ -608,6 +608,9 @@ function schemaFromType(type) {
const schema = {}
if (/\bnull\b/.test(normalized)) schema.nullable = true
if (/SessionProviderApiMode|CodingAgentApiMode/.test(normalized)) {
return { ...schema, type: 'string', enum: ['chat_completions', 'codex_responses', 'anthropic_messages'] }
}
if (/string\[\]|Array<string>/.test(normalized)) {
return { ...schema, type: 'array', items: { type: 'string' } }
}
@@ -546,6 +546,33 @@ describe('chat store reasoning/tool boundaries', () => {
expect(session.apiMode).toBe('chat_completions')
})
it('preserves a scoped coding-agent API mode when reselecting the same provider', async () => {
const store = useChatStore()
const session = makeSession()
session.source = 'coding_agent'
session.agent = 'codex'
session.codingAgentId = 'codex'
session.codingAgentMode = 'scoped'
session.provider = 'fun-codex'
session.model = 'gpt-5.4'
session.apiMode = 'chat_completions'
store.sessions = [session]
store.activeSessionId = 'session-1'
store.activeSession = session
const ok = await store.switchSessionModel('gpt-5.5', 'fun-codex', 'session-1')
expect(ok).toBe(true)
expect(sessionsApi.setSessionModel).toHaveBeenCalledWith(
'session-1',
'gpt-5.5',
'fun-codex',
'chat_completions',
)
expect(session.apiMode).toBe('chat_completions')
})
it('sends the selected workspace when starting a coding-agent run', async () => {
const store = useChatStore()
const session = makeSession()
@@ -278,6 +278,39 @@ describe('coding agent resumed session config', () => {
}))
})
it('prefers a stored coding-agent API mode when resuming without an explicit mode', async () => {
const home = makeHome()
getSessionMock.mockReturnValue({
id: 'session-1',
profile: 'default',
source: 'coding_agent',
agent: 'codex',
agent_session_id: 'agent-session-1',
provider: 'fun-codex',
model: 'gpt-5.5',
api_mode: 'chat_completions',
})
readConfigYamlForProfileMock.mockResolvedValue({})
safeReadFileMock.mockResolvedValue('')
const { startCodingAgentRun } = await import('../../packages/server/src/services/coding-agents')
await startCodingAgentRun('codex', {
sessionId: 'session-1',
baseUrl: 'https://api.apikey.fun/v1',
apiKey: 'sk-test',
})
const launch = startRunMock.mock.calls[0][0]
expect(launch.apiMode).toBe('chat_completions')
const config = readFileSync(join(home, 'coding-agent', 'model', 'default', 'fun-codex', 'codex', 'config.toml'), 'utf-8')
const routeKey = config.match(/\/api\/codex-proxy\/([^/]+)\/v1/)?.[1] || ''
expect(Buffer.from(routeKey, 'base64url').toString('utf8')).toContain('chat_completions')
expect(updateSessionMock).toHaveBeenCalledWith('session-1', expect.objectContaining({
api_mode: 'chat_completions',
}))
})
it('rejects OAuth/subscription providers for scoped coding-agent launches', async () => {
makeHome()
getSessionMock.mockReturnValue(null)
@@ -83,6 +83,7 @@ function makeParentSession(overrides: Record<string, any> = {}) {
agent_native_session_id: '',
model: 'openai/gpt-5.4',
provider: 'openai-codex',
api_mode: 'chat_completions',
title: 'Parent chat',
preview: 'Parent prompt',
workspace: '/repo',
@@ -229,6 +230,7 @@ describe('branch session command', () => {
agent: 'hermes',
model: 'openai/gpt-5.4',
provider: 'openai-codex',
api_mode: 'chat_completions',
workspace: '/repo',
ended_at: expect.any(Number),
last_active: expect.any(Number),
@@ -52,6 +52,7 @@ describe('createBranchedSession', () => {
user_id: null,
model: 'openai/gpt-5.4',
provider: 'openai-codex',
api_mode: 'chat_completions',
title: 'Forked child',
parent_session_id: 'parent-session',
fork_point_message_id: '42',
@@ -86,6 +87,7 @@ describe('createBranchedSession', () => {
agent: 'hermes',
model: 'openai/gpt-5.4',
provider: 'openai-codex',
api_mode: 'chat_completions',
title: 'Forked child',
workspace: '/repo',
ended_at: 123,
+4 -2
View File
@@ -1317,7 +1317,7 @@ describe('session conversations controller', () => {
expect(ctx.body).toEqual({ ok: true })
})
it('stores a coding agent session model without stopping the runner or notifying the Hermes bridge', async () => {
it('stores a coding agent session model and API mode without stopping the runner or notifying the Hermes bridge', async () => {
bridgeGetRuntimeStateMock.mockReturnValue({ ready: true, running: true, endpoint: 'ipc:///tmp/hermes-agent-bridge.sock' })
getSessionMock.mockReturnValue({
id: 'codex-session',
@@ -1326,6 +1326,7 @@ describe('session conversations controller', () => {
agent: 'codex',
model: 'old-model',
provider: 'openrouter',
api_mode: 'codex_responses',
agent_native_session_id: 'old-native-thread',
workspace: '/tmp/original-workspace',
})
@@ -1333,7 +1334,7 @@ describe('session conversations controller', () => {
const mod = await import('../../packages/server/src/controllers/hermes/sessions')
const ctx: any = {
params: { id: 'codex-session' },
request: { body: { model: 'gpt-5.5', provider: 'openai-codex' } },
request: { body: { model: 'gpt-5.5', provider: 'openai-codex', apiMode: 'chat_completions' } },
body: null,
}
await mod.setModel(ctx)
@@ -1341,6 +1342,7 @@ describe('session conversations controller', () => {
expect(localUpdateSessionMock).toHaveBeenCalledWith('codex-session', {
model: 'gpt-5.5',
provider: 'openai-codex',
api_mode: 'chat_completions',
agent_native_session_id: '',
})
expect(codingAgentRunManagerMock.stop).not.toHaveBeenCalled()