fix(runtime-fallback): recognize completion progress events

This commit is contained in:
YeonGyu-Kim
2026-05-20 18:19:26 +09:00
parent 2f5fc7c4e9
commit fbcc1435e2
2 changed files with 112 additions and 1 deletions
@@ -0,0 +1,90 @@
/// <reference path="../../../bun-test.d.ts" />
import { describe, expect, it } from "bun:test"
import { observeEventForWatchdog, type FirstPromptWatchdog } from "./first-prompt-watchdog"
interface RecordedWatchdogCalls {
readonly user: string[]
readonly progress: string[]
readonly terminal: string[]
}
function createRecordingWatchdog(calls: RecordedWatchdogCalls): FirstPromptWatchdog {
return {
onUserMessage(sessionID) {
calls.user.push(sessionID)
},
onAssistantProgress(sessionID) {
calls.progress.push(sessionID)
},
onSessionTerminal(sessionID) {
calls.terminal.push(sessionID)
},
dispose() {},
}
}
function freshCalls(): RecordedWatchdogCalls {
return { user: [], progress: [], terminal: [] }
}
describe("observeEventForWatchdog progress markers", () => {
const sessionID = "session-observed-progress"
for (const [label, marker] of [
["finished boolean", { finished: true }],
["completed boolean", { completed: true }],
["completed time", { time: { completed: 1779267000000 } }],
] as const) {
it(`#given a message.updated assistant event with ${label} #when observed #then onAssistantProgress is called`, () => {
const calls = freshCalls()
observeEventForWatchdog(
{
type: "message.updated",
properties: { info: { sessionID, role: "assistant", ...marker } },
},
createRecordingWatchdog(calls),
)
expect(calls.progress).toEqual([sessionID])
})
}
it("#given a message.updated assistant event with false completion markers and no parts #when observed #then no progress is signalled", () => {
const calls = freshCalls()
observeEventForWatchdog(
{
type: "message.updated",
properties: {
info: {
sessionID,
role: "assistant",
completed: false,
finish: false,
finished: false,
},
parts: [],
},
},
createRecordingWatchdog(calls),
)
expect(calls.progress).toEqual([])
})
for (const [eventType] of [
["session.next.text.delta"],
["session.next.reasoning.delta"],
["session.next.tool.called"],
["session.next.tool.success"],
["session.next.step.ended"],
] as const) {
it(`#given a ${eventType} event with a sessionID #when observed #then onAssistantProgress is called`, () => {
const calls = freshCalls()
observeEventForWatchdog(
{ type: eventType, properties: { sessionID } },
createRecordingWatchdog(calls),
)
expect(calls.progress).toEqual([sessionID])
})
}
})
@@ -11,6 +11,7 @@ import { resolveFallbackBootstrapModel } from "./fallback-bootstrap-model"
import { dispatchFallbackRetry } from "./fallback-retry-dispatcher"
const SOURCE = "first-prompt-watchdog"
const SESSION_NEXT_EVENT_PREFIX = "session.next."
declare function setTimeout(callback: () => void | Promise<void>, delay?: number): RuntimeFallbackTimeout
declare function clearTimeout(timeout: RuntimeFallbackTimeout): void
@@ -29,6 +30,19 @@ const TERMINAL_EVENT_TYPES = new Set([
"session.error",
])
function isCompletionMarker(value: unknown): boolean {
if (typeof value === "boolean") return value
return value !== undefined && value !== null
}
function hasAssistantCompletionMarker(info: Record<string, unknown>): boolean {
const time = isRecord(info.time) ? info.time : undefined
return isCompletionMarker(info.finish)
|| isCompletionMarker(info.finished)
|| isCompletionMarker(info.completed)
|| isCompletionMarker(time?.completed)
}
/**
* Translate an OpenCode session event into the appropriate watchdog signal.
*
@@ -48,6 +62,12 @@ export function observeEventForWatchdog(
const props = isRecord(event.properties) ? event.properties : undefined
if (!props) return
if (event.type.startsWith(SESSION_NEXT_EVENT_PREFIX)) {
const sessionID = resolveSessionEventID(props) ?? resolveMessageEventSessionID(props)
if (sessionID) watchdog.onAssistantProgress(sessionID)
return
}
if (event.type === "message.part.updated" || event.type === "message.part.delta") {
const sessionID = resolveMessageEventSessionID(props)
const part = isRecord(props.part) ? props.part : undefined
@@ -63,6 +83,7 @@ export function observeEventForWatchdog(
if (event.type === "message.updated") {
const info = isRecord(props.info) ? props.info : undefined
if (!info) return
const sessionID = typeof info?.sessionID === "string" ? info.sessionID : undefined
const role = typeof info?.role === "string" ? info.role : undefined
if (!sessionID || !role) return
@@ -76,7 +97,7 @@ export function observeEventForWatchdog(
if (role === "assistant") {
const hasError = info?.error !== undefined
const hasFinish = info?.finish !== undefined
const hasFinish = hasAssistantCompletionMarker(info)
const eventParts = Array.isArray(props.parts) ? props.parts : undefined
const infoParts = Array.isArray(info?.parts) ? info.parts : undefined
const parts = eventParts ?? infoParts ?? []