From caaae191556d2740a508f7da1a7826c87afd2b31 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 11 May 2026 12:46:35 +0900 Subject: [PATCH] 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. --- src/hooks/ralph-loop/loop-state-controller.ts | 22 ++++++++ .../pending-verification-handler.ts | 3 ++ .../verification-failure-handler.ts | 52 +++++++++++++++---- 3 files changed, 68 insertions(+), 9 deletions(-) diff --git a/src/hooks/ralph-loop/loop-state-controller.ts b/src/hooks/ralph-loop/loop-state-controller.ts index 2a455412a..3679a3dab 100644 --- a/src/hooks/ralph-loop/loop-state-controller.ts +++ b/src/hooks/ralph-loop/loop-state-controller.ts @@ -174,5 +174,27 @@ export function createLoopStateController(options: { 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 + }, } } diff --git a/src/hooks/ralph-loop/pending-verification-handler.ts b/src/hooks/ralph-loop/pending-verification-handler.ts index 420a2f935..1976ec9aa 100644 --- a/src/hooks/ralph-loop/pending-verification-handler.ts +++ b/src/hooks/ralph-loop/pending-verification-handler.ts @@ -82,6 +82,9 @@ async function detectOracleVerificationFromParentSession( type LoopStateController = { 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 } diff --git a/src/hooks/ralph-loop/verification-failure-handler.ts b/src/hooks/ralph-loop/verification-failure-handler.ts index f6ea8f522..6b8e2d2cb 100644 --- a/src/hooks/ralph-loop/verification-failure-handler.ts +++ b/src/hooks/ralph-loop/verification-failure-handler.ts @@ -6,10 +6,22 @@ import { injectContinuationPrompt } from "./continuation-prompt-injector" import type { RalphLoopState } from "./types" type LoopStateController = { - restartAfterFailedVerification: ( + clearVerificationState: ( sessionID: string, messageCountAtStart?: number, ) => 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 { @@ -72,23 +84,45 @@ export async function handleFailedVerification( ctx.client.session.abort({ path: { id: state.verification_session_id } }).catch(() => {}) } - const resumedState = loopState.restartAfterFailedVerification( + const clearedState = loopState.clearVerificationState( parentSessionID, messageCountAtStart, ) - if (!resumedState) { + if (!clearedState) { log(`[${HOOK_NAME}] Failed to restart loop after verification failure`, { parentSessionID, }) return false } - await injectContinuationPrompt(ctx, { - sessionID: parentSessionID, - prompt: buildVerificationFailurePrompt(resumedState), - directory, - apiTimeoutMs, - }) + const previewState: RalphLoopState = { ...clearedState, iteration: clearedState.iteration + 1 } + + try { + await injectContinuationPrompt(ctx, { + 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?.({ body: {