fix(prompt-gate): harden sync and team prompt dispatch
This commit is contained in:
@@ -69,6 +69,7 @@ describe("delegate-task Oracle gap closure", () => {
|
||||
client: {
|
||||
session: {
|
||||
messages: async () => ({ data: [{ info: { agent: "explore", model: MODEL, variant: "max" } }] }),
|
||||
prompt: async () => ({}),
|
||||
promptAsync: async () => ({}),
|
||||
},
|
||||
},
|
||||
@@ -99,6 +100,7 @@ describe("delegate-task Oracle gap closure", () => {
|
||||
client: {
|
||||
session: {
|
||||
messages: async () => ({ data: [{ info: { agent: "explore", model: MODEL } }] }),
|
||||
prompt: async () => ({}),
|
||||
promptAsync: async () => ({}),
|
||||
},
|
||||
},
|
||||
@@ -190,6 +192,10 @@ describe("delegate-task Oracle gap closure", () => {
|
||||
client: {
|
||||
session: {
|
||||
messages: async () => ({ data: [{ info: { agent: "explore", model: MODEL } }] }),
|
||||
prompt: async (input: { body?: { system?: string } }) => {
|
||||
promptCalls.push(input)
|
||||
return {}
|
||||
},
|
||||
promptAsync: async (input: { body?: { system?: string } }) => {
|
||||
promptCalls.push(input)
|
||||
return {}
|
||||
|
||||
@@ -74,6 +74,7 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
|
||||
},
|
||||
],
|
||||
}),
|
||||
prompt: async () => ({}),
|
||||
promptAsync: async () => ({}),
|
||||
status: async () => ({
|
||||
data: { ses_test: { type: "idle" } },
|
||||
@@ -136,6 +137,7 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
|
||||
},
|
||||
],
|
||||
}),
|
||||
prompt: async () => ({}),
|
||||
promptAsync: async () => ({}),
|
||||
status: async () => ({
|
||||
data: { ses_test: { type: "idle" } },
|
||||
@@ -198,6 +200,7 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
|
||||
},
|
||||
],
|
||||
}),
|
||||
prompt: async () => ({}),
|
||||
promptAsync: async () => ({}),
|
||||
status: async () => ({
|
||||
data: { ses_test: { type: "idle" } },
|
||||
@@ -256,6 +259,7 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
|
||||
},
|
||||
],
|
||||
}),
|
||||
prompt: async () => ({}),
|
||||
promptAsync: async () => ({}),
|
||||
status: async () => ({
|
||||
data: { ses_test: { type: "idle" } },
|
||||
@@ -312,6 +316,7 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
|
||||
},
|
||||
],
|
||||
}),
|
||||
prompt: async () => ({}),
|
||||
promptAsync: async () => ({}),
|
||||
status: async () => ({
|
||||
data: { ses_test: { type: "idle" } },
|
||||
@@ -363,6 +368,7 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
|
||||
messages: async () => {
|
||||
throw new Error("messages unavailable")
|
||||
},
|
||||
prompt: async () => ({}),
|
||||
promptAsync: async () => ({}),
|
||||
status: async () => ({
|
||||
data: { ses_test: { type: "idle" } },
|
||||
@@ -429,6 +435,7 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
|
||||
},
|
||||
],
|
||||
}),
|
||||
prompt: async () => ({}),
|
||||
promptAsync: async () => ({}),
|
||||
status: async () => ({
|
||||
data: { ses_test: { type: "idle" } },
|
||||
@@ -487,6 +494,7 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
|
||||
},
|
||||
],
|
||||
}),
|
||||
prompt: async () => ({}),
|
||||
promptAsync: async () => ({}),
|
||||
status: async () => ({
|
||||
data: { ses_test: { type: "idle" } },
|
||||
@@ -550,6 +558,7 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
|
||||
},
|
||||
],
|
||||
}),
|
||||
prompt: async () => ({}),
|
||||
promptAsync: async () => ({}),
|
||||
status: async () => ({
|
||||
data: { ses_test: { type: "idle" } },
|
||||
@@ -610,6 +619,7 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
|
||||
},
|
||||
],
|
||||
}),
|
||||
prompt: async () => ({}),
|
||||
promptAsync: async () => ({}),
|
||||
status: async () => ({
|
||||
data: { ses_test: { type: "idle" } },
|
||||
@@ -664,6 +674,7 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
|
||||
},
|
||||
],
|
||||
}),
|
||||
prompt: async () => ({}),
|
||||
promptAsync: async () => ({}),
|
||||
status: async () => ({
|
||||
data: { ses_test: { type: "idle" } },
|
||||
@@ -725,6 +736,10 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
|
||||
},
|
||||
],
|
||||
}),
|
||||
prompt: async (input: { path: { id: string }; body: Record<string, unknown> }) => {
|
||||
promptAsyncCalls.push(input)
|
||||
return {}
|
||||
},
|
||||
promptAsync: async (input: { path: { id: string }; body: Record<string, unknown> }) => {
|
||||
promptAsyncCalls.push(input)
|
||||
return {}
|
||||
@@ -796,6 +811,10 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
|
||||
},
|
||||
],
|
||||
}),
|
||||
prompt: async (input: { path: { id: string }; body: Record<string, unknown> }) => {
|
||||
promptAsyncCalls.push(input)
|
||||
return {}
|
||||
},
|
||||
promptAsync: async (input: { path: { id: string }; body: Record<string, unknown> }) => {
|
||||
promptAsyncCalls.push(input)
|
||||
return {}
|
||||
@@ -867,6 +886,10 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
|
||||
},
|
||||
],
|
||||
}),
|
||||
prompt: async (input: { path: { id: string }; body: Record<string, unknown> }) => {
|
||||
promptAsyncCalls.push(input)
|
||||
return {}
|
||||
},
|
||||
promptAsync: async (input: { path: { id: string }; body: Record<string, unknown> }) => {
|
||||
promptAsyncCalls.push(input)
|
||||
return {}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { publishToolMetadata } from "../../features/tool-metadata-store"
|
||||
import { getTaskToastManager } from "../../features/task-toast-manager"
|
||||
import { getAgentToolRestrictions } from "../../shared/agent-tool-restrictions"
|
||||
import { getMessageDir, normalizeSDKResponse } from "../../shared"
|
||||
import { promptWithModelSuggestionRetry } from "../../shared/model-suggestion-retry"
|
||||
import { promptSyncWithModelSuggestionRetry } from "../../shared/model-suggestion-retry"
|
||||
import { resolveMessageContext } from "../../features/hook-message-injector"
|
||||
import { formatDuration } from "./time-formatter"
|
||||
import { syncContinuationDeps, type SyncContinuationDeps } from "./sync-continuation-deps"
|
||||
@@ -160,7 +160,7 @@ export async function executeSyncContinuation(
|
||||
}
|
||||
setSessionTools(continuationID, tools)
|
||||
|
||||
await promptWithModelSuggestionRetry(client, {
|
||||
await promptSyncWithModelSuggestionRetry(client, {
|
||||
path: { id: continuationID },
|
||||
body: {
|
||||
...(resumeAgent !== undefined ? { agent: resumeAgent } : {}),
|
||||
|
||||
@@ -5,19 +5,16 @@ import type { OpencodeClient } from "./types"
|
||||
import { sendSyncPrompt } from "./sync-prompt-sender"
|
||||
import {
|
||||
promptSyncWithModelSuggestionRetry,
|
||||
promptWithModelSuggestionRetry,
|
||||
} from "../../shared/model-suggestion-retry"
|
||||
|
||||
type PromptRetryClient = Parameters<typeof promptWithModelSuggestionRetry>[0]
|
||||
type PromptRetryArgs = Parameters<typeof promptWithModelSuggestionRetry>[1]
|
||||
type PromptSyncRetryClient = Parameters<typeof promptSyncWithModelSuggestionRetry>[0]
|
||||
type PromptSyncRetryArgs = Parameters<typeof promptSyncWithModelSuggestionRetry>[1]
|
||||
|
||||
describe("sendSyncPrompt session routing", () => {
|
||||
test("#given a sync child session directory #when sending the prompt #then promptAsync uses that OpenCode directory route", async () => {
|
||||
test("#given a sync child session directory #when sending the prompt #then prompt uses that OpenCode directory route", async () => {
|
||||
// given
|
||||
const promptCalls: PromptRetryArgs[] = []
|
||||
const promptWithRetry = mock(async (_client: PromptRetryClient, input: PromptRetryArgs) => {
|
||||
const promptCalls: PromptSyncRetryArgs[] = []
|
||||
const promptSyncWithRetry = mock(async (_client: PromptSyncRetryClient, input: PromptSyncRetryArgs) => {
|
||||
promptCalls.push(input)
|
||||
})
|
||||
|
||||
@@ -40,8 +37,7 @@ describe("sendSyncPrompt session routing", () => {
|
||||
taskId: undefined,
|
||||
},
|
||||
{
|
||||
promptWithModelSuggestionRetry: promptWithRetry,
|
||||
promptSyncWithModelSuggestionRetry: mock(async () => {}),
|
||||
promptSyncWithModelSuggestionRetry: promptSyncWithRetry,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -50,14 +46,12 @@ describe("sendSyncPrompt session routing", () => {
|
||||
expect(promptCalls[0]?.query).toEqual({ directory: "/parent/project" })
|
||||
})
|
||||
|
||||
test("#given oracle falls back to promptSync #when async prompt returns unexpected EOF #then the sync retry keeps the same directory route", async () => {
|
||||
test("#given oracle prompt returns unexpected EOF #when sending the prompt #then the sync route keeps the same directory route", async () => {
|
||||
// given
|
||||
const promptSyncCalls: PromptSyncRetryArgs[] = []
|
||||
const promptWithRetry = mock(async () => {
|
||||
throw new Error("JSON Parse error: Unexpected EOF")
|
||||
})
|
||||
const promptSyncWithRetry = mock(async (_client: PromptSyncRetryClient, input: PromptSyncRetryArgs) => {
|
||||
promptSyncCalls.push(input)
|
||||
throw new Error("JSON Parse error: Unexpected EOF")
|
||||
})
|
||||
|
||||
// when
|
||||
@@ -79,7 +73,6 @@ describe("sendSyncPrompt session routing", () => {
|
||||
taskId: undefined,
|
||||
},
|
||||
{
|
||||
promptWithModelSuggestionRetry: promptWithRetry,
|
||||
promptSyncWithModelSuggestionRetry: promptSyncWithRetry,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -28,6 +28,7 @@ bunDescribe("sendSyncPrompt", () => {
|
||||
|
||||
const mockClient = {
|
||||
session: {
|
||||
prompt: promptAsync,
|
||||
promptAsync,
|
||||
},
|
||||
}
|
||||
@@ -67,6 +68,7 @@ bunDescribe("sendSyncPrompt", () => {
|
||||
|
||||
const mockClient = {
|
||||
session: {
|
||||
prompt: promptAsync,
|
||||
promptAsync,
|
||||
},
|
||||
}
|
||||
@@ -107,6 +109,7 @@ bunDescribe("sendSyncPrompt", () => {
|
||||
|
||||
const mockClient = {
|
||||
session: {
|
||||
prompt: promptAsync,
|
||||
promptAsync,
|
||||
},
|
||||
}
|
||||
@@ -147,6 +150,7 @@ bunDescribe("sendSyncPrompt", () => {
|
||||
|
||||
const mockClient = {
|
||||
session: {
|
||||
prompt: promptAsync,
|
||||
promptAsync,
|
||||
},
|
||||
}
|
||||
@@ -187,6 +191,7 @@ bunDescribe("sendSyncPrompt", () => {
|
||||
|
||||
const mockClient = {
|
||||
session: {
|
||||
prompt: promptAsync,
|
||||
promptAsync,
|
||||
},
|
||||
}
|
||||
@@ -229,7 +234,7 @@ bunDescribe("sendSyncPrompt", () => {
|
||||
const { sendSyncPrompt } = require("./sync-prompt-sender")
|
||||
|
||||
let promptArgs: any
|
||||
const promptWithModelSuggestionRetry = bunMock(async (_client: any, input: any) => {
|
||||
const promptSyncWithModelSuggestionRetry = bunMock(async (_client: any, input: any) => {
|
||||
promptArgs = input
|
||||
})
|
||||
|
||||
@@ -259,16 +264,15 @@ bunDescribe("sendSyncPrompt", () => {
|
||||
|
||||
//#when
|
||||
await sendSyncPrompt(
|
||||
{ session: { promptAsync: bunMock(async () => ({ data: {} })) } },
|
||||
{ session: { prompt: bunMock(async () => ({ data: {} })) } },
|
||||
input,
|
||||
{
|
||||
promptWithModelSuggestionRetry,
|
||||
promptSyncWithModelSuggestionRetry: bunMock(async () => {}),
|
||||
promptSyncWithModelSuggestionRetry,
|
||||
},
|
||||
)
|
||||
|
||||
//#then
|
||||
bunExpect(promptWithModelSuggestionRetry).toHaveBeenCalledTimes(1)
|
||||
bunExpect(promptSyncWithModelSuggestionRetry).toHaveBeenCalledTimes(1)
|
||||
bunExpect(promptArgs.body.model).toEqual({
|
||||
providerID: "openai",
|
||||
modelID: "gpt-5.4",
|
||||
@@ -295,7 +299,7 @@ bunDescribe("sendSyncPrompt", () => {
|
||||
const { sendSyncPrompt } = require("./sync-prompt-sender")
|
||||
|
||||
let promptArgs: any
|
||||
const promptWithModelSuggestionRetry = bunMock(async (_client: any, input: any) => {
|
||||
const promptSyncWithModelSuggestionRetry = bunMock(async (_client: any, input: any) => {
|
||||
promptArgs = input
|
||||
})
|
||||
|
||||
@@ -321,26 +325,24 @@ bunDescribe("sendSyncPrompt", () => {
|
||||
|
||||
//#when
|
||||
await sendSyncPrompt(
|
||||
{ session: { promptAsync: bunMock(async () => ({ data: {} })) } },
|
||||
{ session: { prompt: bunMock(async () => ({ data: {} })) } },
|
||||
input,
|
||||
{
|
||||
promptWithModelSuggestionRetry,
|
||||
promptSyncWithModelSuggestionRetry: bunMock(async () => {}),
|
||||
promptSyncWithModelSuggestionRetry,
|
||||
},
|
||||
)
|
||||
|
||||
//#then
|
||||
bunExpect(promptWithModelSuggestionRetry).toHaveBeenCalledTimes(1)
|
||||
bunExpect(promptSyncWithModelSuggestionRetry).toHaveBeenCalledTimes(1)
|
||||
bunExpect(promptArgs.body.temperature).toBe(0.25)
|
||||
})
|
||||
bunTest("retries with promptSync for oracle when promptAsync fails with unexpected EOF", async () => {
|
||||
bunTest("#given oracle promptSync returns unexpected EOF #when sending a sync prompt #then the prompt is treated as started without retrying promptAsync", async () => {
|
||||
//#given
|
||||
const { sendSyncPrompt } = require("./sync-prompt-sender")
|
||||
|
||||
const promptWithModelSuggestionRetry = bunMock(async () => {
|
||||
const promptSyncWithModelSuggestionRetry = bunMock(async () => {
|
||||
throw new Error("JSON Parse error: Unexpected EOF")
|
||||
})
|
||||
const promptSyncWithModelSuggestionRetry = bunMock(async () => {})
|
||||
|
||||
const input = {
|
||||
sessionID: "test-session",
|
||||
@@ -362,25 +364,22 @@ bunDescribe("sendSyncPrompt", () => {
|
||||
{ session: { promptAsync: bunMock(async () => ({ data: {} })) } },
|
||||
input,
|
||||
{
|
||||
promptWithModelSuggestionRetry,
|
||||
promptSyncWithModelSuggestionRetry,
|
||||
},
|
||||
)
|
||||
|
||||
//#then
|
||||
bunExpect(result).toBeNull()
|
||||
bunExpect(promptWithModelSuggestionRetry).toHaveBeenCalledTimes(1)
|
||||
bunExpect(promptSyncWithModelSuggestionRetry).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
bunTest("does not retry with promptSync for non-oracle on unexpected EOF", async () => {
|
||||
bunTest("returns non-oracle unexpected EOF without retrying promptAsync", async () => {
|
||||
//#given
|
||||
const { sendSyncPrompt } = require("./sync-prompt-sender")
|
||||
|
||||
const promptWithModelSuggestionRetry = bunMock(async () => {
|
||||
const promptSyncWithModelSuggestionRetry = bunMock(async () => {
|
||||
throw new Error("JSON Parse error: Unexpected EOF")
|
||||
})
|
||||
const promptSyncWithModelSuggestionRetry = bunMock(async () => {})
|
||||
|
||||
const input = {
|
||||
sessionID: "test-session",
|
||||
@@ -402,24 +401,19 @@ bunDescribe("sendSyncPrompt", () => {
|
||||
{ session: { promptAsync: bunMock(async () => ({ data: {} })) } },
|
||||
input,
|
||||
{
|
||||
promptWithModelSuggestionRetry,
|
||||
promptSyncWithModelSuggestionRetry,
|
||||
},
|
||||
)
|
||||
|
||||
//#then
|
||||
bunExpect(result).toContain("Unexpected EOF")
|
||||
bunExpect(promptWithModelSuggestionRetry).toHaveBeenCalledTimes(1)
|
||||
bunExpect(promptSyncWithModelSuggestionRetry).toHaveBeenCalledTimes(0)
|
||||
bunExpect(promptSyncWithModelSuggestionRetry).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
bunTest("#given oracle promptSync fallback is blocked by the prompt gate #when async prompt reports EOF #then the original EOF error is preserved", async () => {
|
||||
bunTest("#given oracle promptSync is blocked by the prompt gate #when sending a sync prompt #then the gate 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")
|
||||
})
|
||||
@@ -444,14 +438,12 @@ bunDescribe("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(result).toContain("prompt skipped by gate")
|
||||
bunExpect(promptSyncWithModelSuggestionRetry).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,10 +4,9 @@ import { getAgentToolRestrictions } from "../../shared/agent-tool-restrictions"
|
||||
import { createInternalAgentTextPart } from "../../shared/internal-initiator-marker"
|
||||
import {
|
||||
promptSyncWithModelSuggestionRetry,
|
||||
promptWithModelSuggestionRetry,
|
||||
} from "../../shared/model-suggestion-retry"
|
||||
import { applySessionPromptParams } from "../../shared/session-prompt-params-helpers"
|
||||
import { routePromptRetry, routePromptSyncRetry } from "../../shared/session-route"
|
||||
import { routePromptSyncRetry } from "../../shared/session-route"
|
||||
import { setSessionTools } from "../../shared/session-tools-store"
|
||||
import { isPlanFamily } from "./constants"
|
||||
import { formatDetailedError } from "./error-formatting"
|
||||
@@ -15,12 +14,10 @@ import { buildTaskPrompt } from "./prompt-builder"
|
||||
import type { DelegatedModelConfig, DelegateTaskArgs, OpencodeClient } from "./types"
|
||||
|
||||
type SendSyncPromptDeps = {
|
||||
promptWithModelSuggestionRetry: typeof promptWithModelSuggestionRetry
|
||||
promptSyncWithModelSuggestionRetry: typeof promptSyncWithModelSuggestionRetry
|
||||
}
|
||||
|
||||
const sendSyncPromptDeps: SendSyncPromptDeps = {
|
||||
promptWithModelSuggestionRetry,
|
||||
promptSyncWithModelSuggestionRetry,
|
||||
}
|
||||
|
||||
@@ -52,11 +49,6 @@ 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),
|
||||
@@ -109,20 +101,12 @@ export async function sendSyncPrompt(
|
||||
}
|
||||
|
||||
try {
|
||||
const routedPromptArgs = routePromptRetry(promptArgs, input.directory)
|
||||
await deps.promptWithModelSuggestionRetry(client, routedPromptArgs, { queueBehavior: "defer" })
|
||||
await deps.promptSyncWithModelSuggestionRetry(client, routePromptSyncRetry(promptArgs, input.directory), {
|
||||
queueBehavior: "defer",
|
||||
})
|
||||
} catch (promptError) {
|
||||
if (isOracleAgent(input.agentToUse) && isUnexpectedEofError(promptError)) {
|
||||
try {
|
||||
await deps.promptSyncWithModelSuggestionRetry(client, routePromptSyncRetry(promptArgs, input.directory), {
|
||||
queueBehavior: "defer",
|
||||
})
|
||||
return null
|
||||
} catch (oracleRetryError) {
|
||||
if (!isPromptGateReservedError(oracleRetryError)) {
|
||||
promptError = oracleRetryError
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
if (input.toastManager && input.taskId !== undefined) {
|
||||
|
||||
Reference in New Issue
Block a user