fix(runtime-events): honor OpenCode progress shapes
This commit is contained in:
@@ -101,11 +101,12 @@ export function createContextWindowMonitorHook(
|
|||||||
sessionID?: string
|
sessionID?: string
|
||||||
providerID?: string
|
providerID?: string
|
||||||
modelID?: string
|
modelID?: string
|
||||||
finish?: boolean
|
finish?: unknown
|
||||||
tokens?: TokenInfo
|
tokens?: TokenInfo
|
||||||
} | undefined
|
} | undefined
|
||||||
|
|
||||||
if (!info || info.role !== "assistant" || !info.finish) return
|
const finish = info?.finish
|
||||||
|
if (!info || info.role !== "assistant" || !finish) return
|
||||||
if (isCompactionAgent(info.agent)) return
|
if (isCompactionAgent(info.agent)) return
|
||||||
const sessionID = resolveMessageEventSessionID(props)
|
const sessionID = resolveMessageEventSessionID(props)
|
||||||
if (!sessionID || !info.providerID || !info.tokens) return
|
if (!sessionID || !info.providerID || !info.tokens) return
|
||||||
|
|||||||
@@ -76,13 +76,14 @@ export function createPreemptiveCompactionHook(
|
|||||||
sessionID?: string
|
sessionID?: string
|
||||||
providerID?: string
|
providerID?: string
|
||||||
modelID?: string
|
modelID?: string
|
||||||
finish?: boolean
|
finish?: unknown
|
||||||
tokens?: TokenInfo
|
tokens?: TokenInfo
|
||||||
parts?: unknown
|
parts?: unknown
|
||||||
} | undefined
|
} | undefined
|
||||||
|
|
||||||
const sessionID = resolveMessageEventSessionID(props)
|
const sessionID = resolveMessageEventSessionID(props)
|
||||||
if (!info || info.role !== "assistant" || !info.finish || !sessionID) return
|
const finish = info?.finish
|
||||||
|
if (!info || info.role !== "assistant" || !finish || !sessionID) return
|
||||||
if (isCompactionAgent(info.agent)) return
|
if (isCompactionAgent(info.agent)) return
|
||||||
|
|
||||||
if (info.providerID && info.tokens) {
|
if (info.providerID && info.tokens) {
|
||||||
|
|||||||
@@ -277,6 +277,18 @@ describe("observeEventForWatchdog", () => {
|
|||||||
expect(calls.progress).toEqual([sessionID])
|
expect(calls.progress).toEqual([sessionID])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it.each(assistantProgressParts)("#given a message.part.updated event whose part is type=%s #when observed #then onAssistantProgress is called", (_label: string, part: { readonly type: string; readonly text?: string; readonly id?: string; readonly name?: string; readonly tool_use_id?: string }) => {
|
||||||
|
const calls = freshCalls()
|
||||||
|
observeEventForWatchdog(
|
||||||
|
{
|
||||||
|
type: "message.part.updated",
|
||||||
|
properties: { sessionID, part },
|
||||||
|
},
|
||||||
|
createRecordingWatchdog(calls),
|
||||||
|
)
|
||||||
|
expect(calls.progress).toEqual([sessionID])
|
||||||
|
})
|
||||||
|
|
||||||
it("#given a message.updated assistant event with parts: [] and no error/finish #when observed #then no progress is signalled (no activity yet)", () => {
|
it("#given a message.updated assistant event with parts: [] and no error/finish #when observed #then no progress is signalled (no activity yet)", () => {
|
||||||
const calls = freshCalls()
|
const calls = freshCalls()
|
||||||
observeEventForWatchdog(
|
observeEventForWatchdog(
|
||||||
|
|||||||
@@ -46,6 +46,18 @@ export function observeEventForWatchdog(
|
|||||||
const props = event.properties as Record<string, unknown> | undefined
|
const props = event.properties as Record<string, unknown> | undefined
|
||||||
if (!props) return
|
if (!props) return
|
||||||
|
|
||||||
|
if (event.type === "message.part.updated" || event.type === "message.part.delta") {
|
||||||
|
const sessionID = props.sessionID as string | undefined
|
||||||
|
const part = props.part as Record<string, unknown> | undefined
|
||||||
|
const partType = typeof part?.type === "string"
|
||||||
|
? part.type
|
||||||
|
: typeof props.type === "string" ? props.type : undefined
|
||||||
|
if (sessionID && partType) {
|
||||||
|
watchdog.onAssistantProgress(sessionID)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (event.type === "message.updated") {
|
if (event.type === "message.updated") {
|
||||||
const info = props.info as Record<string, unknown> | undefined
|
const info = props.info as Record<string, unknown> | undefined
|
||||||
const sessionID = info?.sessionID as string | undefined
|
const sessionID = info?.sessionID as string | undefined
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ describe("createEventHandler - model fallback", () => {
|
|||||||
const createHandler = (args?: {
|
const createHandler = (args?: {
|
||||||
hooks?: any
|
hooks?: any
|
||||||
pluginConfig?: any
|
pluginConfig?: any
|
||||||
|
abort?: (input: { path: { id: string } }) => Promise<unknown>
|
||||||
promptAsync?: (input: { path: { id: string } }) => Promise<unknown>
|
promptAsync?: (input: { path: { id: string } }) => Promise<unknown>
|
||||||
}) => {
|
}) => {
|
||||||
setupConnectedProviderCacheMocks()
|
setupConnectedProviderCacheMocks()
|
||||||
@@ -41,6 +42,9 @@ describe("createEventHandler - model fallback", () => {
|
|||||||
const sessionClient = {
|
const sessionClient = {
|
||||||
abort: async ({ path }: { path: { id: string } }) => {
|
abort: async ({ path }: { path: { id: string } }) => {
|
||||||
abortCalls.push(path.id)
|
abortCalls.push(path.id)
|
||||||
|
if (args?.abort) {
|
||||||
|
return args.abort({ path })
|
||||||
|
}
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
prompt: async ({ path }: { path: { id: string } }) => {
|
prompt: async ({ path }: { path: { id: string } }) => {
|
||||||
@@ -305,6 +309,58 @@ describe("createEventHandler - model fallback", () => {
|
|||||||
expect(abortCalls).toEqual([sessionID])
|
expect(abortCalls).toEqual([sessionID])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("#given abort fails before model-fallback continuation #when fallback handles assistant error #then it does not inject another prompt", async () => {
|
||||||
|
//#given
|
||||||
|
const sessionID = "ses_model_fallback_abort_failure"
|
||||||
|
setMainSession(sessionID)
|
||||||
|
let pendingFallbackArms = 0
|
||||||
|
const modelFallback = unsafeTestValue({
|
||||||
|
setSessionFallbackChain: () => {},
|
||||||
|
setPendingModelFallback: () => {
|
||||||
|
pendingFallbackArms += 1
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { handler, abortCalls, promptAsyncCalls } = createHandler({
|
||||||
|
hooks: { modelFallback },
|
||||||
|
abort: async () => {
|
||||||
|
throw new Error("abort transport failed")
|
||||||
|
},
|
||||||
|
promptAsync: async () => ({}),
|
||||||
|
})
|
||||||
|
const assistantError = {
|
||||||
|
name: "APIError",
|
||||||
|
data: {
|
||||||
|
message:
|
||||||
|
"Bad Gateway: {\"error\":{\"message\":\"unknown provider for model claude-opus-4-7-thinking\"}}",
|
||||||
|
isRetryable: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
await handler({
|
||||||
|
event: {
|
||||||
|
type: "message.updated",
|
||||||
|
properties: {
|
||||||
|
info: {
|
||||||
|
id: "msg_err_abort_failure",
|
||||||
|
sessionID,
|
||||||
|
role: "assistant",
|
||||||
|
error: assistantError,
|
||||||
|
modelID: "claude-opus-4-7-thinking",
|
||||||
|
providerID: "anthropic",
|
||||||
|
agent: "Sisyphus - Ultraworker",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(pendingFallbackArms).toBe(1)
|
||||||
|
expect(abortCalls).toEqual([sessionID])
|
||||||
|
expect(promptAsyncCalls).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
test("does not collapse fallback continuations for different providers with the same model id", async () => {
|
test("does not collapse fallback continuations for different providers with the same model id", async () => {
|
||||||
//#given
|
//#given
|
||||||
const sessionID = "ses_model_fallback_same_model_different_provider"
|
const sessionID = "ses_model_fallback_same_model_different_provider"
|
||||||
|
|||||||
+7
-3
@@ -474,9 +474,12 @@ export function createEventHandler(args: {
|
|||||||
modelFallbackContinuationsInFlight.add(sessionID);
|
modelFallbackContinuationsInFlight.add(sessionID);
|
||||||
let dispatched = false;
|
let dispatched = false;
|
||||||
try {
|
try {
|
||||||
await pluginContext.client.session.abort({ path: { id: sessionID } }).catch((error) => {
|
try {
|
||||||
|
await pluginContext.client.session.abort({ path: { id: sessionID } });
|
||||||
|
} catch (error) {
|
||||||
log("[event] model-fallback abort failed", { sessionID, source, error });
|
log("[event] model-fallback abort failed", { sessionID, source, error });
|
||||||
});
|
return;
|
||||||
|
}
|
||||||
releasePromptAsyncReservation(sessionID, `model-fallback-abort:${source}`, {
|
releasePromptAsyncReservation(sessionID, `model-fallback-abort:${source}`, {
|
||||||
reservedBy: [`model-fallback:${source}`, `model-fallback:${source}:sync`],
|
reservedBy: [`model-fallback:${source}`, `model-fallback:${source}:sync`],
|
||||||
reservedByPrefix: "model-fallback:",
|
reservedByPrefix: "model-fallback:",
|
||||||
@@ -755,7 +758,8 @@ export function createEventHandler(args: {
|
|||||||
const sessionID = resolveMessageEventSessionID(props);
|
const sessionID = resolveMessageEventSessionID(props);
|
||||||
const agent = info?.agent as string | undefined;
|
const agent = info?.agent as string | undefined;
|
||||||
const role = info?.role as string | undefined;
|
const role = info?.role as string | undefined;
|
||||||
if (sessionID && info?.finish === true) {
|
const finish = info?.finish;
|
||||||
|
if (sessionID && ((typeof finish === "string" && finish.length > 0) || finish === true)) {
|
||||||
invalidateContextWindowUsageCache(pluginContext as PluginInput, sessionID);
|
invalidateContextWindowUsageCache(pluginContext as PluginInput, sessionID);
|
||||||
}
|
}
|
||||||
if (sessionID && role === "user") {
|
if (sessionID && role === "user") {
|
||||||
|
|||||||
Reference in New Issue
Block a user