fix(prompt): treat post-dispatch failures as accepted

This commit is contained in:
YeonGyu-Kim
2026-05-20 11:42:32 +09:00
parent f540249838
commit d3e218f912
31 changed files with 503 additions and 76 deletions
@@ -479,6 +479,39 @@ describe("executeSync", () => {
expect(deps.processMessages).not.toHaveBeenCalled()
})
test("#given sync prompt returns ambiguous EOF after dispatch #when executeSync runs #then it waits for the existing session result", async () => {
//#given
const executeSync = await importExecuteSync()
const deps = createDependencies({
createOrGetSession: mock(async () => ({ sessionID: "ses-ambiguous-prompt", isNew: true })),
waitForCompletion: mock(async () => {}),
processMessages: mock(async () => "accepted response"),
})
const toolContext = createToolContext()
const recorder = createPromptAsyncRecorder(async () => {
throw new Error("JSON Parse error: Unexpected EOF")
})
const args = {
subagent_type: "librarian",
description: "ambiguous prompt",
prompt: "find docs",
run_in_background: false,
}
//#when
const result = await executeSync(args, toolContext, createContext(recorder.promptAsync) as never, deps)
//#then
expect(result).toContain("accepted response")
expect(result).toContain("session_id: ses-ambiguous-prompt")
expect(deps.waitForCompletion).toHaveBeenCalledWith(
"ses-ambiguous-prompt",
toolContext,
expect.objectContaining({ client: expect.anything() }),
)
expect(deps.processMessages).toHaveBeenCalledTimes(1)
})
test("does not send a duplicate sync prompt when a reused session is active", async () => {
//#given
const executeSync = await importExecuteSync()
+12 -3
View File
@@ -1,7 +1,7 @@
import type { PluginInput } from "@opencode-ai/plugin"
import { clearSessionAgent, setSessionAgent, subagentSessions, syncSubagentSessions } from "../../features/claude-code-session-state"
import { dispatchInternalPrompt, isInternalPromptDispatchAccepted } from "../../hooks/shared/prompt-async-gate"
import { getAgentToolRestrictions, log } from "../../shared"
import { getAgentToolRestrictions, isAmbiguousPostDispatchPromptFailure, log } from "../../shared"
import { getAgentDisplayName, stripAgentListSortPrefix } from "../../shared/agent-display-names"
import {
clearDelegatedChildSessionBootstrap,
@@ -153,10 +153,19 @@ export async function executeSync(
},
},
})
const promptMayHaveBeenAccepted = promptResult.status === "failed"
&& isAmbiguousPostDispatchPromptFailure(promptResult)
if (promptResult.status === "failed") {
throw promptResult.error
if (promptMayHaveBeenAccepted) {
log("[call_omo_agent] Prompt returned an ambiguous error after dispatch; waiting for completion", {
sessionID,
error: promptResult.error instanceof Error ? promptResult.error.message : String(promptResult.error),
})
} else {
throw promptResult.error
}
}
if (!isInternalPromptDispatchAccepted(promptResult)) {
if (!promptMayHaveBeenAccepted && !isInternalPromptDispatchAccepted(promptResult)) {
throw new Error(`prompt skipped by gate: ${promptResult.status}`)
}
} catch (error) {
+73 -2
View File
@@ -2006,7 +2006,7 @@ describe("sisyphus-task", () => {
}
const promptMock = async () => {
throw new Error("JSON Parse error: Unexpected EOF")
throw new Error("Synthetic prompt transport failure")
}
const mockClient = {
@@ -2050,11 +2050,82 @@ describe("sisyphus-task", () => {
// then - should return detailed error message with args and stack trace
expect(result).toContain("Send prompt failed")
expect(result).toContain("JSON Parse error")
expect(result).toContain("Synthetic prompt transport failure")
expect(result).toContain("**Arguments**:")
expect(result).toContain("**Stack Trace**:")
})
test("#given sync prompt returns ambiguous EOF #when sync task runs #then it waits for the accepted session result", async () => {
// given
const { createDelegateTask } = require("./tools")
let promptCalls = 0
const mockManager = {
launch: async () => ({}),
}
const promptMock = async () => {
promptCalls += 1
throw new Error("JSON Parse error: Unexpected EOF")
}
const mockClient = {
session: {
get: async () => ({ data: { directory: "/project" } }),
create: async () => ({ data: { id: "ses_sync_ambiguous_eof" } }),
prompt: promptMock,
promptAsync: promptMock,
messages: async () => ({
data: [
{
info: { id: "msg_001", role: "user", time: { created: Date.now() } },
parts: [{ type: "text", text: "Do something" }],
},
{
info: { id: "msg_002", role: "assistant", time: { created: Date.now() + 1 }, finish: "end_turn" },
parts: [{ type: "text", text: "Accepted despite EOF" }],
},
],
}),
status: async () => ({ data: { ses_sync_ambiguous_eof: { type: "idle" } } }),
abort: async () => ({}),
},
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
app: {
agents: async () => ({ data: [{ name: "ultrabrain", mode: "subagent" }] }),
},
}
const tool = createDelegateTask({
manager: mockManager,
client: mockClient,
})
const toolContext = {
sessionID: "parent-session",
messageID: "parent-message",
agent: "sisyphus",
abort: new AbortController().signal,
}
// when
const result = await tool.execute(
{
description: "Sync accepted EOF test",
prompt: "Do something",
category: "ultrabrain",
run_in_background: false,
load_skills: ["git-master"],
},
toolContext
)
// then
expect(result).toContain("Accepted despite EOF")
expect(result).toContain("Task completed")
expect(promptCalls).toBe(1)
}, { timeout: 20000 })
test("sync mode success returns task result with content", async () => {
// given
const { createDelegateTask } = require("./tools")
@@ -6,6 +6,7 @@ import { MULTIMODAL_LOOKER_AGENT } from "./constants"
import { READ_ENABLED, buildLookAtPrompt } from "./look-at-prompt"
import type { LookAtFilePart } from "./look-at-input-preparer"
import { resolveMultimodalLookerAgentMetadata } from "./multimodal-agent-metadata"
import { pollSessionUntilIdle } from "./session-poller"
interface RunLookAtSessionInput {
ctx: PluginInput
@@ -85,6 +86,10 @@ Original error: ${createResult.error}`
log("[look_at] Prompt error (ignored, will still fetch messages):", promptError)
}
if (typeof ctx.client.session.status === "function") {
await pollSessionUntilIdle(ctx.client, sessionID)
}
log(`[look_at] Fetching messages from session ${sessionID}...`)
const messagesResult = await ctx.client.session.messages({
path: { id: sessionID },
+22 -13
View File
@@ -371,28 +371,36 @@ describe("look-at tool", () => {
expect(result).toBe("result")
expect(syncPrompt).toHaveBeenCalledTimes(1)
expect(asyncPrompt).not.toHaveBeenCalled()
expect(statusFn).not.toHaveBeenCalled()
expect(statusFn).toHaveBeenCalledTimes(1)
})
// given sync prompt throws (JSON parse error even on success)
// when tool is executed
// then catches error gracefully and still fetches messages
test("catches sync prompt errors and still fetches messages", async () => {
test("#given sync prompt returns ambiguous EOF #when look_at runs #then it waits for idle before reading messages", async () => {
// given
const callOrder: string[] = []
const mockClient = {
app: {
agents: async () => ({ data: [] }),
},
session: {
get: async () => ({ data: { directory: "/project" } }),
create: async () => ({ data: { id: "ses_sync_error" } }),
prompt: async () => { throw new Error("JSON parse error") },
create: async () => ({ data: { id: "ses_sync_ambiguous" } }),
prompt: async () => {
callOrder.push("prompt")
throw new Error("JSON Parse error: Unexpected EOF")
},
promptAsync: async () => ({}),
status: async () => ({ data: {} }),
messages: async () => ({
data: [
{ info: { role: "assistant", time: { created: 1 } }, parts: [{ type: "text", text: "result despite error" }] },
],
}),
status: async () => {
callOrder.push("status")
return { data: {} }
},
messages: async () => {
callOrder.push("messages")
return {
data: [
{ info: { role: "assistant", time: { created: 1 } }, parts: [{ type: "text", text: "result despite error" }] },
],
}
},
},
}
@@ -418,6 +426,7 @@ describe("look-at tool", () => {
)
expect(result).toBe("result despite error")
expect(callOrder).toEqual(["prompt", "status", "messages"])
})
// given sync prompt throws and no messages available