fix(delegate-task): prevent stale-text abort recovery
This commit is contained in:
@@ -191,7 +191,9 @@ export async function executeSyncContinuation(
|
||||
if (anchorMessageCount === undefined) {
|
||||
return pollError
|
||||
}
|
||||
const recoveredResult = await deps.fetchSyncResult(client, continuationID, anchorMessageCount)
|
||||
const recoveredResult = await deps.fetchSyncResult(client, continuationID, anchorMessageCount, {
|
||||
strictAbortRecovery: true,
|
||||
})
|
||||
if (!recoveredResult.ok) {
|
||||
return pollError
|
||||
}
|
||||
|
||||
@@ -141,4 +141,65 @@ describe("fetchSyncResult", () => {
|
||||
expect(result.ok).toBe(false)
|
||||
expect(result.error).toContain("No assistant response found")
|
||||
})
|
||||
|
||||
test("strict abort recovery: does not fall back to older text when latest assistant is error", async () => {
|
||||
//#given
|
||||
const { fetchSyncResult } = require("./sync-result-fetcher")
|
||||
|
||||
const mockClient = {
|
||||
session: {
|
||||
messages: async () => ({
|
||||
data: [
|
||||
{ info: { id: "msg_001", role: "user", time: { created: 1000 } } },
|
||||
{
|
||||
info: { id: "msg_002", role: "assistant", time: { created: 2000 } },
|
||||
parts: [{ type: "text", text: "Older text" }],
|
||||
},
|
||||
{
|
||||
info: {
|
||||
id: "msg_003",
|
||||
role: "assistant",
|
||||
time: { created: 3000 },
|
||||
error: { name: "MessageAbortedError", message: "The operation was aborted." },
|
||||
},
|
||||
parts: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
//#when
|
||||
const result = await fetchSyncResult(mockClient, "ses_test", 1, { strictAbortRecovery: true })
|
||||
|
||||
//#then
|
||||
expect(result.ok).toBe(false)
|
||||
expect(result.error).toContain("Latest assistant message is an error")
|
||||
})
|
||||
|
||||
test("strict abort recovery: requires latest assistant text output", async () => {
|
||||
//#given
|
||||
const { fetchSyncResult } = require("./sync-result-fetcher")
|
||||
|
||||
const mockClient = {
|
||||
session: {
|
||||
messages: async () => ({
|
||||
data: [
|
||||
{ info: { id: "msg_001", role: "user", time: { created: 1000 } } },
|
||||
{
|
||||
info: { id: "msg_002", role: "assistant", time: { created: 2000 } },
|
||||
parts: [{ type: "tool", toolCallId: "t1", toolName: "x", state: "output-available", input: {}, output: {} }],
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
//#when
|
||||
const result = await fetchSyncResult(mockClient, "ses_test", 0, { strictAbortRecovery: true })
|
||||
|
||||
//#then
|
||||
expect(result.ok).toBe(false)
|
||||
expect(result.error).toContain("No assistant text output found in latest response")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,7 +5,8 @@ import { normalizeSDKResponse } from "../../shared"
|
||||
export async function fetchSyncResult(
|
||||
client: OpencodeClient,
|
||||
sessionID: string,
|
||||
anchorMessageCount?: number
|
||||
anchorMessageCount?: number,
|
||||
options?: { strictAbortRecovery?: boolean }
|
||||
): Promise<{ ok: true; textContent: string } | { ok: false; error: string }> {
|
||||
const messagesResult = await client.session.messages({
|
||||
path: { id: sessionID },
|
||||
@@ -44,6 +45,26 @@ export async function fetchSyncResult(
|
||||
return { ok: false, error: `No assistant response found.\n\nSession ID: ${sessionID}` }
|
||||
}
|
||||
|
||||
if (options?.strictAbortRecovery) {
|
||||
if (lastMessage.info && "error" in lastMessage.info) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `Latest assistant message is an error; refusing abort recovery.\n\nSession ID: ${sessionID}`,
|
||||
}
|
||||
}
|
||||
|
||||
const lastTextParts = lastMessage.parts?.filter((p) => p.type === "text" || p.type === "reasoning") ?? []
|
||||
const lastContent = lastTextParts.map((p) => p.text ?? "").filter(Boolean).join("\n")
|
||||
if (!lastContent) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `No assistant text output found in latest response.\n\nSession ID: ${sessionID}`,
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: true, textContent: lastContent }
|
||||
}
|
||||
|
||||
// Search assistant messages (newest first) for one with text/reasoning content.
|
||||
// The last assistant message may only contain tool calls with no text.
|
||||
let textContent = ""
|
||||
|
||||
@@ -240,7 +240,9 @@ export async function executeSyncTask(
|
||||
}, syncPollTimeoutMs)
|
||||
if (pollError) {
|
||||
if (shouldAttemptPollErrorRecovery(pollError)) {
|
||||
const recoveredResult = await deps.fetchSyncResult(client, activeSessionID)
|
||||
const recoveredResult = await deps.fetchSyncResult(client, activeSessionID, undefined, {
|
||||
strictAbortRecovery: true,
|
||||
})
|
||||
if (recoveredResult.ok) {
|
||||
const duration = formatDuration(startTime)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user