mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-11 05:11:49 +01:00
Address run liveness review feedback
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
b3253f4c0e
commit
79704029d0
3 files changed
+147
-13
No files matched your search
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<number>`count(*)::int`,
|
||||
planCount: sql<number>`count(*) filter (where ${issueDocuments.key} = 'plan')::int`,
|
||||
latestAt: sql<Date | null>`max(${issueDocuments.updatedAt})`,
|
||||
latestAt: sql<Date | null>`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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user