perf: reduce unused task payloads in recovery state checks (#17694)

Use the existing disposition-state fields for source and child task checks. Preserve full action evidence and the current full-task reload before repair.

Verified 61 focused recovery tests, root typecheck, server build, the base counterfactual, full CI, and fresh Apex 5/5 review.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin FoleyandPaperclip authored and GitHub committed 2026-10-25 21:32:21 -07:00
1 parent 7703220d38
commit 6bdcb70244
3 files changed
+101 -4

No files matched your search

@@ -30,6 +30,17 @@ type DispositionRepairIssue = Pick<
| "monitorNextCheckAt"
>;
export const dispositionRepairIssueFields = {
id: issues.id,
companyId: issues.companyId,
status: issues.status,
assigneeAgentId: issues.assigneeAgentId,
assigneeUserId: issues.assigneeUserId,
executionPolicy: issues.executionPolicy,
executionState: issues.executionState,
monitorNextCheckAt: issues.monitorNextCheckAt,
};
export type DispositionRepairSourceState = {
fingerprint: string;
dependencyIssueIds: string[];
@@ -0,0 +1,79 @@
import { randomBytes, randomUUID } from "node:crypto";
import { companies, createDb, issueRecoveryActions, issues } from "@paperclipai/db";
import { createDatabaseResponseCapture, installDatabaseResponseCapture } from "@paperclipai/db/response-capture";
import { eq } from "drizzle-orm";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "../../__tests__/helpers/embedded-postgres.js";
import { recoveryService } from "./service.js";
const support = await getEmbeddedPostgresTestSupport();
describe.skipIf(!support.supported)("recovery issue state reads", () => {
let database: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>>;
let db: ReturnType<typeof createDb>;
beforeAll(async () => {
database = await startEmbeddedPostgresTestDatabase("recovery-issue-state-");
db = createDb(database.connectionString);
});
afterAll(async () => {
await db?.$client.end();
await database?.cleanup();
});
it.each([false, true])("does not load unused source and child descriptions (child=%s)", async (withChild) => {
const [company] = await db.insert(companies).values({
name: "Recovery state reads",
issuePrefix: `R${randomUUID().slice(0, 7).toUpperCase()}`,
}).returning();
const description = randomBytes(128 * 1024).toString("base64");
const [parent] = await db.insert(issues).values({
companyId: company.id,
title: "Await the board decision",
description,
status: "blocked",
}).returning();
const child = withChild
? (await db.insert(issues).values({
companyId: company.id,
title: "A child that also needs a decision",
description,
status: "blocked",
parentId: parent.id,
}).returning())[0]
: null;
const [action] = await db.insert(issueRecoveryActions).values({
companyId: company.id,
sourceIssueId: parent.id,
kind: "stranded_assigned_issue",
cause: "stranded_assigned_issue",
fingerprint: randomUUID(),
status: "active",
ownerType: "board",
wakePolicy: { type: "board_escalation" },
nextAction: "Choose the next disposition",
evidence: { retainedControl: { proof: "unchanged" } },
}).returning();
const enqueueWakeup = vi.fn(async () => {
throw new Error("Board-owned work must remain pending");
});
const service = recoveryService(db, { enqueueWakeup });
const events: Array<Record<string, unknown>> = [];
const now = Date.now();
const capture = createDatabaseResponseCapture({ start: now - 100, end: now + 59_000 }, event => events.push(event));
installDatabaseResponseCapture(db, { issues, issueRecoveryActions }, capture);
try {
await service.reconcileStrandedAssignedIssues();
} finally {
capture.flush();
}
expect(Number(events.at(-1)?.completedQueries)).toBeGreaterThan(5);
expect(Number(events.at(-1)?.responseValueBytes)).toBeLessThan(16_384);
expect(enqueueWakeup).not.toHaveBeenCalled();
expect((await db.select().from(issues).where(eq(issues.id, parent.id)))[0]).toEqual(parent);
if (child) {
expect((await db.select().from(issues).where(eq(issues.id, child.id)))[0]).toEqual(child);
}
expect((await db.select().from(issueRecoveryActions).where(eq(issueRecoveryActions.id, action.id)))[0]).toEqual(action);
});
});
+11 -4
View File
@@ -159,6 +159,7 @@ import { withRecoveryContext } from "./status-only-context.js";
import { isAutomaticRecoverySuppressedByPauseHold } from "./pause-hold-guard.js";
import {
collectDispositionRepairSourceState,
dispositionRepairIssueFields,
dispositionRepairDelayMs,
DISPOSITION_REPAIR_MAX_ATTEMPTS,
} from "./disposition-repair.js";
@@ -2967,10 +2968,13 @@ export function recoveryService(
);
}
async function healthyOpenChildIssues(issue: typeof issues.$inferSelect, sameWorkspaceOnly = false) {
async function healthyOpenChildIssues(
issue: Pick<typeof issues.$inferSelect, "id" | "companyId" | "projectWorkspaceId">,
sameWorkspaceOnly = false,
) {
if (sameWorkspaceOnly && !issue.projectWorkspaceId) return [];
const childCandidates = await db
.select()
.select({ ...dispositionRepairIssueFields, identifier: issues.identifier })
.from(issues)
.where(
and(
@@ -3483,7 +3487,10 @@ export function recoveryService(
async function reconcileActiveRecoveryActions() {
const rows = await db
.select({ action: issueRecoveryActions, issue: issues })
.select({
action: issueRecoveryActions,
issue: { ...dispositionRepairIssueFields, projectWorkspaceId: issues.projectWorkspaceId },
})
.from(issueRecoveryActions)
.innerJoin(
issues,
@@ -3869,7 +3876,7 @@ export function recoveryService(
}
async function reconcileDispositionRepair(
issue: typeof issues.$inferSelect,
issue: Pick<typeof issues.$inferSelect, "id" | "companyId">,
latestRun: LatestIssueRun,
options: { historicalAttemptCount?: number; legacyEpisode?: LegacyDispositionEpisode } = {},
): Promise<"queued" | "escalated" | "covered" | "skipped"> {