fix(ralph-loop): commit iteration only after continuation is dispatched
Reorder the session.idle and session.error retry paths so the durable\niteration counter and the progress toast advance only when continueIteration\nreturns dispatched. On dispatch_rejected or session_creation_rejected,\nclear the loop state and emit a loud failure toast instead of silently\nlogging while the loop appears to make progress.\n\nAdds an explicit settle-window state check so a session.deleted firing\nduring the idleSettleMs sleep no longer feeds dispatch against a cleared\nloop. Keeps idleSettleMs intact for the original idle-settle race.
This commit is contained in:
@@ -19,6 +19,7 @@ type LoopStateController = {
|
|||||||
markVerificationPending: (sessionID: string) => RalphLoopState | null
|
markVerificationPending: (sessionID: string) => RalphLoopState | null
|
||||||
setVerificationSessionID: (sessionID: string, verificationSessionID: string) => RalphLoopState | null
|
setVerificationSessionID: (sessionID: string, verificationSessionID: string) => RalphLoopState | null
|
||||||
restartAfterFailedVerification: (sessionID: string, messageCountAtStart?: number) => RalphLoopState | null
|
restartAfterFailedVerification: (sessionID: string, messageCountAtStart?: number) => RalphLoopState | null
|
||||||
|
clearVerificationState: (sessionID: string, messageCountAtStart?: number) => RalphLoopState | null
|
||||||
}
|
}
|
||||||
type RalphLoopEventHandlerOptions = { directory: string; apiTimeoutMs: number; idleSettleMs: number; getTranscriptPath: (sessionID: string) => string | undefined; checkSessionExists?: RalphLoopOptions["checkSessionExists"]; backgroundManager?: RalphLoopOptions["backgroundManager"]; loopState: LoopStateController }
|
type RalphLoopEventHandlerOptions = { directory: string; apiTimeoutMs: number; idleSettleMs: number; getTranscriptPath: (sessionID: string) => string | undefined; checkSessionExists?: RalphLoopOptions["checkSessionExists"]; backgroundManager?: RalphLoopOptions["backgroundManager"]; loopState: LoopStateController }
|
||||||
|
|
||||||
@@ -272,34 +273,48 @@ export function createRalphLoopEventHandler(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const newState = options.loopState.incrementIteration()
|
await sleep(options.idleSettleMs)
|
||||||
if (!newState) {
|
const stateAfterSettle = options.loopState.getState()
|
||||||
log(`[${HOOK_NAME}] Failed to increment iteration`, { sessionID })
|
if (!stateAfterSettle || !stateAfterSettle.active) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nextIteration = stateAfterSettle.iteration + 1
|
||||||
|
const previewState: RalphLoopState = { ...stateAfterSettle, iteration: nextIteration }
|
||||||
|
|
||||||
log(`[${HOOK_NAME}] Continuing loop`, {
|
log(`[${HOOK_NAME}] Continuing loop`, {
|
||||||
sessionID,
|
sessionID,
|
||||||
iteration: newState.iteration,
|
iteration: nextIteration,
|
||||||
max: newState.max_iterations,
|
max: previewState.max_iterations,
|
||||||
})
|
})
|
||||||
|
|
||||||
showIterationToast(ctx, newState)
|
const result = await continueIteration(ctx, previewState, {
|
||||||
await sleep(options.idleSettleMs)
|
previousSessionID: sessionID,
|
||||||
|
directory: options.directory,
|
||||||
|
apiTimeoutMs: options.apiTimeoutMs,
|
||||||
|
loopState: options.loopState,
|
||||||
|
})
|
||||||
|
|
||||||
try {
|
if (result.status === "dispatched") {
|
||||||
await continueIteration(ctx, newState, {
|
const committed = options.loopState.incrementIteration()
|
||||||
previousSessionID: sessionID,
|
if (committed) {
|
||||||
directory: options.directory,
|
showIterationToast(ctx, committed)
|
||||||
apiTimeoutMs: options.apiTimeoutMs,
|
} else {
|
||||||
loopState: options.loopState,
|
log(`[${HOOK_NAME}] Dispatch succeeded but iteration commit failed`, { sessionID })
|
||||||
})
|
}
|
||||||
} catch (err) {
|
return
|
||||||
log(`[${HOOK_NAME}] Failed to inject continuation`, {
|
|
||||||
sessionID,
|
|
||||||
error: String(err),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log(`[${HOOK_NAME}] Dispatch failed`, { sessionID, status: result.status })
|
||||||
|
options.loopState.clear()
|
||||||
|
showToastBestEffort(ctx, {
|
||||||
|
title: "Ralph Loop Failed",
|
||||||
|
message: result.status === "dispatch_rejected"
|
||||||
|
? `Dispatch ${result.status}: ${String(result.error)}`
|
||||||
|
: `Dispatch ${result.status}`,
|
||||||
|
variant: "warning",
|
||||||
|
duration: 5000,
|
||||||
|
})
|
||||||
return
|
return
|
||||||
} finally {
|
} finally {
|
||||||
inFlightSessions.delete(sessionID)
|
inFlightSessions.delete(sessionID)
|
||||||
@@ -381,28 +396,43 @@ export function createRalphLoopEventHandler(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const newState = options.loopState.incrementIteration()
|
await sleep(options.idleSettleMs)
|
||||||
if (!newState) {
|
const stateAfterSettle = options.loopState.getState()
|
||||||
log(`[${HOOK_NAME}] Failed to increment iteration after runtime error`, { sessionID })
|
if (!stateAfterSettle || !stateAfterSettle.active) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
showIterationToast(ctx, newState)
|
const nextIteration = stateAfterSettle.iteration + 1
|
||||||
await sleep(options.idleSettleMs)
|
const previewState: RalphLoopState = { ...stateAfterSettle, iteration: nextIteration }
|
||||||
try {
|
|
||||||
await continueIteration(ctx, newState, {
|
const result = await continueIteration(ctx, previewState, {
|
||||||
previousSessionID: sessionID,
|
previousSessionID: sessionID,
|
||||||
directory: options.directory,
|
directory: options.directory,
|
||||||
apiTimeoutMs: options.apiTimeoutMs,
|
apiTimeoutMs: options.apiTimeoutMs,
|
||||||
loopState: options.loopState,
|
loopState: options.loopState,
|
||||||
})
|
})
|
||||||
runtimeErrorRetriedSessions.set(sessionID, newState.iteration)
|
|
||||||
} catch (err) {
|
if (result.status === "dispatched") {
|
||||||
log(`[${HOOK_NAME}] Failed to retry after runtime error`, {
|
const committed = options.loopState.incrementIteration()
|
||||||
sessionID,
|
if (committed) {
|
||||||
error: String(err),
|
showIterationToast(ctx, committed)
|
||||||
})
|
runtimeErrorRetriedSessions.set(sessionID, committed.iteration)
|
||||||
|
} else {
|
||||||
|
log(`[${HOOK_NAME}] Dispatch succeeded but iteration commit failed after runtime error`, { sessionID })
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log(`[${HOOK_NAME}] Dispatch failed after runtime error`, { sessionID, status: result.status })
|
||||||
|
options.loopState.clear()
|
||||||
|
showToastBestEffort(ctx, {
|
||||||
|
title: "Ralph Loop Failed",
|
||||||
|
message: result.status === "dispatch_rejected"
|
||||||
|
? `Dispatch ${result.status}: ${String(result.error)}`
|
||||||
|
: `Dispatch ${result.status}`,
|
||||||
|
variant: "warning",
|
||||||
|
duration: 5000,
|
||||||
|
})
|
||||||
} finally {
|
} finally {
|
||||||
inFlightSessions.delete(sessionID)
|
inFlightSessions.delete(sessionID)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user