fix(prompt-retry): preserve async holds without blocking validation fallbacks
This commit is contained in:
@@ -412,4 +412,46 @@ bunDescribe("sendSyncPrompt", () => {
|
||||
bunExpect(promptWithModelSuggestionRetry).toHaveBeenCalledTimes(1)
|
||||
bunExpect(promptSyncWithModelSuggestionRetry).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
bunTest("#given oracle promptSync fallback is blocked by the prompt gate #when async prompt reports EOF #then the original EOF error is preserved", async () => {
|
||||
//#given
|
||||
const { sendSyncPrompt } = require("./sync-prompt-sender")
|
||||
|
||||
const promptWithModelSuggestionRetry = bunMock(async () => {
|
||||
throw new Error("JSON Parse error: Unexpected EOF")
|
||||
})
|
||||
const promptSyncWithModelSuggestionRetry = bunMock(async () => {
|
||||
throw new Error("prompt skipped by gate: reserved")
|
||||
})
|
||||
|
||||
const input = {
|
||||
sessionID: "test-session",
|
||||
agentToUse: "oracle",
|
||||
args: {
|
||||
description: "test task",
|
||||
prompt: "test prompt",
|
||||
run_in_background: false,
|
||||
load_skills: [],
|
||||
},
|
||||
systemContent: undefined,
|
||||
categoryModel: undefined,
|
||||
toastManager: null,
|
||||
taskId: undefined,
|
||||
}
|
||||
|
||||
//#when
|
||||
const result = await sendSyncPrompt(
|
||||
{ session: { promptAsync: bunMock(async () => ({ data: {} })) } },
|
||||
input,
|
||||
{
|
||||
promptWithModelSuggestionRetry,
|
||||
promptSyncWithModelSuggestionRetry,
|
||||
},
|
||||
)
|
||||
|
||||
//#then
|
||||
bunExpect(result).toContain("JSON Parse error")
|
||||
bunExpect(result).not.toContain("prompt skipped by gate")
|
||||
bunExpect(promptSyncWithModelSuggestionRetry).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -52,6 +52,11 @@ function isUnexpectedEofError(error: unknown): boolean {
|
||||
return lowered.includes("unexpected eof") || lowered.includes("json parse error")
|
||||
}
|
||||
|
||||
function isPromptGateReservedError(error: unknown): boolean {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return message.includes("promptAsync skipped by gate: reserved") || message.includes("prompt skipped by gate: reserved")
|
||||
}
|
||||
|
||||
export function buildSyncPromptTools(agentToUse: string): Record<string, boolean> {
|
||||
return {
|
||||
task: isPlanFamily(agentToUse),
|
||||
@@ -112,7 +117,9 @@ export async function sendSyncPrompt(
|
||||
await deps.promptSyncWithModelSuggestionRetry(client, routePromptSyncRetry(promptArgs, input.directory))
|
||||
return null
|
||||
} catch (oracleRetryError) {
|
||||
promptError = oracleRetryError
|
||||
if (!isPromptGateReservedError(oracleRetryError)) {
|
||||
promptError = oracleRetryError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ function toDelegatedModelConfig(fallback: NonNullable<ReturnType<typeof getNextR
|
||||
}
|
||||
}
|
||||
|
||||
function isPromptGateReservedError(error: string): boolean {
|
||||
return error.includes("promptAsync skipped by gate: reserved") || error.includes("prompt skipped by gate: reserved")
|
||||
}
|
||||
|
||||
export async function retrySyncPromptWithFallbacks(input: {
|
||||
sessionID: string
|
||||
initialError: string
|
||||
@@ -63,6 +67,14 @@ export async function retrySyncPromptWithFallbacks(input: {
|
||||
}
|
||||
}
|
||||
|
||||
if (isPromptGateReservedError(promptError)) {
|
||||
return {
|
||||
promptError: finalError,
|
||||
categoryModel,
|
||||
fallbackState,
|
||||
}
|
||||
}
|
||||
|
||||
finalError = promptError
|
||||
fallbackState.providerID = fallbackModel.providerID
|
||||
fallbackState.modelID = fallbackModel.modelID
|
||||
|
||||
@@ -514,6 +514,34 @@ describe("executeSyncTask - cleanup on error paths", () => {
|
||||
])
|
||||
})
|
||||
|
||||
test("#given sync prompt fallback is blocked by the prompt gate #when retrying prompt fallback #then preserves the original prompt error", async () => {
|
||||
//#given
|
||||
const { retrySyncPromptWithFallbacks } = require("./sync-task-fallback")
|
||||
const sendPrompt = mock(async () => "promptAsync skipped by gate: reserved")
|
||||
const initialModel = {
|
||||
providerID: "anthropic",
|
||||
modelID: "claude-opus-4-7",
|
||||
variant: "max",
|
||||
}
|
||||
|
||||
//#when
|
||||
const result = await retrySyncPromptWithFallbacks({
|
||||
sessionID: "ses_gate_reserved",
|
||||
initialError: "JSON Parse error: Unexpected EOF",
|
||||
categoryModel: initialModel,
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic"], model: "claude-opus-4-7", variant: "max" },
|
||||
{ providers: ["openai"], model: "gpt-5.4", variant: "medium" },
|
||||
],
|
||||
sendPrompt,
|
||||
})
|
||||
|
||||
//#then
|
||||
expect(result.promptError).toBe("JSON Parse error: Unexpected EOF")
|
||||
expect(result.categoryModel).toEqual(initialModel)
|
||||
expect(sendPrompt).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test("cleans up toast and subagentSessions on successful completion", async () => {
|
||||
const mockClient = {
|
||||
session: {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { clearSkillCache } from "../../features/opencode-skill-loader/skill-cont
|
||||
import { __setTimingConfig, __resetTimingConfig } from "./timing"
|
||||
import * as connectedProvidersCache from "../../shared/connected-providers-cache"
|
||||
import * as executor from "./executor"
|
||||
import { releaseAllPromptAsyncReservationsForTesting } from "../../shared/prompt-async-gate"
|
||||
|
||||
const runtimeRequire = require as NodeJS.Require & { cache?: Record<string, unknown> }
|
||||
|
||||
@@ -78,6 +79,7 @@ describe("sisyphus-task", () => {
|
||||
|
||||
afterEach(() => {
|
||||
__resetTimingConfig()
|
||||
releaseAllPromptAsyncReservationsForTesting()
|
||||
cacheSpy?.mockRestore()
|
||||
providerModelsSpy?.mockRestore()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user