From 2e36a92c25458572d12d2da3382e9223962a88c2 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 11 May 2026 13:02:16 +0900 Subject: [PATCH] fix(ralph-loop): revalidate ownership and harden commit/session-creation failures Three correctness fixes on top of the dispatch-before-commit invariant: - ralph-loop-event-handler.ts: after idleSettleMs, also require state ownership and non-verification-pending to match the event source before dispatching. Applied to both the session.idle and session.error retry paths. - verification-failure-handler.ts: if incrementIteration fails after a successful continuation injection, clear the loop state and emit a warning toast instead of returning success. - session-reset-strategy.ts: catch thrown session.create errors so they route through the typed session_creation_rejected path instead of surfacing as an unhandled rejection. --- .../ralph-loop/ralph-loop-event-handler.ts | 22 +++++++++++++ .../ralph-loop/session-reset-strategy.ts | 32 ++++++++++++------- .../verification-failure-handler.ts | 8 +++++ 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/src/hooks/ralph-loop/ralph-loop-event-handler.ts b/src/hooks/ralph-loop/ralph-loop-event-handler.ts index 3af7c1a3a..167454b58 100644 --- a/src/hooks/ralph-loop/ralph-loop-event-handler.ts +++ b/src/hooks/ralph-loop/ralph-loop-event-handler.ts @@ -278,6 +278,17 @@ export function createRalphLoopEventHandler( if (!stateAfterSettle || !stateAfterSettle.active) { return } + if (stateAfterSettle.session_id !== undefined && stateAfterSettle.session_id !== sessionID) { + log(`[${HOOK_NAME}] Skipped: state rebound during settle window`, { + sessionID, + currentOwner: stateAfterSettle.session_id, + }) + return + } + if (stateAfterSettle.verification_pending) { + log(`[${HOOK_NAME}] Skipped: state entered verification_pending during settle window`, { sessionID }) + return + } const nextIteration = stateAfterSettle.iteration + 1 const previewState: RalphLoopState = { ...stateAfterSettle, iteration: nextIteration } @@ -401,6 +412,17 @@ export function createRalphLoopEventHandler( if (!stateAfterSettle || !stateAfterSettle.active) { return } + if (stateAfterSettle.session_id !== undefined && stateAfterSettle.session_id !== sessionID) { + log(`[${HOOK_NAME}] Skipped: state rebound during settle window`, { + sessionID, + currentOwner: stateAfterSettle.session_id, + }) + return + } + if (stateAfterSettle.verification_pending) { + log(`[${HOOK_NAME}] Skipped: state entered verification_pending during settle window`, { sessionID }) + return + } const nextIteration = stateAfterSettle.iteration + 1 const previewState: RalphLoopState = { ...stateAfterSettle, iteration: nextIteration } diff --git a/src/hooks/ralph-loop/session-reset-strategy.ts b/src/hooks/ralph-loop/session-reset-strategy.ts index d6854727d..bf8d3b5af 100644 --- a/src/hooks/ralph-loop/session-reset-strategy.ts +++ b/src/hooks/ralph-loop/session-reset-strategy.ts @@ -7,23 +7,31 @@ export async function createIterationSession( parentSessionID: string, directory: string, ): Promise { - const createResult = await ctx.client.session.create({ - body: { - parentID: parentSessionID, - title: "Ralph Loop Iteration", - }, - query: { directory }, - }) + try { + const createResult = await ctx.client.session.create({ + body: { + parentID: parentSessionID, + title: "Ralph Loop Iteration", + }, + query: { directory }, + }) - if (createResult.error || !createResult.data?.id) { - log("[ralph-loop] Failed to create iteration session", { + if (createResult.error || !createResult.data?.id) { + log("[ralph-loop] Failed to create iteration session", { + parentSessionID, + error: String(createResult.error ?? "No session ID returned"), + }) + return null + } + + return createResult.data.id + } catch (error: unknown) { + log("[ralph-loop] session.create threw during iteration session creation", { parentSessionID, - error: String(createResult.error ?? "No session ID returned"), + error: String(error), }) return null } - - return createResult.data.id } export async function selectSessionInTui( diff --git a/src/hooks/ralph-loop/verification-failure-handler.ts b/src/hooks/ralph-loop/verification-failure-handler.ts index 6b8e2d2cb..89917f033 100644 --- a/src/hooks/ralph-loop/verification-failure-handler.ts +++ b/src/hooks/ralph-loop/verification-failure-handler.ts @@ -122,6 +122,14 @@ export async function handleFailedVerification( const committed = loopState.incrementIteration() if (!committed) { log(`[${HOOK_NAME}] Failed to commit iteration after verification restart`, { parentSessionID }) + loopState.clear() + showToastBestEffort(ctx, { + title: "Ralph Loop Failed", + message: "Verification continuation dispatched but iteration commit failed", + variant: "warning", + duration: 5000, + }) + return false } await ctx.client.tui?.showToast?.({