fix(ralph-loop): detect completion promise from session messages API (#413)

* fix(ralph-loop): detect completion promise from session messages API

The completion promise (e.g., <promise>DONE</promise>) was not being detected
because assistant text messages were never recorded to the transcript file.
Only user messages, tool uses, and tool results were recorded.

This fix adds a new detection method that fetches session messages via the
OpenCode API and checks assistant text messages for the completion promise.
The transcript file check is kept as a fallback.

Fixes #412

* refactor(ralph-loop): address review feedback

- Simplify response parsing to use response.data consistently (greptile)
- Add session ID tracking to messages mock for better test coverage (cubic)
- Add assertion to verify correct session ID is passed to API

---------

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
This commit is contained in:
Sisyphus
2026-01-02 16:37:04 +09:00
committed by GitHub
parent 5efbfa6d17
commit 3448e98707
2 changed files with 93 additions and 4 deletions
+55 -3
View File
@@ -18,6 +18,17 @@ interface SessionState {
isRecovering?: boolean
}
interface OpenCodeSessionMessage {
info?: {
role?: string
}
parts?: Array<{
type: string
text?: string
[key: string]: unknown
}>
}
const CONTINUATION_PROMPT = `[RALPH LOOP - ITERATION {{ITERATION}}/{{MAX}}]
Your previous attempt did not output the completion promise. Continue working on the task.
@@ -81,6 +92,41 @@ export function createRalphLoopHook(
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
}
async function detectCompletionInSessionMessages(
sessionID: string,
promise: string
): Promise<boolean> {
try {
const response = await ctx.client.session.messages({
path: { id: sessionID },
query: { directory: ctx.directory },
})
const messages = (response as { data?: unknown[] }).data ?? []
if (!Array.isArray(messages)) return false
const pattern = new RegExp(`<promise>\\s*${escapeRegex(promise)}\\s*</promise>`, "is")
for (const msg of messages as OpenCodeSessionMessage[]) {
if (msg.info?.role !== "assistant") continue
for (const part of msg.parts || []) {
if (part.type === "text" && part.text) {
if (pattern.test(part.text)) {
return true
}
}
}
}
return false
} catch (err) {
log(`[${HOOK_NAME}] Failed to fetch session messages`, { sessionID, error: String(err) })
return false
}
}
const startLoop = (
sessionID: string,
prompt: string,
@@ -151,14 +197,20 @@ export function createRalphLoopHook(
return
}
// Generate transcript path from sessionID - OpenCode doesn't pass it in event properties
const transcriptPath = getTranscriptPath(sessionID)
const completionDetectedViaApi = await detectCompletionInSessionMessages(
sessionID,
state.completion_promise
)
if (detectCompletionPromise(transcriptPath, state.completion_promise)) {
const transcriptPath = getTranscriptPath(sessionID)
const completionDetectedViaTranscript = detectCompletionPromise(transcriptPath, state.completion_promise)
if (completionDetectedViaApi || completionDetectedViaTranscript) {
log(`[${HOOK_NAME}] Completion detected!`, {
sessionID,
iteration: state.iteration,
promise: state.completion_promise,
detectedVia: completionDetectedViaApi ? "session_messages_api" : "transcript_file",
})
clearState(ctx.directory, stateDir)