mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 11:20:42 +01:00
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:
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);
|
||||
});
|
||||
});
|
||||
@@ -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"> {
|
||||
|
||||
Reference in new issue
Block a user