Fix recovery handoff PR follow-ups

This commit is contained in:
Dotta committed 2026-05-05 11:53:39 -05:00
1 parent 156f4b81f7
commit 392bb1ccc1
6 files changed
+46 -24

No files matched your search

+2
View File
@@ -244,6 +244,7 @@ describe("renderCompanyImportPreview", () => {
billingCode: null,
executionWorkspaceSettings: null,
assigneeAdapterOverrides: null,
comments: [],
metadata: null,
},
],
@@ -460,6 +461,7 @@ describe("import selection catalog", () => {
billingCode: null,
executionWorkspaceSettings: null,
assigneeAdapterOverrides: null,
comments: [],
metadata: null,
},
],
+3
View File
@@ -1258,9 +1258,12 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness {
id: randomUUID(),
companyId: parentIssue.companyId,
issueId,
authorType: options?.authorAgentId ? "agent" : "system",
authorAgentId: options?.authorAgentId ?? null,
authorUserId: null,
body,
presentation: null,
metadata: null,
createdAt: now,
updatedAt: now,
};
+25 -21
View File
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
import { Router, type Request, type Response } from "express";
import multer from "multer";
import { z } from "zod";
import { and, desc, eq, inArray } from "drizzle-orm";
import { and, desc, eq, inArray, sql } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import { activityLog, issueExecutionDecisions } from "@paperclipai/db";
import {
@@ -116,6 +116,14 @@ type ExecutionStageWakeContext = {
lastDecisionOutcome: ParsedExecutionState["lastDecisionOutcome"];
allowedActions: string[];
};
type SuccessfulRunHandoffActivityRow = {
entityId: string;
action: string;
agentId: string | null;
runId: string | null;
details: Record<string, unknown> | null;
createdAt: Date;
};
const SUCCESSFUL_RUN_HANDOFF_ACTIONS = [
"issue.successful_run_handoff_required",
@@ -181,29 +189,25 @@ async function listSuccessfulRunHandoffStates(
issueIds: string[],
): Promise<Map<string, SuccessfulRunHandoffState>> {
if (issueIds.length === 0) return new Map();
const rows = await db
.select({
entityId: activityLog.entityId,
action: activityLog.action,
agentId: activityLog.agentId,
runId: activityLog.runId,
details: activityLog.details,
createdAt: activityLog.createdAt,
})
.from(activityLog)
.where(
and(
eq(activityLog.companyId, companyId),
eq(activityLog.entityType, "issue"),
inArray(activityLog.entityId, issueIds),
inArray(activityLog.action, [...SUCCESSFUL_RUN_HANDOFF_ACTIONS]),
),
)
.orderBy(desc(activityLog.createdAt));
const result = await db.execute(sql`
SELECT DISTINCT ON (${activityLog.entityId})
${activityLog.entityId} AS "entityId",
${activityLog.action} AS "action",
${activityLog.agentId} AS "agentId",
${activityLog.runId} AS "runId",
${activityLog.details} AS "details",
${activityLog.createdAt} AS "createdAt"
FROM ${activityLog}
WHERE ${activityLog.companyId} = ${companyId}
AND ${activityLog.entityType} = 'issue'
AND ${activityLog.entityId} IN (${sql.join(issueIds.map((id) => sql`${id}`), sql`, `)})
AND ${activityLog.action} IN (${sql.join(SUCCESSFUL_RUN_HANDOFF_ACTIONS.map((action) => sql`${action}`), sql`, `)})
ORDER BY ${activityLog.entityId}, ${activityLog.createdAt} DESC, ${activityLog.id} DESC
`);
const rows = Array.from(result as Iterable<SuccessfulRunHandoffActivityRow>);
const states = new Map<string, SuccessfulRunHandoffState>();
for (const row of rows) {
if (states.has(row.entityId)) continue;
const state = successfulRunHandoffStateFromActivity(row);
if (state) states.set(row.entityId, state);
}
@@ -8,6 +8,7 @@ import {
buildSuccessfulRunHandoffExhaustedNotice,
buildSuccessfulRunHandoffRequiredNotice,
decideSuccessfulRunHandoff,
isIdempotentFinishSuccessfulRunHandoffWakeStatus,
isSuccessfulRunHandoffRequiredNoticeBody,
} from "./successful-run-handoff.js";
@@ -183,6 +184,14 @@ describe("successful run handoff decision", () => {
})).toBe("finish_successful_run_handoff:issue-1:run-1:1");
});
it("allows failed or cancelled corrective wakes to be retried", () => {
expect(isIdempotentFinishSuccessfulRunHandoffWakeStatus("queued")).toBe(true);
expect(isIdempotentFinishSuccessfulRunHandoffWakeStatus("claimed")).toBe(true);
expect(isIdempotentFinishSuccessfulRunHandoffWakeStatus("completed")).toBe(true);
expect(isIdempotentFinishSuccessfulRunHandoffWakeStatus("failed")).toBe(false);
expect(isIdempotentFinishSuccessfulRunHandoffWakeStatus("cancelled")).toBe(false);
});
it("builds the required system notice with hidden structured metadata", () => {
const notice = buildSuccessfulRunHandoffRequiredNotice({
issue: {
@@ -35,9 +35,12 @@ const IDEMPOTENT_HANDOFF_WAKE_STATUSES = [
"deferred_issue_execution",
"claimed",
"completed",
"failed",
"cancelled",
];
const IDEMPOTENT_HANDOFF_WAKE_STATUS_SET = new Set<string>(IDEMPOTENT_HANDOFF_WAKE_STATUSES);
export function isIdempotentFinishSuccessfulRunHandoffWakeStatus(status: string) {
return IDEMPOTENT_HANDOFF_WAKE_STATUS_SET.has(status);
}
type HeartbeatRunRow = typeof heartbeatRuns.$inferSelect;
type IssueRow = Pick<
+2 -1
View File
@@ -2,6 +2,7 @@ import { useId, useState, type ReactNode } from "react";
import {
ChevronDown,
CircleAlert,
CircleCheck,
Info,
OctagonAlert,
TriangleAlert,
@@ -72,7 +73,7 @@ const TONE_TOKENS: Record<SystemNoticeTone, ToneTokens> = {
container:
"border-emerald-300/70 bg-emerald-50/70 dark:border-emerald-500/30 dark:bg-emerald-500/10",
iconWrap: "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200",
icon: CircleAlert,
icon: CircleCheck,
iconClass: "text-emerald-700 dark:text-emerald-300",
label: "text-emerald-800 dark:text-emerald-200",
divider: "border-emerald-300/50 dark:border-emerald-500/30",