fix(prompt-gate): harden sync and team prompt dispatch
This commit is contained in:
@@ -5704,6 +5704,7 @@ describe("BackgroundManager.handleEvent - session.error", () => {
|
||||
{
|
||||
info: {
|
||||
role: "assistant",
|
||||
finish: "end_turn",
|
||||
time: { created: 2_000 },
|
||||
},
|
||||
parts: [{ type: "text", text: "wake was already accepted" }],
|
||||
|
||||
@@ -197,6 +197,7 @@ export class ParentWakeNotifier {
|
||||
source: "background-agent-parent-wake",
|
||||
settleMs: 0,
|
||||
postDispatchHoldMs: 250,
|
||||
queueBehavior: "defer",
|
||||
checkToolState: !toolWaitDecision.skipPromptGateToolStateCheck,
|
||||
input: {
|
||||
path: { id: sessionID },
|
||||
|
||||
@@ -220,6 +220,53 @@ describe("ParentWakeNotifier — user message race guard (issue #4120)", () => {
|
||||
releaseAllPromptAsyncReservationsForTesting()
|
||||
})
|
||||
|
||||
test("#given all-complete wake arrives while prior assistant turn is still streaming #when the parent status is stale-idle #then the wake stays pending", async () => {
|
||||
// given
|
||||
const sessionMessages: SessionMessageStub[] = [
|
||||
{
|
||||
info: {
|
||||
role: "user",
|
||||
time: { created: Date.now() - 20_000 },
|
||||
},
|
||||
parts: [{ type: "text", text: "start work" }],
|
||||
},
|
||||
{
|
||||
info: {
|
||||
role: "assistant",
|
||||
time: { created: Date.now() - 5_000 },
|
||||
},
|
||||
parts: [{ type: "reasoning", text: "still gathering background results" }],
|
||||
},
|
||||
{
|
||||
info: {
|
||||
role: "user",
|
||||
time: { created: Date.now() - 4_000 },
|
||||
},
|
||||
parts: [{ type: "text", text: "partial wake\n<!-- OMO_INTERNAL_INITIATOR -->" }],
|
||||
},
|
||||
]
|
||||
const { notifier, promptAsyncCalls } = createNotifier({
|
||||
sessionStatuses: { "parent-stale-idle": { type: "idle" } },
|
||||
sessionMessages,
|
||||
})
|
||||
notifier.queuePendingParentWake(
|
||||
"parent-stale-idle",
|
||||
"<system-reminder>\n[ALL BACKGROUND TASKS COMPLETE]\n</system-reminder>",
|
||||
{ agent: "sisyphus" },
|
||||
true,
|
||||
)
|
||||
|
||||
// when
|
||||
await notifier.flushPendingParentWake("parent-stale-idle")
|
||||
|
||||
// then
|
||||
expect(promptAsyncCalls).toHaveLength(0)
|
||||
expect(notifier.getPendingParentWakes().has("parent-stale-idle")).toBe(true)
|
||||
|
||||
notifier.shutdown()
|
||||
releaseAllPromptAsyncReservationsForTesting()
|
||||
})
|
||||
|
||||
test("#given latest message is a user message just added #when flushing pending wake #then dispatch is deferred (no promptAsync)", async () => {
|
||||
// given
|
||||
const { notifier, promptAsyncCalls } = createNotifier({
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { afterEach, describe, expect, mock, test } from "bun:test"
|
||||
|
||||
import { releaseAllPromptAsyncReservationsForTesting } from "../../shared/prompt-async-gate"
|
||||
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
import { promptAsyncInDirectory, promptWithRetryInDirectory } from "./session-route"
|
||||
|
||||
describe("background-agent session routing", () => {
|
||||
afterEach(() => {
|
||||
releaseAllPromptAsyncReservationsForTesting()
|
||||
})
|
||||
|
||||
test("#given a routed prompt just dispatched #when the same child session is prompted again immediately #then promptAsync routing defers instead of enqueueing", async () => {
|
||||
// given
|
||||
const promptAsync = mock(async () => ({ data: "sent" }))
|
||||
const client = {
|
||||
session: {
|
||||
promptAsync,
|
||||
},
|
||||
}
|
||||
const args = {
|
||||
path: { id: "ses_background_route_hold" },
|
||||
body: { parts: [{ type: "text", text: "continue" }] },
|
||||
}
|
||||
|
||||
// when
|
||||
const first = await promptAsyncInDirectory(
|
||||
unsafeTestValue(client),
|
||||
unsafeTestValue(args),
|
||||
"/workspace/project",
|
||||
)
|
||||
const second = promptAsyncInDirectory(
|
||||
unsafeTestValue(client),
|
||||
unsafeTestValue(args),
|
||||
"/workspace/project",
|
||||
)
|
||||
|
||||
// then
|
||||
expect(first).toEqual({ data: "sent" })
|
||||
await expect(second).rejects.toThrow("promptAsync skipped by gate: reserved")
|
||||
expect(promptAsync).toHaveBeenCalledTimes(1)
|
||||
expect(promptAsync.mock.calls[0]?.[0].query).toEqual({ directory: "/workspace/project" })
|
||||
})
|
||||
|
||||
test("#given a background retry prompt just dispatched #when the same child session is prompted again immediately #then retry routing defers instead of enqueueing", async () => {
|
||||
// given
|
||||
const promptAsync = mock(async () => undefined)
|
||||
const client = {
|
||||
session: {
|
||||
promptAsync,
|
||||
},
|
||||
}
|
||||
const args = {
|
||||
path: { id: "ses_background_retry_route_hold" },
|
||||
body: { parts: [{ type: "text", text: "continue" }] },
|
||||
}
|
||||
|
||||
// when
|
||||
await promptWithRetryInDirectory(
|
||||
unsafeTestValue(client),
|
||||
unsafeTestValue(args),
|
||||
"/workspace/project",
|
||||
)
|
||||
const second = promptWithRetryInDirectory(
|
||||
unsafeTestValue(client),
|
||||
unsafeTestValue(args),
|
||||
"/workspace/project",
|
||||
)
|
||||
|
||||
// then
|
||||
await expect(second).rejects.toThrow("promptAsync skipped by gate: reserved")
|
||||
expect(promptAsync).toHaveBeenCalledTimes(1)
|
||||
expect(promptAsync.mock.calls[0]?.[0].query).toEqual({ directory: "/workspace/project" })
|
||||
})
|
||||
})
|
||||
@@ -42,6 +42,7 @@ export function promptAsyncInDirectory(
|
||||
input: routedArgs,
|
||||
source: "background-agent-session-route",
|
||||
settleMs: 0,
|
||||
queueBehavior: "defer",
|
||||
}).then((result) => {
|
||||
if (result.status === "failed") {
|
||||
throw result.error
|
||||
@@ -58,7 +59,7 @@ export function promptWithRetryInDirectory(
|
||||
args: PromptRetryArgs,
|
||||
directory: string,
|
||||
): Promise<void> {
|
||||
return promptWithModelSuggestionRetry(client, routePromptRetry(args, directory))
|
||||
return promptWithModelSuggestionRetry(client, routePromptRetry(args, directory), { queueBehavior: "defer" })
|
||||
}
|
||||
|
||||
export function messagesInDirectory(
|
||||
|
||||
Reference in New Issue
Block a user