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,11 @@
/// <reference path="../../../bun-test.d.ts" />
import { describe, expect, it } from "bun:test"
import { afterEach, describe, expect, it } from "bun:test"
import { setCompactionAgentConfigCheckpoint } from "../../shared/compaction-agent-config-checkpoint"
import {
releaseAllPromptAsyncReservationsForTesting,
releasePromptAsyncReservation,
} from "../shared/prompt-async-gate"
import { createCompactionContextInjector } from "./index"
type SessionMessageResponse = Array<{
@@ -98,6 +102,10 @@ function createMeaningfulPartUpdatedEvent(
}
describe("createCompactionContextInjector recovery", () => {
afterEach(() => {
releaseAllPromptAsyncReservationsForTesting()
})
it("re-injects after compaction when agent and model match but tools are missing", async () => {
//#given
const promptAsyncRecorder = createPromptAsyncRecorder()
@@ -304,6 +312,68 @@ describe("createCompactionContextInjector recovery", () => {
expect(promptAsyncRecorder.calls.length).toBe(1)
})
it("#given recovery promptAsync may have been accepted before EOF #when compaction repeats after the gate hold #then recovery is not duplicated", async () => {
//#given
const calls: PromptAsyncInput[] = []
const checkpointedPromptConfig = [
{
info: {
role: "user",
agent: "atlas",
model: { providerID: "openai", modelID: "gpt-5" },
tools: { bash: true },
},
},
]
const incompletePromptConfig = [
{
info: {
role: "user",
agent: "atlas",
model: { providerID: "openai", modelID: "gpt-5" },
},
},
]
const ctx = createMockContext(
[
checkpointedPromptConfig,
incompletePromptConfig,
incompletePromptConfig,
incompletePromptConfig,
incompletePromptConfig,
incompletePromptConfig,
],
async (input: PromptAsyncInput) => {
calls.push(input)
throw new Error("JSON Parse error: Unexpected EOF")
},
)
const injector = createCompactionContextInjector({ ctx })
const sessionID = "ses_recovery_eof_duplicate"
//#when
await injector.capture(sessionID)
await injector.event({
event: {
type: "session.compacted",
properties: { sessionID },
},
})
const released = releasePromptAsyncReservation(sessionID, "test:simulate-expired-hold", {
reservedBy: "compaction-context-injector",
})
await injector.event({
event: {
type: "session.compacted",
properties: { sessionID },
},
})
//#then
expect(released).toBe(true)
expect(calls.length).toBe(1)
})
it("does not treat reasoning-only assistant messages as a no-text tail", async () => {
//#given
const promptAsyncRecorder = createPromptAsyncRecorder()
@@ -7,6 +7,7 @@ import {
} from "../../shared/compaction-agent-config-checkpoint"
import { createInternalAgentContinuationTextPart } from "../../shared/internal-initiator-marker"
import { log } from "../../shared/logger"
import { isAmbiguousPromptDispatchFailure } from "../../shared/prompt-failure-classifier"
import { setSessionModel } from "../../shared/session-model-state"
import { setSessionTools } from "../../shared/session-tools-store"
import {
@@ -21,7 +22,7 @@ import {
import { AGENT_RECOVERY_PROMPT, NO_TEXT_TAIL_THRESHOLD, RECOVERY_COOLDOWN_MS, RECENT_COMPACTION_WINDOW_MS } from "./constants"
import type { CompactionContextClient } from "./types"
import type { TailMonitorState } from "./tail-monitor"
import { dispatchInternalPrompt } from "../shared/prompt-async-gate"
import { dispatchInternalPrompt, isInternalPromptDispatchAccepted } from "../shared/prompt-async-gate"
export function createRecoveryLogic(
ctx: CompactionContextClient | undefined,
@@ -99,7 +100,10 @@ export function createRecoveryLogic(
query: { directory: ctx.directory },
},
})
if (promptResult.status !== "dispatched") {
if (!isInternalPromptDispatchAccepted(promptResult)) {
if (promptResult.status === "failed" && isAmbiguousPromptDispatchFailure(promptResult.error)) {
tailState.lastRecoveryAt = now
}
log(`[compaction-context-injector] Recovery skipped by promptAsync gate`, {
sessionID,
reason,