From 79704029d0af30154805ecbddb42881a0dcfd174 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 19 Apr 2026 20:51:49 -0500 Subject: [PATCH] Address run liveness review feedback Co-Authored-By: Paperclip --- server/src/__tests__/activity-service.test.ts | 134 +++++++++++++++++- server/src/services/activity.ts | 15 +- ui/src/api/activity.ts | 11 +- 3 files changed, 147 insertions(+), 13 deletions(-) diff --git a/server/src/__tests__/activity-service.test.ts b/server/src/__tests__/activity-service.test.ts index 652ba1c076d..cd63779f347 100644 --- a/server/src/__tests__/activity-service.test.ts +++ b/server/src/__tests__/activity-service.test.ts @@ -1,6 +1,16 @@ import { randomUUID } from "node:crypto"; import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; -import { agents, companies, createDb, heartbeatRuns, issueComments, issues } from "@paperclipai/db"; +import { + agents, + companies, + createDb, + documentRevisions, + documents, + heartbeatRuns, + issueComments, + issueDocuments, + issues, +} from "@paperclipai/db"; import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase, @@ -27,6 +37,9 @@ describeEmbeddedPostgres("activity service", () => { afterEach(async () => { await db.delete(issueComments); + await db.delete(issueDocuments); + await db.delete(documentRevisions); + await db.delete(documents); await db.delete(issues); await db.delete(heartbeatRuns); await db.delete(agents); @@ -218,4 +231,123 @@ describeEmbeddedPostgres("activity service", () => { lastUsefulActionAt: completedAt, }); }); + + it("does not backfill document evidence from a different run", async () => { + const companyId = randomUUID(); + const agentId = randomUUID(); + const issueId = randomUUID(); + const runId = randomUUID(); + const otherRunId = randomUUID(); + const documentId = randomUUID(); + const revisionId = randomUUID(); + const createdAt = new Date("2026-04-18T20:08:00.000Z"); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + requireBoardApprovalForNewAgents: false, + }); + + await db.insert(agents).values({ + id: agentId, + companyId, + name: "CodexCoder", + role: "engineer", + status: "idle", + adapterType: "codex_local", + adapterConfig: {}, + runtimeConfig: {}, + permissions: {}, + }); + + await db.insert(issues).values({ + id: issueId, + companyId, + title: "Fix run ledger", + description: "Make the run ledger answer whether a run advanced.", + status: "in_progress", + priority: "medium", + assigneeAgentId: agentId, + }); + + await db.insert(heartbeatRuns).values([ + { + id: runId, + companyId, + agentId, + invocationSource: "assignment", + status: "succeeded", + startedAt: new Date("2026-04-18T20:00:00.000Z"), + finishedAt: new Date("2026-04-18T20:02:00.000Z"), + contextSnapshot: { issueId }, + resultJson: { + summary: "Next steps:\n- inspect files", + }, + livenessState: null, + livenessReason: null, + }, + { + id: otherRunId, + companyId, + agentId, + invocationSource: "assignment", + status: "succeeded", + startedAt: new Date("2026-04-18T20:05:00.000Z"), + finishedAt: createdAt, + contextSnapshot: { issueId }, + resultJson: { + summary: "Updated the plan document.", + }, + livenessState: "advanced", + livenessReason: "Run produced concrete action evidence: 1 document revision(s)", + }, + ]); + + await db.insert(documents).values({ + id: documentId, + companyId, + title: "Plan", + format: "markdown", + latestBody: "# Plan\n\n- Inspect files", + latestRevisionId: revisionId, + latestRevisionNumber: 1, + createdByAgentId: agentId, + updatedByAgentId: agentId, + createdAt, + updatedAt: createdAt, + }); + + await db.insert(documentRevisions).values({ + id: revisionId, + companyId, + documentId, + revisionNumber: 1, + title: "Plan", + format: "markdown", + body: "# Plan\n\n- Inspect files", + createdByAgentId: agentId, + createdByRunId: otherRunId, + createdAt, + }); + + await db.insert(issueDocuments).values({ + companyId, + issueId, + documentId, + key: "plan", + createdAt, + updatedAt: createdAt, + }); + + const runs = await activityService(db).runsForIssue(companyId, issueId); + const backfilledRun = runs.find((run) => run.runId === runId); + + expect(backfilledRun).toMatchObject({ + runId, + livenessState: "plan_only", + livenessReason: "Run described future work without concrete action evidence", + lastUsefulActionAt: null, + }); + }); }); diff --git a/server/src/services/activity.ts b/server/src/services/activity.ts index 08e417b8d04..44c6707a4b7 100644 --- a/server/src/services/activity.ts +++ b/server/src/services/activity.ts @@ -3,6 +3,7 @@ import type { Db } from "@paperclipai/db"; import { activityLog, agents, + documentRevisions, heartbeatRunEvents, heartbeatRuns, issueComments, @@ -202,10 +203,18 @@ export function activityService(db: Db) { .select({ count: sql`count(*)::int`, planCount: sql`count(*) filter (where ${issueDocuments.key} = 'plan')::int`, - latestAt: sql`max(${issueDocuments.updatedAt})`, + latestAt: sql`max(${documentRevisions.createdAt})`, }) - .from(issueDocuments) - .where(and(eq(issueDocuments.companyId, companyId), eq(issueDocuments.issueId, issueId))); + .from(documentRevisions) + .innerJoin(issueDocuments, eq(documentRevisions.documentId, issueDocuments.documentId)) + .where( + and( + eq(documentRevisions.companyId, companyId), + eq(documentRevisions.createdByRunId, run.id), + eq(issueDocuments.companyId, companyId), + eq(issueDocuments.issueId, issueId), + ), + ); const [workProductStats] = await db .select({ diff --git a/ui/src/api/activity.ts b/ui/src/api/activity.ts index 0947a21e6cc..da14ff808d1 100644 --- a/ui/src/api/activity.ts +++ b/ui/src/api/activity.ts @@ -1,14 +1,7 @@ -import type { ActivityEvent } from "@paperclipai/shared"; +import type { ActivityEvent, RunLivenessState } from "@paperclipai/shared"; import { api } from "./client"; -export type RunLivenessState = - | "completed" - | "advanced" - | "plan_only" - | "empty_response" - | "blocked" - | "failed" - | "needs_followup"; +export type { RunLivenessState } from "@paperclipai/shared"; export interface RunForIssue { runId: string;