fix(prompt-gate): harden internal prompt dispatch

This commit is contained in:
YeonGyu-Kim
2026-05-19 16:02:18 +09:00
parent 6c63372ef9
commit 1492bffd20
49 changed files with 1488 additions and 141 deletions
@@ -1,7 +1,12 @@
import { describe, expect, test } from "bun:test"
import { afterEach, describe, expect, test } from "bun:test"
import { releaseAllPromptAsyncReservationsForTesting } from "../shared/prompt-async-gate"
import { injectContinuationPrompt } from "./continuation-prompt-injector"
describe("ralph-loop continuation prompt injector", () => {
afterEach(() => {
releaseAllPromptAsyncReservationsForTesting()
})
test("#given promptAsync resolves SDK error #when injecting continuation prompt #then it returns rejection without throwing", async () => {
// given
const ctx = {
@@ -59,6 +64,32 @@ describe("ralph-loop continuation prompt injector", () => {
}
})
test("#given promptAsync may have accepted before EOF #when injecting continuation prompt #then it returns dispatched", async () => {
// given
const ctx = {
client: {
session: {
messages: async () => ({ data: [] }),
promptAsync: async () => {
throw new Error("JSON Parse error: Unexpected EOF")
},
},
},
}
// when
const result = await injectContinuationPrompt(ctx as never, {
sessionID: "ses_ralph_eof",
prompt: "continue",
directory: "/tmp/test",
apiTimeoutMs: 50,
})
// then
expect(result.status).toBe("dispatched")
})
test("#given inherited message agent has ZWSP prefix #when injecting continuation prompt #then promptAsync receives registered display agent", async () => {
// given
let promptBody: { agent?: string; noReply?: boolean } | undefined
@@ -5,6 +5,7 @@ import { getMessageDir } from "./message-storage-directory"
import { withTimeout } from "./with-timeout"
import {
createInternalAgentContinuationTextPart,
isAmbiguousPromptDispatchFailure,
isRecord,
normalizeSDKResponse,
resolveInheritedPromptTools,
@@ -145,6 +146,7 @@ export async function injectContinuationPrompt(
sessionID: options.sessionID,
source: "ralph-loop",
settleMs: options.idleSettleMs,
queueBehavior: "defer",
input: {
path: { id: options.sessionID },
body: {
@@ -158,8 +160,14 @@ export async function injectContinuationPrompt(
},
})
if (promptResult.status === "failed") {
if (isAmbiguousPromptDispatchFailure(promptResult.error)) {
return { status: "dispatched" }
}
throw promptResult.error
}
if (promptResult.status === "queued") {
return { status: "deferred", reason: "reserved" }
}
if (promptResult.status === "active" || promptResult.status === "reserved") {
return { status: "deferred", reason: promptResult.status }
}
@@ -171,6 +179,9 @@ export async function injectContinuationPrompt(
}
response = promptResult.response
} catch (error) {
if (isAmbiguousPromptDispatchFailure(error)) {
return { status: "dispatched" }
}
const promptError = error instanceof Error
? error
: createPromptAsyncError("promptAsync rejected", error)