fix(prompt-gate): harden internal prompt dispatch
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user