fix(ulw-loop): add fallback for Oracle verification session tracking

The verification_session_id was never reliably set because the
prompt-based attempt_id matching in tool-execute-after depends on
metadata.prompt surviving the delegate-task execution chain. When
this fails silently, the loop never detects Oracle's VERIFIED
emission.

Add a fallback: when exact attempt_id matching fails but oracle
agent + verification_pending state match, still set the session ID.
Add diagnostic logging to trace verification flow failures.
Add integration test covering the full verification chain.
This commit is contained in:
YeonGyu-Kim
2026-03-17 16:20:27 +09:00
parent 78dac0642e
commit 4bc7b1d27c
5 changed files with 220 additions and 44 deletions
@@ -1,11 +1,96 @@
import type { PluginInput } from "@opencode-ai/plugin"
import { log } from "../../shared/logger"
import { HOOK_NAME } from "./constants"
import { ULTRAWORK_VERIFICATION_PROMISE } from "./constants"
import type { RalphLoopState } from "./types"
import { handleFailedVerification } from "./verification-failure-handler"
import { withTimeout } from "./with-timeout"
type OpenCodeSessionMessage = {
info?: { role?: string }
parts?: Array<{ type?: string; text?: string }>
}
const ORACLE_AGENT_PATTERN = /Agent:\s*oracle/i
const TASK_METADATA_SESSION_PATTERN = /<task_metadata>[\s\S]*?session_id:\s*([^\s<]+)[\s\S]*?<\/task_metadata>/i
const VERIFIED_PROMISE_PATTERN = new RegExp(
`<promise>\\s*${ULTRAWORK_VERIFICATION_PROMISE}\\s*<\\/promise>`,
"i",
)
function collectAssistantText(message: OpenCodeSessionMessage): string {
if (!Array.isArray(message.parts)) {
return ""
}
let text = ""
for (const part of message.parts) {
if (part.type !== "text") {
continue
}
text += `${text ? "\n" : ""}${part.text ?? ""}`
}
return text
}
async function detectOracleVerificationFromParentSession(
ctx: PluginInput,
parentSessionID: string,
directory: string,
apiTimeoutMs: number,
): Promise<string | undefined> {
try {
const response = await withTimeout(
ctx.client.session.messages({
path: { id: parentSessionID },
query: { directory },
}),
apiTimeoutMs,
)
const messagesResponse: unknown = response
const responseData =
typeof messagesResponse === "object" && messagesResponse !== null && "data" in messagesResponse
? (messagesResponse as { data?: unknown }).data
: undefined
const messageArray: unknown[] = Array.isArray(messagesResponse)
? messagesResponse
: Array.isArray(responseData)
? responseData
: []
for (let index = messageArray.length - 1; index >= 0; index -= 1) {
const message = messageArray[index] as OpenCodeSessionMessage
if (message.info?.role !== "assistant") {
continue
}
const assistantText = collectAssistantText(message)
if (!VERIFIED_PROMISE_PATTERN.test(assistantText) || !ORACLE_AGENT_PATTERN.test(assistantText)) {
continue
}
const sessionMatch = assistantText.match(TASK_METADATA_SESSION_PATTERN)
const detectedOracleSessionID = sessionMatch?.[1]?.trim()
if (detectedOracleSessionID) {
return detectedOracleSessionID
}
}
return undefined
} catch (error) {
log(`[${HOOK_NAME}] Failed to scan parent session for oracle verification evidence`, {
parentSessionID,
error: String(error),
})
return undefined
}
}
type LoopStateController = {
restartAfterFailedVerification: (sessionID: string, messageCountAtStart?: number) => RalphLoopState | null
setVerificationSessionID: (sessionID: string, verificationSessionID: string) => RalphLoopState | null
}
export async function handlePendingVerification(
@@ -33,6 +118,29 @@ export async function handlePendingVerification(
} = input
if (matchesParentSession || (verificationSessionID && matchesVerificationSession)) {
if (!verificationSessionID && state.session_id) {
const recoveredVerificationSessionID = await detectOracleVerificationFromParentSession(
ctx,
state.session_id,
directory,
apiTimeoutMs,
)
if (recoveredVerificationSessionID) {
const updatedState = loopState.setVerificationSessionID(
state.session_id,
recoveredVerificationSessionID,
)
if (updatedState) {
log(`[${HOOK_NAME}] Recovered missing verification session from parent evidence`, {
parentSessionID: state.session_id,
recoveredVerificationSessionID,
})
return
}
}
}
const restarted = await handleFailedVerification(ctx, {
state,
loopState,
@@ -136,6 +136,13 @@ export function createRalphLoopEventHandler(
}
if (state.verification_pending) {
if (!verificationSessionID && matchesParentSession) {
log(`[${HOOK_NAME}] Verification pending without tracked oracle session, running recovery check`, {
sessionID,
iteration: state.iteration,
})
}
await handlePendingVerification(ctx, {
sessionID,
state,