fix(prompt-retry): preserve async holds without blocking validation fallbacks
This commit is contained in:
@@ -405,6 +405,67 @@ describe("promptWithModelSuggestionRetry", () => {
|
||||
expect(promptMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("#given promptAsync throws after dispatch was attempted #when caller observes the error #then the post-dispatch hold remains reserved", async () => {
|
||||
// given
|
||||
const promptMock = mock().mockRejectedValueOnce(new Error("JSON Parse error: Unexpected EOF"))
|
||||
const client = { session: { promptAsync: promptMock } }
|
||||
const args = {
|
||||
path: { id: "session-failed-async-hold" },
|
||||
body: {
|
||||
parts: [{ type: "text", text: "hello" }],
|
||||
model: { providerID: "anthropic", modelID: "claude-sonnet-4" },
|
||||
},
|
||||
}
|
||||
|
||||
// when
|
||||
await expect(
|
||||
promptWithModelSuggestionRetry(unsafeTestValue(client), args)
|
||||
).rejects.toThrow("Unexpected EOF")
|
||||
const second = await dispatchInternalPrompt({
|
||||
mode: "async",
|
||||
client,
|
||||
sessionID: "session-failed-async-hold",
|
||||
input: args,
|
||||
source: "test:after-failed-async",
|
||||
settleMs: 0,
|
||||
postDispatchHoldMs: 0,
|
||||
})
|
||||
|
||||
// then
|
||||
expect(second).toEqual({ status: "reserved", reservedBy: "model-suggestion-retry" })
|
||||
expect(promptMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("#given promptAsync rejects before acceptance with an agent lookup error #when retried immediately #then the reservation is released", async () => {
|
||||
// given
|
||||
const promptMock = mock()
|
||||
.mockRejectedValueOnce(new Error("Agent not found: missing-agent"))
|
||||
.mockResolvedValueOnce(undefined)
|
||||
const client = { session: { promptAsync: promptMock } }
|
||||
const args = {
|
||||
path: { id: "session-agent-preaccept-failure" },
|
||||
body: {
|
||||
agent: "missing-agent",
|
||||
parts: [{ type: "text", text: "hello" }],
|
||||
},
|
||||
}
|
||||
|
||||
// when
|
||||
await expect(
|
||||
promptWithModelSuggestionRetry(unsafeTestValue(client), args)
|
||||
).rejects.toThrow("Agent not found")
|
||||
await promptWithModelSuggestionRetry(unsafeTestValue(client), {
|
||||
...args,
|
||||
body: {
|
||||
...args.body,
|
||||
agent: "general",
|
||||
},
|
||||
})
|
||||
|
||||
// then
|
||||
expect(promptMock).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it("should pass all body fields through to promptAsync", async () => {
|
||||
// given a client where promptAsync succeeds
|
||||
const promptMock = mock().mockResolvedValueOnce(undefined)
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
import {
|
||||
dispatchInternalPrompt,
|
||||
releasePromptAsyncReservation,
|
||||
type InternalPromptDispatchResult,
|
||||
} from "./prompt-async-gate"
|
||||
|
||||
type Client = ReturnType<typeof createOpencodeClient>
|
||||
@@ -34,6 +33,15 @@ function extractMessage(error: unknown): string {
|
||||
return String(error)
|
||||
}
|
||||
|
||||
function isAgentResolutionError(error: unknown): boolean {
|
||||
const message = extractMessage(error)
|
||||
return message.includes("Agent not found") || message.includes("agent.name")
|
||||
}
|
||||
|
||||
function shouldReleaseReservationAfterFailedAsyncPrompt(error: unknown): boolean {
|
||||
return parseModelSuggestion(error) !== null || isAgentResolutionError(error)
|
||||
}
|
||||
|
||||
export function parseModelSuggestion(error: unknown): ModelSuggestionInfo | null {
|
||||
if (!error) return null
|
||||
|
||||
@@ -98,10 +106,9 @@ export async function promptWithModelSuggestionRetry(
|
||||
): Promise<void> {
|
||||
const timeoutMs = options.timeoutMs ?? PROMPT_TIMEOUT_MS
|
||||
const timeoutContext = createPromptTimeoutContext(args, timeoutMs)
|
||||
let promptResult: InternalPromptDispatchResult | undefined
|
||||
|
||||
try {
|
||||
promptResult = await dispatchInternalPrompt({
|
||||
const promptResult = await dispatchInternalPrompt({
|
||||
mode: "async",
|
||||
client,
|
||||
sessionID: args.path.id,
|
||||
@@ -125,7 +132,7 @@ export async function promptWithModelSuggestionRetry(
|
||||
if (timeoutContext.wasTimedOut()) {
|
||||
throw new Error(`promptAsync timed out after ${timeoutMs}ms`)
|
||||
}
|
||||
if (promptResult?.status === "failed") {
|
||||
if (shouldReleaseReservationAfterFailedAsyncPrompt(error)) {
|
||||
releasePromptAsyncReservation(args.path.id, "model-suggestion-retry")
|
||||
}
|
||||
throw error
|
||||
|
||||
Reference in New Issue
Block a user