d490b4dc20
Replace fragile regex text matching with structured detection for Oracle verification evidence. - Add oracle-verification-detector.ts with parseOracleVerificationEvidence() - Use structured parsing instead of multiple regex patterns - Add comprehensive test coverage for edge cases - Update completion-promise-detector.ts to use isOracleVerified() - Update pending-verification-handler.ts to use structured extraction Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
166 lines
4.5 KiB
TypeScript
166 lines
4.5 KiB
TypeScript
import type { PluginInput } from "@opencode-ai/plugin"
|
|
import { existsSync, readFileSync } from "node:fs"
|
|
import { log } from "../../shared/logger"
|
|
import { HOOK_NAME } from "./constants"
|
|
import { ULTRAWORK_VERIFICATION_PROMISE } from "./constants"
|
|
import { isOracleVerified } from "./oracle-verification-detector"
|
|
import { withTimeout } from "./with-timeout"
|
|
|
|
interface OpenCodeSessionMessage {
|
|
info?: { role?: string }
|
|
parts?: Array<{ type: string; text?: string }>
|
|
}
|
|
|
|
interface TranscriptEntry {
|
|
type?: string
|
|
timestamp?: string
|
|
content?: string
|
|
tool_output?: { output?: string } | string
|
|
}
|
|
|
|
function extractTranscriptEntryText(entry: TranscriptEntry): string {
|
|
if (typeof entry.content === "string") return entry.content
|
|
if (typeof entry.tool_output === "string") return entry.tool_output
|
|
if (entry.tool_output && typeof entry.tool_output === "object" && typeof entry.tool_output.output === "string") return entry.tool_output.output
|
|
return ""
|
|
}
|
|
|
|
function escapeRegex(str: string): string {
|
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
}
|
|
|
|
function buildPromisePattern(promise: string): RegExp {
|
|
return new RegExp(`<promise>\\s*${escapeRegex(promise)}\\s*</promise>`, "is")
|
|
}
|
|
|
|
function shouldInspectSessionMessagePart(
|
|
partType: string,
|
|
promise: string,
|
|
partText: string,
|
|
): boolean {
|
|
if (partType === "text") {
|
|
return true
|
|
}
|
|
|
|
if (partType !== "tool_result") {
|
|
return false
|
|
}
|
|
|
|
return promise === ULTRAWORK_VERIFICATION_PROMISE && isOracleVerified(partText)
|
|
}
|
|
|
|
function shouldInspectTranscriptEntry(
|
|
entry: TranscriptEntry,
|
|
promise: string,
|
|
entryText: string,
|
|
): boolean {
|
|
if (entry.type === "assistant" || entry.type === "text") {
|
|
return true
|
|
}
|
|
|
|
if (entry.type !== "tool_result") {
|
|
return false
|
|
}
|
|
|
|
return promise === ULTRAWORK_VERIFICATION_PROMISE && isOracleVerified(entryText)
|
|
}
|
|
|
|
export function detectCompletionInTranscript(
|
|
transcriptPath: string | undefined,
|
|
promise: string,
|
|
startedAt?: string,
|
|
): boolean {
|
|
if (!transcriptPath) return false
|
|
|
|
try {
|
|
if (!existsSync(transcriptPath)) return false
|
|
|
|
const content = readFileSync(transcriptPath, "utf-8")
|
|
const pattern = buildPromisePattern(promise)
|
|
const lines = content.split("\n").filter((line: string) => line.trim())
|
|
|
|
for (const line of lines) {
|
|
try {
|
|
const entry = JSON.parse(line) as TranscriptEntry
|
|
if (entry.type === "user") continue
|
|
if (startedAt && entry.timestamp && entry.timestamp < startedAt) continue
|
|
const entryText = extractTranscriptEntryText(entry)
|
|
if (!entryText) continue
|
|
if (!shouldInspectTranscriptEntry(entry, promise, entryText)) continue
|
|
if (pattern.test(entryText)) return true
|
|
} catch {
|
|
continue
|
|
}
|
|
}
|
|
return false
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
export async function detectCompletionInSessionMessages(
|
|
ctx: PluginInput,
|
|
options: {
|
|
sessionID: string
|
|
promise: string
|
|
apiTimeoutMs: number
|
|
directory: string
|
|
sinceMessageIndex?: number
|
|
},
|
|
): Promise<boolean> {
|
|
try {
|
|
const response = await withTimeout(
|
|
ctx.client.session.messages({
|
|
path: { id: options.sessionID },
|
|
query: { directory: options.directory },
|
|
}),
|
|
options.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
|
|
: []
|
|
|
|
const scopedMessages =
|
|
typeof options.sinceMessageIndex === "number" && options.sinceMessageIndex >= 0 && options.sinceMessageIndex < messageArray.length
|
|
? messageArray.slice(options.sinceMessageIndex)
|
|
: messageArray
|
|
|
|
const assistantMessages = (scopedMessages as OpenCodeSessionMessage[]).filter((msg) => msg.info?.role === "assistant")
|
|
if (assistantMessages.length === 0) return false
|
|
|
|
const pattern = buildPromisePattern(options.promise)
|
|
for (let index = assistantMessages.length - 1; index >= 0; index -= 1) {
|
|
const assistant = assistantMessages[index]
|
|
if (!assistant.parts) continue
|
|
|
|
for (const part of assistant.parts) {
|
|
const partText = part.text ?? ""
|
|
if (!partText) continue
|
|
if (!shouldInspectSessionMessagePart(part.type, options.promise, partText)) continue
|
|
if (pattern.test(partText)) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
} catch (err) {
|
|
setTimeout(() => {
|
|
log(`[${HOOK_NAME}] Session messages check failed`, {
|
|
sessionID: options.sessionID,
|
|
error: String(err),
|
|
})
|
|
}, 0)
|
|
return false
|
|
}
|
|
}
|