mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 20:03:13 +08:00
fix: defer runtime restarts during active turns (#626)
Avoid killing an in-flight desktop SDK turn when runtime config changes during tool execution. Persist the requested runtime immediately, then restart after the turn emits its terminal result. Tested: - bun run check:server - real provider tool-turn model switch smoke - bun run check:coverage (changed-line coverage passed; unrelated global lanes still fail) Scope-risk: narrow
This commit is contained in:
@@ -76,11 +76,19 @@ const sessionTitleState = new Map<string, {
|
||||
generationSeq: number
|
||||
}>()
|
||||
|
||||
const runtimeOverrides = new Map<string, {
|
||||
type RuntimeOverride = {
|
||||
providerId: string | null
|
||||
modelId: string
|
||||
effort?: string
|
||||
}>()
|
||||
}
|
||||
|
||||
type ActiveUserTurnState = {
|
||||
messageSent: boolean
|
||||
}
|
||||
|
||||
const runtimeOverrides = new Map<string, RuntimeOverride>()
|
||||
const activeUserTurns = new Map<string, ActiveUserTurnState>()
|
||||
const deferredRuntimeRestarts = new Map<string, RuntimeOverride>()
|
||||
|
||||
const runtimeTransitionPromises = new Map<string, Promise<void>>()
|
||||
const sessionStartupPromises = new Map<string, Promise<void>>()
|
||||
@@ -306,8 +314,14 @@ async function handleUserMessage(
|
||||
// Send thinking status
|
||||
sendMessage(ws, { type: 'status', state: 'thinking', verb: 'Thinking' })
|
||||
|
||||
const activeTurn: ActiveUserTurnState = { messageSent: false }
|
||||
activeUserTurns.set(sessionId, activeTurn)
|
||||
|
||||
const initialRuntimeTransition = await waitForRuntimeTransitionBeforeUserTurn(ws, sessionId)
|
||||
if (!initialRuntimeTransition.ok) return
|
||||
if (!initialRuntimeTransition.ok) {
|
||||
clearActiveUserTurn(sessionId, activeTurn)
|
||||
return
|
||||
}
|
||||
if (initialRuntimeTransition.waited) {
|
||||
sendMessage(ws, { type: 'status', state: 'thinking', verb: 'Thinking' })
|
||||
}
|
||||
@@ -357,6 +371,7 @@ async function handleUserMessage(
|
||||
err instanceof ConversationStartupError ? err.retryable : false,
|
||||
})
|
||||
sendMessage(ws, { type: 'status', state: 'idle' })
|
||||
clearActiveUserTurn(sessionId, activeTurn)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -366,6 +381,7 @@ async function handleUserMessage(
|
||||
sendMessage(ws, { type: 'status', state: 'thinking', verb: 'Thinking' })
|
||||
}
|
||||
} else {
|
||||
clearActiveUserTurn(sessionId, activeTurn)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -387,6 +403,7 @@ async function handleUserMessage(
|
||||
return shouldForwardCurrentTurnLocalCommand(cliMsg)
|
||||
},
|
||||
})
|
||||
const removeActiveTurnOutputCallback = bindActiveUserTurnCompletion(ws, sessionId, activeTurn)
|
||||
|
||||
const sent = await conversationService.sendMessage(
|
||||
sessionId,
|
||||
@@ -394,6 +411,8 @@ async function handleUserMessage(
|
||||
message.attachments
|
||||
)
|
||||
if (!sent) {
|
||||
removeActiveTurnOutputCallback()
|
||||
clearActiveUserTurn(sessionId, activeTurn)
|
||||
removeTitleOutputCallback?.()
|
||||
discardActiveTitleTurn(sessionId, titleTurnNumber)
|
||||
sendMessage(ws, {
|
||||
@@ -406,6 +425,57 @@ async function handleUserMessage(
|
||||
}
|
||||
|
||||
userMessageSent = true
|
||||
activeTurn.messageSent = true
|
||||
}
|
||||
|
||||
function clearActiveUserTurn(sessionId: string, activeTurn: ActiveUserTurnState): void {
|
||||
if (activeUserTurns.get(sessionId) === activeTurn) {
|
||||
activeUserTurns.delete(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
function bindActiveUserTurnCompletion(
|
||||
ws: ServerWebSocket<WebSocketData>,
|
||||
sessionId: string,
|
||||
activeTurn: ActiveUserTurnState,
|
||||
): () => void {
|
||||
const callback = (cliMsg: any) => {
|
||||
if (!activeTurn.messageSent || cliMsg?.type !== 'result') return
|
||||
|
||||
conversationService.removeOutputCallback(sessionId, callback)
|
||||
clearActiveUserTurn(sessionId, activeTurn)
|
||||
applyDeferredRuntimeRestartAfterActiveTurn(ws, sessionId)
|
||||
}
|
||||
|
||||
conversationService.onOutput(sessionId, callback)
|
||||
return () => conversationService.removeOutputCallback(sessionId, callback)
|
||||
}
|
||||
|
||||
function shouldDeferRuntimeRestartForActiveTurn(sessionId: string): boolean {
|
||||
return activeUserTurns.get(sessionId)?.messageSent === true
|
||||
}
|
||||
|
||||
function applyDeferredRuntimeRestartAfterActiveTurn(
|
||||
ws: ServerWebSocket<WebSocketData>,
|
||||
sessionId: string,
|
||||
): void {
|
||||
const deferred = deferredRuntimeRestarts.get(sessionId)
|
||||
if (!deferred) return
|
||||
|
||||
deferredRuntimeRestarts.delete(sessionId)
|
||||
void enqueueRuntimeTransition(sessionId, async () => {
|
||||
const currentOverride = runtimeOverrides.get(sessionId)
|
||||
if (
|
||||
!currentOverride ||
|
||||
currentOverride.providerId !== deferred.providerId ||
|
||||
currentOverride.modelId !== deferred.modelId ||
|
||||
currentOverride.effort !== deferred.effort ||
|
||||
!conversationService.hasSession(sessionId)
|
||||
) {
|
||||
return
|
||||
}
|
||||
await restartSessionWithRuntimeConfig(ws, sessionId)
|
||||
})
|
||||
}
|
||||
|
||||
async function handleDesktopClearCommand(
|
||||
@@ -621,6 +691,12 @@ async function handleSetRuntimeConfig(
|
||||
(runtimeOverrideVersions.get(sessionId) ?? 0) + 1,
|
||||
)
|
||||
|
||||
if (shouldDeferRuntimeRestartForActiveTurn(sessionId)) {
|
||||
deferredRuntimeRestarts.set(sessionId, nextOverride)
|
||||
await persistSessionRuntimeConfig(sessionId, nextOverride)
|
||||
return
|
||||
}
|
||||
|
||||
if (conversationService.hasSession(sessionId)) {
|
||||
await enqueueRuntimeTransition(sessionId, async () => {
|
||||
await persistSessionRuntimeConfig(sessionId, nextOverride)
|
||||
@@ -1081,6 +1157,8 @@ function cleanupSessionRuntimeState(sessionId: string) {
|
||||
sessionSlashCommands.delete(sessionId)
|
||||
sessionTitleState.delete(sessionId)
|
||||
runtimeOverrides.delete(sessionId)
|
||||
activeUserTurns.delete(sessionId)
|
||||
deferredRuntimeRestarts.delete(sessionId)
|
||||
runtimeTransitionPromises.delete(sessionId)
|
||||
sessionStartupPromises.delete(sessionId)
|
||||
lastResolvedStartupWorkDirs.delete(sessionId)
|
||||
|
||||
Reference in New Issue
Block a user