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.
This commit is contained in:
YeonGyu-Kim
2026-05-11 13:02:16 +09:00
parent 35cab4db10
commit 2e36a92c25
3 changed files with 50 additions and 12 deletions
@@ -278,6 +278,17 @@ export function createRalphLoopEventHandler(
if (!stateAfterSettle || !stateAfterSettle.active) { if (!stateAfterSettle || !stateAfterSettle.active) {
return 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 nextIteration = stateAfterSettle.iteration + 1
const previewState: RalphLoopState = { ...stateAfterSettle, iteration: nextIteration } const previewState: RalphLoopState = { ...stateAfterSettle, iteration: nextIteration }
@@ -401,6 +412,17 @@ export function createRalphLoopEventHandler(
if (!stateAfterSettle || !stateAfterSettle.active) { if (!stateAfterSettle || !stateAfterSettle.active) {
return 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 nextIteration = stateAfterSettle.iteration + 1
const previewState: RalphLoopState = { ...stateAfterSettle, iteration: nextIteration } const previewState: RalphLoopState = { ...stateAfterSettle, iteration: nextIteration }
@@ -7,6 +7,7 @@ export async function createIterationSession(
parentSessionID: string, parentSessionID: string,
directory: string, directory: string,
): Promise<string | null> { ): Promise<string | null> {
try {
const createResult = await ctx.client.session.create({ const createResult = await ctx.client.session.create({
body: { body: {
parentID: parentSessionID, parentID: parentSessionID,
@@ -24,6 +25,13 @@ export async function createIterationSession(
} }
return createResult.data.id return createResult.data.id
} catch (error: unknown) {
log("[ralph-loop] session.create threw during iteration session creation", {
parentSessionID,
error: String(error),
})
return null
}
} }
export async function selectSessionInTui( export async function selectSessionInTui(
@@ -122,6 +122,14 @@ export async function handleFailedVerification(
const committed = loopState.incrementIteration() const committed = loopState.incrementIteration()
if (!committed) { if (!committed) {
log(`[${HOOK_NAME}] Failed to commit iteration after verification restart`, { parentSessionID }) 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?.({ await ctx.client.tui?.showToast?.({