mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 02:44:50 +01:00
fix: advance manual feedback retry keys
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
2c081e4b47
commit
d3366b1a8e
3 files changed
+62
-2
No files matched your search
@@ -2,10 +2,11 @@ import { createHash, randomUUID } from "node:crypto";
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import multer from "multer";
|
||||
import { z } from "zod";
|
||||
import { and, asc, desc, eq, inArray, isNull, notInArray } from "drizzle-orm";
|
||||
import { and, asc, desc, eq, inArray, isNull, like, notInArray } from "drizzle-orm";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import {
|
||||
activityLog,
|
||||
agentWakeupRequests,
|
||||
agents,
|
||||
approvals,
|
||||
companyMemberships,
|
||||
@@ -113,6 +114,7 @@ import {
|
||||
FEEDBACK_DELIVERY_RECOVERY_CAUSE,
|
||||
FEEDBACK_DELIVERY_RETRY_WAKE_REASON,
|
||||
FEEDBACK_DELIVERY_WAKE_COMMENT_IDS_KEY,
|
||||
nextFeedbackDeliveryManualRetryAttempt,
|
||||
remainingFeedbackDispositionCommentIds,
|
||||
readFeedbackDeliveryRunContext,
|
||||
} from "../services/recovery/feedback-delivery.js";
|
||||
@@ -10978,6 +10980,18 @@ export function issueRoutes(
|
||||
}
|
||||
|
||||
const latestCommentId = outstandingCommentIds.at(-1) ?? null;
|
||||
const manualRetryKeyPrefix = `${activeAction.fingerprint}:manual:`;
|
||||
const priorManualRetryKeys = await db
|
||||
.select({ idempotencyKey: agentWakeupRequests.idempotencyKey })
|
||||
.from(agentWakeupRequests)
|
||||
.where(and(
|
||||
eq(agentWakeupRequests.companyId, issue.companyId),
|
||||
like(agentWakeupRequests.idempotencyKey, `${manualRetryKeyPrefix}%`),
|
||||
));
|
||||
const manualRetryAttempt = nextFeedbackDeliveryManualRetryAttempt({
|
||||
fingerprint: activeAction.fingerprint,
|
||||
existingIdempotencyKeys: priorManualRetryKeys.map((row) => row.idempotencyKey),
|
||||
});
|
||||
const deliveryContext = {
|
||||
issueId: issue.id,
|
||||
taskId: issue.id,
|
||||
@@ -10994,7 +11008,7 @@ export function issueRoutes(
|
||||
payload: deliveryContext,
|
||||
idempotencyKey: buildFeedbackDeliveryManualRetryIdempotencyKey({
|
||||
fingerprint: activeAction.fingerprint,
|
||||
attempt: (activeAction.attemptCount ?? 0) + 1,
|
||||
attempt: manualRetryAttempt,
|
||||
}),
|
||||
requestedByActorType: actor.actorType === "user" ? "user" : "system",
|
||||
requestedByActorId: actor.actorId,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
decideStrandedFeedbackDeliveryBackstop,
|
||||
hasCompleteFeedbackDispositionCoverage,
|
||||
isSuccessfulFeedbackDeliveryRecoveryMatch,
|
||||
nextFeedbackDeliveryManualRetryAttempt,
|
||||
remainingFeedbackDispositionCommentIds,
|
||||
readFeedbackDeliveryRunContext,
|
||||
type StrandedFeedbackDeliveryBackstopProbe,
|
||||
@@ -204,6 +205,31 @@ describe("feedback disposition coverage", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("manual feedback retry attempts", () => {
|
||||
it("advances past durable failed wakes instead of relying on the recovery-action counter", () => {
|
||||
const fingerprint = "feedback_delivery:company:issue:agent:root";
|
||||
expect(nextFeedbackDeliveryManualRetryAttempt({
|
||||
fingerprint,
|
||||
existingIdempotencyKeys: [
|
||||
`${fingerprint}:manual:2`,
|
||||
`${fingerprint}:manual:5`,
|
||||
`${fingerprint}:manual:not-a-number`,
|
||||
"feedback_delivery:other:manual:99",
|
||||
null,
|
||||
],
|
||||
})).toBe(6);
|
||||
});
|
||||
|
||||
it("gives concurrent requests the same first attempt for database deduplication", () => {
|
||||
const input = {
|
||||
fingerprint: "feedback_delivery:company:issue:agent:root",
|
||||
existingIdempotencyKeys: [],
|
||||
};
|
||||
expect(nextFeedbackDeliveryManualRetryAttempt(input)).toBe(1);
|
||||
expect(nextFeedbackDeliveryManualRetryAttempt(input)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("stranded feedback delivery backstop", () => {
|
||||
it("requeues exactly one normal-model recovery for the original assignee", () => {
|
||||
const decision = decide();
|
||||
|
||||
@@ -77,6 +77,26 @@ export function buildFeedbackDeliveryManualRetryIdempotencyKey(input: {
|
||||
return `${input.fingerprint}:manual:${input.attempt}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate from durable wake keys rather than the recovery-action counter.
|
||||
* Concurrent requests observe the same next value and collapse at the unique
|
||||
* index; a later request advances even if a failed wake never updated its
|
||||
* recovery action.
|
||||
*/
|
||||
export function nextFeedbackDeliveryManualRetryAttempt(input: {
|
||||
fingerprint: string;
|
||||
existingIdempotencyKeys: Array<string | null>;
|
||||
}) {
|
||||
const prefix = `${input.fingerprint}:manual:`;
|
||||
let maxAttempt = 0;
|
||||
for (const key of input.existingIdempotencyKeys) {
|
||||
if (!key?.startsWith(prefix)) continue;
|
||||
const attempt = Number(key.slice(prefix.length));
|
||||
if (Number.isSafeInteger(attempt) && attempt > maxAttempt) maxAttempt = attempt;
|
||||
}
|
||||
return maxAttempt + 1;
|
||||
}
|
||||
|
||||
export function hasCompleteFeedbackDispositionCoverage(input: {
|
||||
deliveryCommentIds: string[];
|
||||
handledCommentIds: string[];
|
||||
|
||||
Reference in new issue
Block a user