fix(ralph-loop): commit iteration only after verification continuation dispatches
Split the verification-failure restart into clearVerificationState (clears\nthe verification flags so we cleanly transition back to the main loop)\nfollowed by injectContinuationPrompt, with incrementIteration only on\nsuccessful injection. On rejection: clear the loop state and emit a loud\nwarning toast. Mirrors the dispatch-before-commit contract enforced for\nthe idle and session.error paths.
This commit is contained in:
@@ -174,5 +174,27 @@ export function createLoopStateController(options: {
|
|||||||
|
|
||||||
return state
|
return state
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clearVerificationState(sessionID: string, messageCountAtStart?: number): RalphLoopState | null {
|
||||||
|
const state = readState(directory, stateDir)
|
||||||
|
if (!state || state.session_id !== sessionID || !state.ultrawork || !state.verification_pending) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
state.started_at = new Date().toISOString()
|
||||||
|
state.completion_promise = state.initial_completion_promise ?? DEFAULT_COMPLETION_PROMISE
|
||||||
|
state.verification_pending = undefined
|
||||||
|
state.verification_attempt_id = undefined
|
||||||
|
state.verification_session_id = undefined
|
||||||
|
if (typeof messageCountAtStart === "number") {
|
||||||
|
state.message_count_at_start = messageCountAtStart
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!writeState(directory, state, stateDir)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ async function detectOracleVerificationFromParentSession(
|
|||||||
|
|
||||||
type LoopStateController = {
|
type LoopStateController = {
|
||||||
restartAfterFailedVerification: (sessionID: string, messageCountAtStart?: number) => RalphLoopState | null
|
restartAfterFailedVerification: (sessionID: string, messageCountAtStart?: number) => RalphLoopState | null
|
||||||
|
clearVerificationState: (sessionID: string, messageCountAtStart?: number) => RalphLoopState | null
|
||||||
|
incrementIteration: () => RalphLoopState | null
|
||||||
|
clear: () => boolean
|
||||||
setVerificationSessionID: (sessionID: string, verificationSessionID: string) => RalphLoopState | null
|
setVerificationSessionID: (sessionID: string, verificationSessionID: string) => RalphLoopState | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,22 @@ import { injectContinuationPrompt } from "./continuation-prompt-injector"
|
|||||||
import type { RalphLoopState } from "./types"
|
import type { RalphLoopState } from "./types"
|
||||||
|
|
||||||
type LoopStateController = {
|
type LoopStateController = {
|
||||||
restartAfterFailedVerification: (
|
clearVerificationState: (
|
||||||
sessionID: string,
|
sessionID: string,
|
||||||
messageCountAtStart?: number,
|
messageCountAtStart?: number,
|
||||||
) => RalphLoopState | null
|
) => RalphLoopState | null
|
||||||
|
incrementIteration: () => RalphLoopState | null
|
||||||
|
clear: () => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function showToastBestEffort(
|
||||||
|
ctx: PluginInput,
|
||||||
|
body: { title: string; message: string; variant: "warning" | "info"; duration: number },
|
||||||
|
): void {
|
||||||
|
try {
|
||||||
|
void Promise.resolve(ctx.client.tui?.showToast?.({ body })).catch(() => {})
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMessageCountFromResponse(messagesResponse: unknown): number {
|
function getMessageCountFromResponse(messagesResponse: unknown): number {
|
||||||
@@ -72,23 +84,45 @@ export async function handleFailedVerification(
|
|||||||
ctx.client.session.abort({ path: { id: state.verification_session_id } }).catch(() => {})
|
ctx.client.session.abort({ path: { id: state.verification_session_id } }).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
const resumedState = loopState.restartAfterFailedVerification(
|
const clearedState = loopState.clearVerificationState(
|
||||||
parentSessionID,
|
parentSessionID,
|
||||||
messageCountAtStart,
|
messageCountAtStart,
|
||||||
)
|
)
|
||||||
if (!resumedState) {
|
if (!clearedState) {
|
||||||
log(`[${HOOK_NAME}] Failed to restart loop after verification failure`, {
|
log(`[${HOOK_NAME}] Failed to restart loop after verification failure`, {
|
||||||
parentSessionID,
|
parentSessionID,
|
||||||
})
|
})
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
await injectContinuationPrompt(ctx, {
|
const previewState: RalphLoopState = { ...clearedState, iteration: clearedState.iteration + 1 }
|
||||||
sessionID: parentSessionID,
|
|
||||||
prompt: buildVerificationFailurePrompt(resumedState),
|
try {
|
||||||
directory,
|
await injectContinuationPrompt(ctx, {
|
||||||
apiTimeoutMs,
|
sessionID: parentSessionID,
|
||||||
})
|
prompt: buildVerificationFailurePrompt(previewState),
|
||||||
|
directory,
|
||||||
|
apiTimeoutMs,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
log(`[${HOOK_NAME}] Failed to inject verification failure prompt`, {
|
||||||
|
parentSessionID,
|
||||||
|
error: String(error),
|
||||||
|
})
|
||||||
|
loopState.clear()
|
||||||
|
showToastBestEffort(ctx, {
|
||||||
|
title: "Ralph Loop Failed",
|
||||||
|
message: `Verification continuation rejected: ${String(error)}`,
|
||||||
|
variant: "warning",
|
||||||
|
duration: 5000,
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const committed = loopState.incrementIteration()
|
||||||
|
if (!committed) {
|
||||||
|
log(`[${HOOK_NAME}] Failed to commit iteration after verification restart`, { parentSessionID })
|
||||||
|
}
|
||||||
|
|
||||||
await ctx.client.tui?.showToast?.({
|
await ctx.client.tui?.showToast?.({
|
||||||
body: {
|
body: {
|
||||||
|
|||||||
Reference in New Issue
Block a user