fix(prompt-gate): pin duplicate prompt dispatches

Keep prompt reservations briefly after successful dispatch so rapid idle/message/error transitions cannot inject the same follow-up twice.

Route all production session prompt calls through the shared gate, restore skipped background resume state, release holds after abort/recovery paths, and preserve Ralph/ULW loop state when a dispatch is deferred.

Add regression coverage for session routing, static prompt route auditing, team-mode live messaging, model suggestion retries, call-omo-agent reuse, background parent wakes, runtime fallback, compaction recovery, Atlas, and Ralph/ULW loops.
This commit is contained in:
YeonGyu-Kim
2026-05-15 13:19:10 +09:00
parent 05189700fb
commit c2aa180e7e
26 changed files with 893 additions and 48 deletions
+25
View File
@@ -871,6 +871,24 @@ describe("ralph-loop", () => {
expect(state?.iteration).toBe(2)
})
test("#given duplicate real idle fires before assistant activity #then loop state is preserved without another prompt", async () => {
// given - active loop
const hook = createRalphLoopHook(createMockPluginInput(), { idleSettleMs: 0 })
hook.startLoop("session-123", "Build feature", { maxIterations: 5 })
// when - duplicate idle events arrive without any intervening activity
await hook.event({
event: { type: "session.idle", properties: { sessionID: "session-123" } },
})
await hook.event({
event: { type: "session.idle", properties: { sessionID: "session-123" } },
})
// then - the second dispatch is deferred, not treated as loop failure
expect(hook.getState()?.iteration).toBe(2)
expect(promptCalls.length).toBe(1)
})
test("should handle multiple iterations correctly", async () => {
// given - active loop
const hook = createRalphLoopHook(createMockPluginInput())
@@ -880,6 +898,9 @@ describe("ralph-loop", () => {
await hook.event({
event: { type: "session.idle", properties: { sessionID: "session-123" } },
})
await hook.event({
event: { type: "message.part.updated", properties: { sessionID: "session-123" } },
})
await hook.event({
event: { type: "session.idle", properties: { sessionID: "session-123" } },
})
@@ -1127,6 +1148,9 @@ describe("ralph-loop", () => {
await hook.event({
event: { type: "session.idle", properties: { sessionID: "session-A" } },
})
await hook.event({
event: { type: "message.part.updated", properties: { sessionID: "session-A" } },
})
await hook.event({
event: { type: "session.idle", properties: { sessionID: "session-A" } },
})
@@ -1328,6 +1352,7 @@ Original task: Build something`
// when - delayed start snapshot resolves after the loop has already advanced
resolveInitialMessages?.({ data: mockSessionMessages })
await new Promise((resolve) => setTimeout(resolve, 0))
await hook.event({ event: { type: "message.part.updated", properties: { sessionID: "session-123" } } })
await hook.event({ event: { type: "session.idle", properties: { sessionID: "session-123" } } })
// then - the late snapshot must not hide the DONE message from verification gating