mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 19:35:13 +01:00
perf: omit unused descriptions from recovery candidates (#17695)
Keep unused task descriptions out of the recurring candidate read. Preserve the task content, state gates, and full current-task reload before repair. Verified 86 focused recovery tests, root typecheck, server build, a failing base counterfactual, all 55 CI checks, and fresh Apex 5/5 review with no unresolved comments. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
6bdcb70244
commit
1fbbef45a8
2 files changed
+75
-33
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { randomBytes, randomUUID } from "node:crypto";
|
||||
import { companies, createDb, issueRecoveryActions, issues } from "@paperclipai/db";
|
||||
import { agents, 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";
|
||||
@@ -76,4 +76,42 @@ describe.skipIf(!support.supported)("recovery issue state reads", () => {
|
||||
}
|
||||
expect((await db.select().from(issueRecoveryActions).where(eq(issueRecoveryActions.id, action.id)))[0]).toEqual(action);
|
||||
});
|
||||
|
||||
it("keeps a candidate waiting for agent setup without reading its description", async () => {
|
||||
const [company] = await db.insert(companies).values({
|
||||
name: "Recovery candidate read",
|
||||
issuePrefix: `R${randomUUID().slice(0, 7).toUpperCase()}`,
|
||||
}).returning();
|
||||
const [agent] = await db.insert(agents).values({
|
||||
companyId: company.id,
|
||||
name: "Preparing agent",
|
||||
lifecycleState: "preparing",
|
||||
}).returning();
|
||||
const [issue] = await db.insert(issues).values({
|
||||
companyId: company.id,
|
||||
title: "Wait for agent setup",
|
||||
description: randomBytes(128 * 1024).toString("base64"),
|
||||
status: "todo",
|
||||
assigneeAgentId: agent.id,
|
||||
}).returning();
|
||||
const enqueueWakeup = vi.fn(async () => {
|
||||
throw new Error("Setup still owns this task");
|
||||
});
|
||||
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, { agents, issues, issueRecoveryActions }, capture);
|
||||
try {
|
||||
expect(await service.reconcileStrandedAssignedIssues()).toMatchObject({ skipped: 1 });
|
||||
} 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, issue.id)))[0]).toEqual(issue);
|
||||
expect((await db.select().from(agents).where(eq(agents.id, agent.id)))[0]).toEqual(agent);
|
||||
expect(await db.select().from(issueRecoveryActions).where(eq(issueRecoveryActions.companyId, company.id))).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -864,7 +864,7 @@ function formatIssueLinksForComment(
|
||||
}
|
||||
|
||||
function isStrandedIssueRecoveryIssue(
|
||||
issue: Pick<typeof issues.$inferSelect, "originKind">,
|
||||
issue: Pick<RecoveryIssue, "originKind">,
|
||||
) {
|
||||
return isStrandedIssueRecoveryOriginKind(issue.originKind);
|
||||
}
|
||||
@@ -936,6 +936,10 @@ function isRepeatedProductiveContinuationRecovery(
|
||||
);
|
||||
}
|
||||
|
||||
type RecoveryIssue = Omit<typeof issues.$inferSelect, "description">;
|
||||
|
||||
const { description: _unusedIssueDescription, ...recoveryIssueFields } = getTableColumns(issues);
|
||||
|
||||
export function recoveryService(
|
||||
db: Db,
|
||||
deps: {
|
||||
@@ -1251,7 +1255,7 @@ export function recoveryService(
|
||||
* Keep it separate from the broader monitor/delegated/legacy wait predicate.
|
||||
*/
|
||||
async function hasCurrentNativePassiveWait(
|
||||
issue: typeof issues.$inferSelect,
|
||||
issue: RecoveryIssue,
|
||||
latestRun: LatestIssueRun,
|
||||
): Promise<boolean> {
|
||||
if (
|
||||
@@ -1461,7 +1465,7 @@ export function recoveryService(
|
||||
}
|
||||
|
||||
async function hasPersistedDurableWaitPath(
|
||||
issue: typeof issues.$inferSelect,
|
||||
issue: RecoveryIssue,
|
||||
latestRun: LatestIssueRun,
|
||||
) {
|
||||
if (issue.monitorNextCheckAt) return true;
|
||||
@@ -1526,7 +1530,7 @@ export function recoveryService(
|
||||
}
|
||||
|
||||
async function nativeBlockedUnblockAction(
|
||||
issue: typeof issues.$inferSelect,
|
||||
issue: RecoveryIssue,
|
||||
latestRun: LatestIssueRun,
|
||||
database: Db = db,
|
||||
) {
|
||||
@@ -1597,7 +1601,7 @@ export function recoveryService(
|
||||
}
|
||||
|
||||
async function repairNativeBlockedWait(
|
||||
issue: typeof issues.$inferSelect,
|
||||
issue: RecoveryIssue,
|
||||
latestRun: NonNullable<LatestIssueRun>,
|
||||
candidate: NonNullable<
|
||||
Awaited<ReturnType<typeof nativeBlockedUnblockAction>>
|
||||
@@ -1801,7 +1805,7 @@ export function recoveryService(
|
||||
}
|
||||
|
||||
async function wasTodoHandedBackDuringOrAfterLatestRun(
|
||||
issue: typeof issues.$inferSelect,
|
||||
issue: RecoveryIssue,
|
||||
latestRun: LatestIssueRun,
|
||||
) {
|
||||
if (issue.status !== "todo" || latestRun?.status !== "succeeded")
|
||||
@@ -2133,7 +2137,7 @@ export function recoveryService(
|
||||
}
|
||||
|
||||
async function enqueueInitialAssignedTodoDispatch(
|
||||
issue: typeof issues.$inferSelect,
|
||||
issue: RecoveryIssue,
|
||||
agentId: string,
|
||||
) {
|
||||
return deps.enqueueWakeup(agentId, {
|
||||
@@ -2166,7 +2170,7 @@ export function recoveryService(
|
||||
// contract is that nothing runs until the user types. Until a user-authored
|
||||
// comment exists on it, the issue is intentionally idle rather than stranded.
|
||||
async function isOnboardingFirstTaskAwaitingUser(
|
||||
issue: typeof issues.$inferSelect,
|
||||
issue: RecoveryIssue,
|
||||
) {
|
||||
if (issue.originKind !== ONBOARDING_FIRST_TASK_ORIGIN_KIND) return false;
|
||||
const [result] = await db.execute<{ awaiting: boolean }>(sql`SELECT
|
||||
@@ -2175,7 +2179,7 @@ export function recoveryService(
|
||||
}
|
||||
|
||||
async function isInvocationBudgetBlocked(
|
||||
issue: typeof issues.$inferSelect,
|
||||
issue: RecoveryIssue,
|
||||
agentId: string,
|
||||
) {
|
||||
const budgetBlock = await budgets.getInvocationBlock(
|
||||
@@ -2409,12 +2413,12 @@ export function recoveryService(
|
||||
}
|
||||
}
|
||||
|
||||
function isStrandedIssueRecoveryIssue(issue: typeof issues.$inferSelect) {
|
||||
function isStrandedIssueRecoveryIssue(issue: RecoveryIssue) {
|
||||
return issue.originKind === STRANDED_ISSUE_RECOVERY_ORIGIN_KIND;
|
||||
}
|
||||
|
||||
async function buildNestedStrandedRecoveryLine(
|
||||
issue: typeof issues.$inferSelect,
|
||||
issue: RecoveryIssue,
|
||||
prefix: string,
|
||||
) {
|
||||
const sourceIssueId = readNonEmptyString(issue.originId);
|
||||
@@ -2445,7 +2449,7 @@ export function recoveryService(
|
||||
}
|
||||
|
||||
function resolveStrandedRecoveryRouting(input: {
|
||||
issue: typeof issues.$inferSelect;
|
||||
issue: RecoveryIssue;
|
||||
latestRun: LatestIssueRun;
|
||||
}) {
|
||||
const originalAgentId =
|
||||
@@ -2468,7 +2472,7 @@ export function recoveryService(
|
||||
}
|
||||
|
||||
function strandedRecoveryActionFingerprint(input: {
|
||||
issue: typeof issues.$inferSelect;
|
||||
issue: RecoveryIssue;
|
||||
recoveryCause: StrandedRecoveryCause;
|
||||
latestRun: LatestIssueRun;
|
||||
}) {
|
||||
@@ -2514,7 +2518,7 @@ export function recoveryService(
|
||||
}
|
||||
|
||||
function buildStrandedRecoveryActionEvidence(input: {
|
||||
issue: typeof issues.$inferSelect;
|
||||
issue: RecoveryIssue;
|
||||
latestRun: LatestIssueRun;
|
||||
previousStatus: StrandedPreviousStatus;
|
||||
recoveryCause: StrandedRecoveryCause;
|
||||
@@ -2549,7 +2553,7 @@ export function recoveryService(
|
||||
}
|
||||
|
||||
async function ensureSourceScopedStrandedRecoveryAction(input: {
|
||||
issue: typeof issues.$inferSelect;
|
||||
issue: RecoveryIssue;
|
||||
latestRun: LatestIssueRun;
|
||||
previousStatus: StrandedPreviousStatus;
|
||||
recoveryCause?: StrandedRecoveryCause;
|
||||
@@ -2680,7 +2684,7 @@ export function recoveryService(
|
||||
}
|
||||
|
||||
async function ensureProviderQuotaWaitRecoveryMonitor(input: {
|
||||
issue: typeof issues.$inferSelect;
|
||||
issue: RecoveryIssue;
|
||||
latestRun: LatestIssueRun;
|
||||
actionId: string;
|
||||
agentId: string;
|
||||
@@ -2780,7 +2784,7 @@ export function recoveryService(
|
||||
}
|
||||
|
||||
function buildRecoveryIssueInPlaceEscalationComment(input: {
|
||||
issue: typeof issues.$inferSelect;
|
||||
issue: RecoveryIssue;
|
||||
previousStatus: StrandedPreviousStatus;
|
||||
latestRun: LatestIssueRun;
|
||||
prefix: string;
|
||||
@@ -2815,7 +2819,7 @@ export function recoveryService(
|
||||
}
|
||||
|
||||
async function escalateStrandedRecoveryIssueInPlace(input: {
|
||||
issue: typeof issues.$inferSelect;
|
||||
issue: RecoveryIssue;
|
||||
previousStatus: StrandedPreviousStatus;
|
||||
latestRun: LatestIssueRun;
|
||||
}, transaction?: {
|
||||
@@ -2954,7 +2958,7 @@ export function recoveryService(
|
||||
);
|
||||
}
|
||||
|
||||
async function openChildIssues(issue: typeof issues.$inferSelect) {
|
||||
async function openChildIssues(issue: RecoveryIssue) {
|
||||
return db
|
||||
.select({ id: issues.id, identifier: issues.identifier })
|
||||
.from(issues)
|
||||
@@ -2969,7 +2973,7 @@ export function recoveryService(
|
||||
}
|
||||
|
||||
async function healthyOpenChildIssues(
|
||||
issue: Pick<typeof issues.$inferSelect, "id" | "companyId" | "projectWorkspaceId">,
|
||||
issue: Pick<RecoveryIssue, "id" | "companyId" | "projectWorkspaceId">,
|
||||
sameWorkspaceOnly = false,
|
||||
) {
|
||||
if (sameWorkspaceOnly && !issue.projectWorkspaceId) return [];
|
||||
@@ -3001,7 +3005,7 @@ export function recoveryService(
|
||||
}
|
||||
|
||||
async function resolveContinuationWaitingOnReview(
|
||||
issue: typeof issues.$inferSelect,
|
||||
issue: RecoveryIssue,
|
||||
) {
|
||||
const [existingBlockers, openChildren] = await Promise.all([
|
||||
existingUnresolvedBlockerIssues(issue.companyId, issue.id),
|
||||
@@ -3102,7 +3106,7 @@ export function recoveryService(
|
||||
}
|
||||
|
||||
async function resolveDispositionRepairActionAsCovered(
|
||||
issue: typeof issues.$inferSelect,
|
||||
issue: RecoveryIssue,
|
||||
reason: string,
|
||||
) {
|
||||
const active = await recoveryActionsSvc.getActiveForIssue(
|
||||
@@ -3136,7 +3140,7 @@ export function recoveryService(
|
||||
}
|
||||
|
||||
async function ensureDispositionRepairAction(input: {
|
||||
issue: typeof issues.$inferSelect;
|
||||
issue: RecoveryIssue;
|
||||
latestRun: LatestIssueRun;
|
||||
fingerprint: string;
|
||||
attemptCount: number;
|
||||
@@ -3214,7 +3218,7 @@ export function recoveryService(
|
||||
}
|
||||
|
||||
async function scheduleDispositionRepairAttempt(input: {
|
||||
issue: typeof issues.$inferSelect;
|
||||
issue: RecoveryIssue;
|
||||
latestRun: LatestIssueRun;
|
||||
action: Awaited<ReturnType<typeof ensureDispositionRepairAction>>;
|
||||
fingerprint: string;
|
||||
@@ -3651,7 +3655,7 @@ export function recoveryService(
|
||||
}
|
||||
|
||||
async function escalateDispositionRepair(input: {
|
||||
issue: typeof issues.$inferSelect;
|
||||
issue: RecoveryIssue;
|
||||
latestRun: LatestIssueRun;
|
||||
fingerprint: string;
|
||||
attemptCount: number;
|
||||
@@ -3796,7 +3800,7 @@ export function recoveryService(
|
||||
}
|
||||
|
||||
async function decidePersistedLegacyContinuation(
|
||||
issue: typeof issues.$inferSelect,
|
||||
issue: RecoveryIssue,
|
||||
runId: string,
|
||||
state: Awaited<ReturnType<typeof collectDispositionRepairSourceState>>,
|
||||
episode: LegacyDispositionEpisode,
|
||||
@@ -3876,7 +3880,7 @@ export function recoveryService(
|
||||
}
|
||||
|
||||
async function reconcileDispositionRepair(
|
||||
issue: Pick<typeof issues.$inferSelect, "id" | "companyId">,
|
||||
issue: Pick<RecoveryIssue, "id" | "companyId">,
|
||||
latestRun: LatestIssueRun,
|
||||
options: { historicalAttemptCount?: number; legacyEpisode?: LegacyDispositionEpisode } = {},
|
||||
): Promise<"queued" | "escalated" | "covered" | "skipped"> {
|
||||
@@ -4023,7 +4027,7 @@ export function recoveryService(
|
||||
}
|
||||
|
||||
async function escalateStrandedAssignedIssue(input: {
|
||||
issue: typeof issues.$inferSelect;
|
||||
issue: RecoveryIssue;
|
||||
previousStatus: StrandedPreviousStatus;
|
||||
latestRun: LatestIssueRun;
|
||||
comment?: string;
|
||||
@@ -4390,7 +4394,7 @@ export function recoveryService(
|
||||
}
|
||||
|
||||
async function scheduleProviderQuotaRecoveryMonitor(input: {
|
||||
issue: typeof issues.$inferSelect;
|
||||
issue: RecoveryIssue;
|
||||
latestRun: NonNullable<LatestIssueRun>;
|
||||
classification: Extract<
|
||||
NonNullable<AdapterFailureRecoveryClassification>,
|
||||
@@ -4473,7 +4477,7 @@ export function recoveryService(
|
||||
}
|
||||
|
||||
function getAdapterFailureRecoveryTargetAgentId(
|
||||
issue: typeof issues.$inferSelect,
|
||||
issue: RecoveryIssue,
|
||||
) {
|
||||
if (issue.status !== "in_review") return issue.assigneeAgentId;
|
||||
|
||||
@@ -4488,7 +4492,7 @@ export function recoveryService(
|
||||
}
|
||||
|
||||
function hasPendingProviderQuotaRecoveryMonitor(
|
||||
issue: typeof issues.$inferSelect,
|
||||
issue: RecoveryIssue,
|
||||
latestRun: LatestIssueRun,
|
||||
now: Date,
|
||||
) {
|
||||
@@ -4510,7 +4514,7 @@ export function recoveryService(
|
||||
issueCreatedAtGte?: Date | null;
|
||||
}) {
|
||||
const candidates = await db
|
||||
.select()
|
||||
.select(recoveryIssueFields)
|
||||
.from(issues)
|
||||
.where(
|
||||
and(
|
||||
|
||||
Reference in new issue
Block a user