From 686e06c8ea55b6dae07463303e6da6d358e01362 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 19 May 2026 16:30:47 -0500 Subject: [PATCH] Tighten runtime feedback and interaction catchup Co-Authored-By: Paperclip --- .../feedback-flush-controller.test.ts | 6 ++++ server/src/app.ts | 2 -- .../src/services/issue-thread-interactions.ts | 29 +++++++++++++++---- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/server/src/__tests__/feedback-flush-controller.test.ts b/server/src/__tests__/feedback-flush-controller.test.ts index 5fa0f2eb720..b8f12908e45 100644 --- a/server/src/__tests__/feedback-flush-controller.test.ts +++ b/server/src/__tests__/feedback-flush-controller.test.ts @@ -15,4 +15,10 @@ describe("feedback export flush error classification", () => { it("does not classify ordinary feedback upload failures as database outages", () => { expect(isDatabaseConnectionUnavailableError(new Error("upstream returned 500"))).toBe(false); }); + + it("does not trust unrelated error messages that mention ECONNREFUSED", () => { + expect(isDatabaseConnectionUnavailableError( + new Error("feedback upload payload mentioned ECONNREFUSED in user content"), + )).toBe(false); + }); }); diff --git a/server/src/app.ts b/server/src/app.ts index 15e94216bcc..421a72a4390 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -86,8 +86,6 @@ const VITE_DEV_STATIC_PATHS = new Set([ export function isDatabaseConnectionUnavailableError(err: unknown): boolean { const error = err as { code?: unknown; message?: unknown; cause?: unknown }; if (error?.code === "ECONNREFUSED") return true; - const message = typeof error?.message === "string" ? error.message : ""; - if (message.includes("ECONNREFUSED")) return true; return Boolean(error?.cause && isDatabaseConnectionUnavailableError(error.cause)); } diff --git a/server/src/services/issue-thread-interactions.ts b/server/src/services/issue-thread-interactions.ts index 38e121dc95b..6fc377c259a 100644 --- a/server/src/services/issue-thread-interactions.ts +++ b/server/src/services/issue-thread-interactions.ts @@ -1068,6 +1068,13 @@ export function issueThreadInteractionService(db: Db) { const now = new Date(); const expired: IssueThreadInteraction[] = []; + const supersededByComment = new Map< + string, + { + comment: (typeof comments)[number]; + rowIds: string[]; + } + >(); for (const row of rows) { const interaction = hydrateInteraction(row) as RequestConfirmationInteraction; if (!shouldSupersedeRequestConfirmationOnUserComment(interaction)) continue; @@ -1078,26 +1085,38 @@ export function issueThreadInteractionService(db: Db) { })); if (!supersedingComment) continue; - const [updated] = await db + const group = supersededByComment.get(supersedingComment.id); + if (group) { + group.rowIds.push(row.id); + } else { + supersededByComment.set(supersedingComment.id, { + comment: supersedingComment, + rowIds: [row.id], + }); + } + } + + for (const { comment, rowIds } of supersededByComment.values()) { + const updatedRows = await db .update(issueThreadInteractions) .set({ status: "expired", result: { version: 1, outcome: "superseded_by_comment", - commentId: supersedingComment.id, + commentId: comment.id, }, resolvedByAgentId: null, - resolvedByUserId: supersedingComment.authorUserId, + resolvedByUserId: comment.authorUserId, resolvedAt: now, updatedAt: now, }) .where(and( - eq(issueThreadInteractions.id, row.id), + inArray(issueThreadInteractions.id, rowIds), eq(issueThreadInteractions.status, "pending"), )) .returning(); - if (updated) expired.push(hydrateInteraction(updated)); + expired.push(...updatedRows.map(hydrateInteraction)); } if (expired.length > 0) {