mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 17:45:53 +01:00
fix(interactions): retain suppressed answer deliveries
This commit is contained in:
1 parent
79c3783fff
commit
893b54212b
2 files changed
+52
-13
No files matched your search
@@ -250,6 +250,47 @@ describeEmbeddedPostgres("question response delivery", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("delivers after wake suppression outlasts the bounded error retry limit", async () => {
|
||||
const seeded = await seed({ sourceStatus: "running" });
|
||||
const fallbackRunId = randomUUID();
|
||||
let wakeAttempts = 0;
|
||||
const wakeup = vi.fn().mockImplementation(async () => {
|
||||
wakeAttempts += 1;
|
||||
if (wakeAttempts <= 5) return null;
|
||||
return db.insert(heartbeatRuns).values({
|
||||
id: fallbackRunId,
|
||||
companyId: seeded.companyId,
|
||||
agentId: seeded.agentId,
|
||||
invocationSource: "automation",
|
||||
status: "queued",
|
||||
runtimeMode: "legacy",
|
||||
driverKind: "codex",
|
||||
contextSnapshot: { issueId: seeded.issueId },
|
||||
}).returning().then((rows) => rows[0]!);
|
||||
});
|
||||
const service = questionResponseDeliveryService(db, {
|
||||
heartbeat: { wakeup } as never,
|
||||
steer: vi.fn(),
|
||||
});
|
||||
|
||||
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||
await expect(service.deliver(seeded.interaction.id)).resolves.toBeNull();
|
||||
}
|
||||
const delivered = await service.deliver(seeded.interaction.id);
|
||||
|
||||
expect(delivered).toMatchObject({
|
||||
status: "fallback_queued",
|
||||
mode: "wake_fallback",
|
||||
targetRunId: fallbackRunId,
|
||||
});
|
||||
const [delivery] = await db.select().from(issueQuestionResponseDeliveries);
|
||||
expect(delivery).toMatchObject({
|
||||
status: "fallback_queued",
|
||||
attemptCount: 6,
|
||||
targetRunId: fallbackRunId,
|
||||
});
|
||||
});
|
||||
|
||||
it("reuses a durable wake receipt instead of issuing a duplicate continuation", async () => {
|
||||
const seeded = await seed({ sourceStatus: "running" });
|
||||
const [wakeRequest] = await db.insert(agentWakeupRequests).values({
|
||||
|
||||
@@ -365,9 +365,13 @@ export function questionResponseDeliveryService(
|
||||
};
|
||||
}
|
||||
|
||||
async function releaseForRetry(delivery: DeliveryRow, errorCode: string) {
|
||||
async function releaseForRetry(
|
||||
delivery: DeliveryRow,
|
||||
errorCode: string,
|
||||
options: { bounded: boolean } = { bounded: true },
|
||||
) {
|
||||
const at = now();
|
||||
const exhausted = delivery.attemptCount >= MAX_DELIVERY_ATTEMPTS;
|
||||
const exhausted = options.bounded && delivery.attemptCount >= MAX_DELIVERY_ATTEMPTS;
|
||||
if (!exhausted) {
|
||||
await db.update(issueQuestionResponseDeliveries).set({
|
||||
status: "pending",
|
||||
@@ -630,17 +634,11 @@ export function questionResponseDeliveryService(
|
||||
}));
|
||||
if (!wakeRun && !durableWake) {
|
||||
const errorCode = "question_response_wake_skipped";
|
||||
const exhausted = await releaseForRetry(claimed, errorCode);
|
||||
if (!exhausted) return null;
|
||||
return recordTerminal({
|
||||
delivery: claimed,
|
||||
interaction,
|
||||
status: "failed",
|
||||
mode: null,
|
||||
targetRunId: null,
|
||||
adapter,
|
||||
errorCode,
|
||||
});
|
||||
// Scheduling suppression is an availability state, not a delivery
|
||||
// failure. Keep the durable receipt retryable until the suppression is
|
||||
// lifted; the bounded limit remains reserved for actual wake errors.
|
||||
await releaseForRetry(claimed, errorCode, { bounded: false });
|
||||
return null;
|
||||
}
|
||||
const targetRun = wakeRun ?? durableWake?.run ?? queuedSuccessor ?? null;
|
||||
const coalesced = Boolean(queuedSuccessor && targetRun?.id === queuedSuccessor.id);
|
||||
|
||||
Reference in new issue
Block a user