diff --git a/src/features/builtin-commands/templates/ralph-loop.ts b/src/features/builtin-commands/templates/ralph-loop.ts index 1540aea82..5da026a70 100644 --- a/src/features/builtin-commands/templates/ralph-loop.ts +++ b/src/features/builtin-commands/templates/ralph-loop.ts @@ -35,7 +35,7 @@ export const ULW_LOOP_TEMPLATE = `You are starting an ULTRAWORK Loop - a self-re 1. You will work on the task continuously 2. When you believe the work is complete, output: \`{{COMPLETION_PROMISE}}\` 3. That does NOT finish the loop yet. The system will require Oracle verification -4. The loop only ends after Oracle verifies the result with \`VERIFIED\` +4. The loop only ends after the system confirms Oracle verified the result 5. There is no iteration limit ## Rules @@ -46,7 +46,7 @@ export const ULW_LOOP_TEMPLATE = `You are starting an ULTRAWORK Loop - a self-re ## Exit Conditions -1. **Verified Completion**: Oracle returns \`VERIFIED\` +1. **Verified Completion**: Oracle verifies the result and the system confirms it 2. **Cancel**: User runs \`/cancel-ralph\` ## Your Task diff --git a/src/hooks/ralph-loop/continuation-prompt-builder.ts b/src/hooks/ralph-loop/continuation-prompt-builder.ts index 60b1cf244..ede8561f5 100644 --- a/src/hooks/ralph-loop/continuation-prompt-builder.ts +++ b/src/hooks/ralph-loop/continuation-prompt-builder.ts @@ -25,7 +25,7 @@ You already emitted {{INITIAL_PROMISE}}. This does NOT finish REQUIRED NOW: - Call Oracle using task(subagent_type="oracle", load_skills=[], run_in_background=false, ...) - Ask Oracle to verify whether the original task is actually complete -- The loop only finishes when Oracle returns {{PROMISE}} +- The system will inspect the Oracle session directly for the verification result - If Oracle does not verify, continue fixing the task and do not consider it complete Original task: diff --git a/src/hooks/ralph-loop/loop-state-controller.ts b/src/hooks/ralph-loop/loop-state-controller.ts index 93045cf98..e1836aee8 100644 --- a/src/hooks/ralph-loop/loop-state-controller.ts +++ b/src/hooks/ralph-loop/loop-state-controller.ts @@ -43,6 +43,8 @@ export function createLoopStateController(options: { message_count_at_start: loopOptions?.messageCountAtStart, completion_promise: initialCompletionPromise, initial_completion_promise: initialCompletionPromise, + verification_attempt_id: undefined, + verification_session_id: undefined, ultrawork: loopOptions?.ultrawork, verification_pending: undefined, strategy: loopOptions?.strategy ?? config?.default_strategy ?? "continue", @@ -123,6 +125,8 @@ export function createLoopStateController(options: { state.verification_pending = true state.completion_promise = ULTRAWORK_VERIFICATION_PROMISE + state.verification_attempt_id = undefined + state.verification_session_id = undefined state.initial_completion_promise ??= DEFAULT_COMPLETION_PROMISE if (!writeState(directory, state, stateDir)) { @@ -131,5 +135,20 @@ export function createLoopStateController(options: { return state }, + + setVerificationSessionID(sessionID: string, verificationSessionID: string): RalphLoopState | null { + const state = readState(directory, stateDir) + if (!state || state.session_id !== sessionID || !state.ultrawork || !state.verification_pending) { + return null + } + + state.verification_session_id = verificationSessionID + + if (!writeState(directory, state, stateDir)) { + return null + } + + return state + }, } } diff --git a/src/hooks/ralph-loop/ralph-loop-event-handler.ts b/src/hooks/ralph-loop/ralph-loop-event-handler.ts index 8eefb6b9b..6861d051e 100644 --- a/src/hooks/ralph-loop/ralph-loop-event-handler.ts +++ b/src/hooks/ralph-loop/ralph-loop-event-handler.ts @@ -21,6 +21,7 @@ type LoopStateController = { incrementIteration: () => RalphLoopState | null setSessionID: (sessionID: string) => RalphLoopState | null markVerificationPending: (sessionID: string) => RalphLoopState | null + setVerificationSessionID: (sessionID: string, verificationSessionID: string) => RalphLoopState | null } type RalphLoopEventHandlerOptions = { directory: string; apiTimeoutMs: number; getTranscriptPath: (sessionID: string) => string | undefined; checkSessionExists?: RalphLoopOptions["checkSessionExists"]; sessionRecovery: SessionRecovery; loopState: LoopStateController } @@ -78,14 +79,30 @@ export function createRalphLoopEventHandler( return } - const transcriptPath = options.getTranscriptPath(sessionID) - const completionViaTranscript = detectCompletionInTranscript( - transcriptPath, - state.completion_promise, - state.started_at, - ) + const verificationSessionID = state.verification_pending + ? state.verification_session_id + : undefined + const completionSessionID = verificationSessionID ?? (state.verification_pending ? undefined : sessionID) + const transcriptPath = completionSessionID ? options.getTranscriptPath(completionSessionID) : undefined + const completionViaTranscript = completionSessionID + ? detectCompletionInTranscript( + transcriptPath, + state.completion_promise, + state.started_at, + ) + : false const completionViaApi = completionViaTranscript ? false + : verificationSessionID + ? await detectCompletionInSessionMessages(ctx, { + sessionID: verificationSessionID, + promise: state.completion_promise, + apiTimeoutMs: options.apiTimeoutMs, + directory: options.directory, + sinceMessageIndex: undefined, + }) + : state.verification_pending + ? false : await detectCompletionInSessionMessages(ctx, { sessionID, promise: state.completion_promise, diff --git a/src/hooks/ralph-loop/storage.ts b/src/hooks/ralph-loop/storage.ts index 824b6ecb8..309151a43 100644 --- a/src/hooks/ralph-loop/storage.ts +++ b/src/hooks/ralph-loop/storage.ts @@ -62,6 +62,12 @@ export function readState(directory: string, customPath?: string): RalphLoopStat initial_completion_promise: data.initial_completion_promise ? stripQuotes(data.initial_completion_promise) : undefined, + verification_attempt_id: data.verification_attempt_id + ? stripQuotes(data.verification_attempt_id) + : undefined, + verification_session_id: data.verification_session_id + ? stripQuotes(data.verification_session_id) + : undefined, started_at: stripQuotes(data.started_at) || new Date().toISOString(), prompt: body.trim(), session_id: data.session_id ? stripQuotes(data.session_id) : undefined, @@ -100,6 +106,12 @@ export function writeState( const initialCompletionPromiseLine = state.initial_completion_promise ? `initial_completion_promise: "${state.initial_completion_promise}"\n` : "" + const verificationAttemptLine = state.verification_attempt_id + ? `verification_attempt_id: "${state.verification_attempt_id}"\n` + : "" + const verificationSessionLine = state.verification_session_id + ? `verification_session_id: "${state.verification_session_id}"\n` + : "" const messageCountAtStartLine = typeof state.message_count_at_start === "number" ? `message_count_at_start: ${state.message_count_at_start}\n` @@ -112,7 +124,7 @@ export function writeState( active: ${state.active} iteration: ${state.iteration} ${maxIterationsLine}completion_promise: "${state.completion_promise}" -${initialCompletionPromiseLine}started_at: "${state.started_at}" +${initialCompletionPromiseLine}${verificationAttemptLine}${verificationSessionLine}started_at: "${state.started_at}" ${sessionIdLine}${ultraworkLine}${verificationPendingLine}${strategyLine}${messageCountAtStartLine}--- ${state.prompt} ` diff --git a/src/hooks/ralph-loop/types.ts b/src/hooks/ralph-loop/types.ts index 42a5fc695..0c19a1f9b 100644 --- a/src/hooks/ralph-loop/types.ts +++ b/src/hooks/ralph-loop/types.ts @@ -7,6 +7,8 @@ export interface RalphLoopState { message_count_at_start?: number completion_promise: string initial_completion_promise?: string + verification_attempt_id?: string + verification_session_id?: string started_at: string prompt: string session_id?: string diff --git a/src/hooks/ralph-loop/ulw-loop-verification.test.ts b/src/hooks/ralph-loop/ulw-loop-verification.test.ts index eb90eaec2..4506bee04 100644 --- a/src/hooks/ralph-loop/ulw-loop-verification.test.ts +++ b/src/hooks/ralph-loop/ulw-loop-verification.test.ts @@ -4,13 +4,14 @@ import { tmpdir } from "node:os" import { join } from "node:path" import { createRalphLoopHook } from "./index" import { ULTRAWORK_VERIFICATION_PROMISE } from "./constants" -import { clearState } from "./storage" +import { clearState, writeState } from "./storage" describe("ulw-loop verification", () => { const testDir = join(tmpdir(), `ulw-loop-verification-${Date.now()}`) let promptCalls: Array<{ sessionID: string; text: string }> let toastCalls: Array<{ title: string; message: string; variant: string }> - let transcriptPath: string + let parentTranscriptPath: string + let oracleTranscriptPath: string function createMockPluginInput() { return { @@ -39,7 +40,8 @@ describe("ulw-loop verification", () => { beforeEach(() => { promptCalls = [] toastCalls = [] - transcriptPath = join(testDir, "transcript.jsonl") + parentTranscriptPath = join(testDir, "transcript-parent.jsonl") + oracleTranscriptPath = join(testDir, "transcript-oracle.jsonl") if (!existsSync(testDir)) { mkdirSync(testDir, { recursive: true }) @@ -57,11 +59,11 @@ describe("ulw-loop verification", () => { test("#given ulw loop emits DONE #when idle fires #then verification phase starts instead of completing", async () => { const hook = createRalphLoopHook(createMockPluginInput(), { - getTranscriptPath: () => transcriptPath, + getTranscriptPath: (sessionID) => sessionID === "ses-oracle" ? oracleTranscriptPath : parentTranscriptPath, }) hook.startLoop("session-123", "Build API", { ultrawork: true }) writeFileSync( - transcriptPath, + parentTranscriptPath, `${JSON.stringify({ type: "tool_result", timestamp: new Date().toISOString(), tool_output: { output: "done DONE" } })}\n`, ) @@ -69,25 +71,30 @@ describe("ulw-loop verification", () => { expect(hook.getState()?.verification_pending).toBe(true) expect(hook.getState()?.completion_promise).toBe(ULTRAWORK_VERIFICATION_PROMISE) + expect(hook.getState()?.verification_session_id).toBeUndefined() expect(promptCalls).toHaveLength(1) expect(promptCalls[0].text).toContain('task(subagent_type="oracle"') expect(toastCalls.some((toast) => toast.title === "ULTRAWORK LOOP COMPLETE!")).toBe(false) }) - test("#given ulw loop is awaiting verification #when VERIFIED appears #then loop completes", async () => { + test("#given ulw loop is awaiting verification #when VERIFIED appears in oracle session #then loop completes", async () => { const hook = createRalphLoopHook(createMockPluginInput(), { - getTranscriptPath: () => transcriptPath, + getTranscriptPath: (sessionID) => sessionID === "ses-oracle" ? oracleTranscriptPath : parentTranscriptPath, }) hook.startLoop("session-123", "Build API", { ultrawork: true }) writeFileSync( - transcriptPath, + parentTranscriptPath, `${JSON.stringify({ type: "tool_result", timestamp: new Date().toISOString(), tool_output: { output: "done DONE" } })}\n`, ) await hook.event({ event: { type: "session.idle", properties: { sessionID: "session-123" } } }) + writeState(testDir, { + ...hook.getState()!, + verification_session_id: "ses-oracle", + }) writeFileSync( - transcriptPath, - `${JSON.stringify({ type: "tool_result", timestamp: new Date().toISOString(), tool_output: { output: "done DONE" } })}\n${JSON.stringify({ type: "tool_result", timestamp: new Date().toISOString(), tool_output: { output: `verified ${ULTRAWORK_VERIFICATION_PROMISE}` } })}\n`, + oracleTranscriptPath, + `${JSON.stringify({ type: "tool_result", timestamp: new Date().toISOString(), tool_output: { output: `verified ${ULTRAWORK_VERIFICATION_PROMISE}` } })}\n`, ) await hook.event({ event: { type: "session.idle", properties: { sessionID: "session-123" } } }) @@ -98,7 +105,7 @@ describe("ulw-loop verification", () => { test("#given ulw loop without max iterations #when it continues #then it stays unbounded", async () => { const hook = createRalphLoopHook(createMockPluginInput(), { - getTranscriptPath: () => transcriptPath, + getTranscriptPath: (sessionID) => sessionID === "ses-oracle" ? oracleTranscriptPath : parentTranscriptPath, }) hook.startLoop("session-123", "Build API", { ultrawork: true }) @@ -111,11 +118,11 @@ describe("ulw-loop verification", () => { test("#given prior transcript completion from older run #when new ulw loop starts #then old completion is ignored", async () => { writeFileSync( - transcriptPath, + parentTranscriptPath, `${JSON.stringify({ type: "tool_result", timestamp: "2000-01-01T00:00:00.000Z", tool_output: { output: "old DONE" } })}\n`, ) const hook = createRalphLoopHook(createMockPluginInput(), { - getTranscriptPath: () => transcriptPath, + getTranscriptPath: (sessionID) => sessionID === "ses-oracle" ? oracleTranscriptPath : parentTranscriptPath, }) hook.startLoop("session-123", "Build API", { ultrawork: true }) @@ -128,11 +135,11 @@ describe("ulw-loop verification", () => { test("#given ulw loop was awaiting verification #when same session starts again #then verification state is overwritten", async () => { const hook = createRalphLoopHook(createMockPluginInput(), { - getTranscriptPath: () => transcriptPath, + getTranscriptPath: (sessionID) => sessionID === "ses-oracle" ? oracleTranscriptPath : parentTranscriptPath, }) hook.startLoop("session-123", "Build API", { ultrawork: true }) writeFileSync( - transcriptPath, + parentTranscriptPath, `${JSON.stringify({ type: "tool_result", timestamp: new Date().toISOString(), tool_output: { output: "done DONE" } })}\n`, ) @@ -143,4 +150,26 @@ describe("ulw-loop verification", () => { expect(hook.getState()?.verification_pending).toBeUndefined() expect(hook.getState()?.completion_promise).toBe("DONE") }) + + test("#given parent session emits VERIFIED #when oracle session is not tracked #then ulw loop does not complete", async () => { + const hook = createRalphLoopHook(createMockPluginInput(), { + getTranscriptPath: (sessionID) => sessionID === "ses-oracle" ? oracleTranscriptPath : parentTranscriptPath, + }) + hook.startLoop("session-123", "Build API", { ultrawork: true }) + writeFileSync( + parentTranscriptPath, + `${JSON.stringify({ type: "tool_result", timestamp: new Date().toISOString(), tool_output: { output: "done DONE" } })}\n`, + ) + + await hook.event({ event: { type: "session.idle", properties: { sessionID: "session-123" } } }) + writeFileSync( + parentTranscriptPath, + `${JSON.stringify({ type: "tool_result", timestamp: new Date().toISOString(), tool_output: { output: "done DONE" } })}\n${JSON.stringify({ type: "tool_result", timestamp: new Date().toISOString(), tool_output: { output: `bad parent leak ${ULTRAWORK_VERIFICATION_PROMISE}` } })}\n`, + ) + + await hook.event({ event: { type: "session.idle", properties: { sessionID: "session-123" } } }) + + expect(hook.getState()).not.toBeNull() + expect(hook.getState()?.verification_pending).toBe(true) + }) }) diff --git a/src/plugin-interface.ts b/src/plugin-interface.ts index 634b08d2e..0cff2ee7c 100644 --- a/src/plugin-interface.ts +++ b/src/plugin-interface.ts @@ -68,6 +68,7 @@ export function createPluginInterface(args: { }), "tool.execute.after": createToolExecuteAfterHandler({ + ctx, hooks, }), } diff --git a/src/plugin/tool-execute-after.ts b/src/plugin/tool-execute-after.ts index 58717c9bb..4dfeda03c 100644 --- a/src/plugin/tool-execute-after.ts +++ b/src/plugin/tool-execute-after.ts @@ -1,7 +1,12 @@ import { consumeToolMetadata } from "../features/tool-metadata-store" import type { CreatedHooks } from "../create-hooks" +import type { PluginContext } from "./types" +import { readState, writeState } from "../hooks/ralph-loop/storage" + +const VERIFICATION_ATTEMPT_PATTERN = /(.*?)<\/ulw_verification_attempt_id>/i export function createToolExecuteAfterHandler(args: { + ctx: PluginContext hooks: CreatedHooks }): ( input: { tool: string; sessionID: string; callID: string }, @@ -9,7 +14,7 @@ export function createToolExecuteAfterHandler(args: { | { title: string; output: string; metadata: Record } | undefined, ) => Promise { - const { hooks } = args + const { ctx, hooks } = args return async ( input: { tool: string; sessionID: string; callID: string }, @@ -27,6 +32,30 @@ export function createToolExecuteAfterHandler(args: { } } + if (input.tool === "task") { + const sessionId = typeof output.metadata?.sessionId === "string" ? output.metadata.sessionId : undefined + const agent = typeof output.metadata?.agent === "string" ? output.metadata.agent : undefined + const prompt = typeof output.metadata?.prompt === "string" ? output.metadata.prompt : undefined + const verificationAttemptId = prompt?.match(VERIFICATION_ATTEMPT_PATTERN)?.[1]?.trim() + const loopState = readState(ctx.directory) + + if ( + agent === "oracle" + && sessionId + && verificationAttemptId + && loopState?.active === true + && loopState.ultrawork === true + && loopState.verification_pending === true + && loopState.session_id === input.sessionID + && loopState.verification_attempt_id === verificationAttemptId + ) { + writeState(ctx.directory, { + ...loopState, + verification_session_id: sessionId, + }) + } + } + await hooks.claudeCodeHooks?.["tool.execute.after"]?.(input, output) await hooks.toolOutputTruncator?.["tool.execute.after"]?.(input, output) await hooks.preemptiveCompaction?.["tool.execute.after"]?.(input, output) diff --git a/src/plugin/tool-execute-before.ts b/src/plugin/tool-execute-before.ts index 44cd2b248..000b121dd 100644 --- a/src/plugin/tool-execute-before.ts +++ b/src/plugin/tool-execute-before.ts @@ -1,4 +1,5 @@ import type { PluginContext } from "./types" +import { randomUUID } from "node:crypto" import { getMainSessionID } from "../features/claude-code-session-state" import { clearBoulderState } from "../features/boulder-state" @@ -6,7 +7,7 @@ import { log } from "../shared" import { resolveSessionAgent } from "./session-agent-resolver" import { parseRalphLoopArguments } from "../hooks/ralph-loop/command-arguments" import { ULTRAWORK_VERIFICATION_PROMISE } from "../hooks/ralph-loop/constants" -import { readState } from "../hooks/ralph-loop/storage" +import { readState, writeState } from "../hooks/ralph-loop/storage" import type { CreatedHooks } from "../create-hooks" @@ -77,8 +78,14 @@ export function createToolExecuteBeforeHandler(args: { && loopState.session_id === input.sessionID if (shouldInjectOracleVerification) { + const verificationAttemptId = randomUUID() + writeState(ctx.directory, { + ...loopState, + verification_attempt_id: verificationAttemptId, + verification_session_id: undefined, + }) argsObject.run_in_background = false - argsObject.prompt = `${prompt ? `${prompt}\n\n` : ""}You are verifying the active ULTRAWORK loop result for this session. Review whether the original task is truly complete: ${loopState.prompt}\n\nIf the work is fully complete, end your response with ${ULTRAWORK_VERIFICATION_PROMISE}. If the work is not complete, explain the blocking issues clearly and DO NOT emit that promise.` + argsObject.prompt = `${prompt ? `${prompt}\n\n` : ""}You are verifying the active ULTRAWORK loop result for this session. Review whether the original task is truly complete: ${loopState.prompt}\n\nIf the work is fully complete, end your response with ${ULTRAWORK_VERIFICATION_PROMISE}. If the work is not complete, explain the blocking issues clearly and DO NOT emit that promise.\n\n${verificationAttemptId}` } } diff --git a/src/plugin/tool-execute-before.ulw-loop.test.ts b/src/plugin/tool-execute-before.ulw-loop.test.ts index c5606821d..aef5b36d2 100644 --- a/src/plugin/tool-execute-before.ulw-loop.test.ts +++ b/src/plugin/tool-execute-before.ulw-loop.test.ts @@ -2,9 +2,10 @@ import { describe, expect, test } from "bun:test" import { mkdirSync, rmSync } from "node:fs" import { tmpdir } from "node:os" import { join } from "node:path" +import { createToolExecuteAfterHandler } from "./tool-execute-after" import { createToolExecuteBeforeHandler } from "./tool-execute-before" import { ULTRAWORK_VERIFICATION_PROMISE } from "../hooks/ralph-loop/constants" -import { clearState, writeState } from "../hooks/ralph-loop/storage" +import { clearState, readState, writeState } from "../hooks/ralph-loop/storage" describe("tool.execute.before ultrawork oracle verification", () => { function createCtx(directory: string) { @@ -47,6 +48,7 @@ describe("tool.execute.before ultrawork oracle verification", () => { await handler({ tool: "task", sessionID: "ses-main", callID: "call-1" }, output) + expect(readState(directory)?.verification_attempt_id).toBeTruthy() expect(output.args.run_in_background).toBe(false) expect(output.args.prompt).toContain("Ship feature") expect(output.args.prompt).toContain(`${ULTRAWORK_VERIFICATION_PROMISE}`) @@ -77,4 +79,138 @@ describe("tool.execute.before ultrawork oracle verification", () => { rmSync(directory, { recursive: true, force: true }) }) + + test("#given ulw loop is awaiting verification #when oracle task finishes #then oracle session id is stored", async () => { + const directory = join(tmpdir(), `tool-after-ulw-${Date.now()}`) + mkdirSync(directory, { recursive: true }) + writeState(directory, { + active: true, + iteration: 3, + completion_promise: ULTRAWORK_VERIFICATION_PROMISE, + initial_completion_promise: "DONE", + started_at: new Date().toISOString(), + prompt: "Ship feature", + session_id: "ses-main", + ultrawork: true, + verification_pending: true, + }) + + const beforeHandler = createToolExecuteBeforeHandler({ + ctx: createCtx(directory) as unknown as Parameters[0]["ctx"], + hooks: {} as Parameters[0]["hooks"], + }) + const beforeOutput = { + args: { + subagent_type: "oracle", + run_in_background: true, + prompt: "Check it", + } as Record, + } + await beforeHandler({ tool: "task", sessionID: "ses-main", callID: "call-1" }, beforeOutput) + + const handler = createToolExecuteAfterHandler({ + ctx: createCtx(directory) as unknown as Parameters[0]["ctx"], + hooks: {} as Parameters[0]["hooks"], + }) + + await handler( + { tool: "task", sessionID: "ses-main", callID: "call-1" }, + { + title: "oracle task", + output: "done", + metadata: { + agent: "oracle", + prompt: String(beforeOutput.args.prompt), + sessionId: "ses-oracle", + }, + }, + ) + + expect(readState(directory)?.verification_session_id).toBe("ses-oracle") + + clearState(directory) + rmSync(directory, { recursive: true, force: true }) + }) + + test("#given newer oracle attempt exists #when older oracle task finishes #then old session does not overwrite active verification", async () => { + const directory = join(tmpdir(), `tool-race-ulw-${Date.now()}`) + mkdirSync(directory, { recursive: true }) + writeState(directory, { + active: true, + iteration: 3, + completion_promise: ULTRAWORK_VERIFICATION_PROMISE, + initial_completion_promise: "DONE", + started_at: new Date().toISOString(), + prompt: "Ship feature", + session_id: "ses-main", + ultrawork: true, + verification_pending: true, + }) + + const beforeHandler = createToolExecuteBeforeHandler({ + ctx: createCtx(directory) as unknown as Parameters[0]["ctx"], + hooks: {} as Parameters[0]["hooks"], + }) + const afterHandler = createToolExecuteAfterHandler({ + ctx: createCtx(directory) as unknown as Parameters[0]["ctx"], + hooks: {} as Parameters[0]["hooks"], + }) + + const firstOutput = { + args: { + subagent_type: "oracle", + run_in_background: true, + prompt: "Check it", + } as Record, + } + await beforeHandler({ tool: "task", sessionID: "ses-main", callID: "call-1" }, firstOutput) + const firstAttemptId = readState(directory)?.verification_attempt_id + + const secondOutput = { + args: { + subagent_type: "oracle", + run_in_background: true, + prompt: "Check it again", + } as Record, + } + await beforeHandler({ tool: "task", sessionID: "ses-main", callID: "call-2" }, secondOutput) + const secondAttemptId = readState(directory)?.verification_attempt_id + + expect(firstAttemptId).toBeTruthy() + expect(secondAttemptId).toBeTruthy() + expect(secondAttemptId).not.toBe(firstAttemptId) + + await afterHandler( + { tool: "task", sessionID: "ses-main", callID: "call-1" }, + { + title: "oracle task", + output: "done", + metadata: { + agent: "oracle", + prompt: String(firstOutput.args.prompt), + sessionId: "ses-oracle-old", + }, + }, + ) + + expect(readState(directory)?.verification_session_id).toBeUndefined() + + await afterHandler( + { tool: "task", sessionID: "ses-main", callID: "call-2" }, + { + title: "oracle task", + output: "done", + metadata: { + agent: "oracle", + prompt: String(secondOutput.args.prompt), + sessionId: "ses-oracle-new", + }, + }, + ) + + expect(readState(directory)?.verification_session_id).toBe("ses-oracle-new") + + clearState(directory) + rmSync(directory, { recursive: true, force: true }) + }) })