mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 00:53:38 +02:00
Fix recovery handoff PR follow-ups
This commit is contained in:
1 parent
156f4b81f7
commit
392bb1ccc1
6 files changed
+46
-24
No files matched your search
@@ -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,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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
@@ -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,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",
|
||||
|
||||
Reference in new issue
Block a user