fix(prompt): treat post-dispatch failures as accepted
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user