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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user