diff --git a/server/src/__tests__/company-portability-import-batching.test.ts b/server/src/__tests__/company-portability-import-batching.test.ts index 4c32bfe5cd..53ed945bc1 100644 --- a/server/src/__tests__/company-portability-import-batching.test.ts +++ b/server/src/__tests__/company-portability-import-batching.test.ts @@ -1,6 +1,8 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { + agents, assets, + companies, createDb, documentRevisions, documents, @@ -371,6 +373,56 @@ describeEmbeddedPostgres("company import batches inserts", () => { expect(mineAfterCreate.filter((issue) => importedIds.has(issue.id))).toEqual([]); }); + it("imports in_progress issues with a null startedAt", async () => { + const companyId = randomUUID(); + const agentId = randomUUID(); + await db.insert(companies).values({ + id: companyId, + name: "Carried Over Co", + issuePrefix: "CAR", + requireBoardApprovalForNewAgents: false, + }); + // pauseAutomations imports assignees paused; paused agents keep assignments. + await db.insert(agents).values({ + id: agentId, + companyId, + name: "Carried Coder", + role: "engineer", + status: "paused", + adapterType: "codex_local", + adapterConfig: {}, + runtimeConfig: {}, + permissions: {}, + }); + + await issueService(db).importIssues(companyId, [{ + id: randomUUID(), + ref: "carried-over", + projectId: null, + projectWorkspaceId: null, + title: "Carried-over work", + description: null, + assigneeAgentId: agentId, + status: "in_progress", + priority: "medium", + billingCode: null, + assigneeAdapterOverrides: null, + executionWorkspaceSettings: null, + labelIds: [], + monitorNotes: null, + monitorScheduledBy: null, + }]); + + const [imported] = await db + .select({ status: issues.status, startedAt: issues.startedAt }) + .from(issues) + .where(eq(issues.companyId, companyId)); + expect(imported?.status).toBe("in_progress"); + // A fabricated import-time startedAt made carried-over work look hours + // stale to duration-based sweeps (e.g. the productivity review). + expect(imported?.startedAt).toBeNull(); + }); + it("rolls back the whole work-product batch when a later chunk fails", async () => { // Seed a real company + issue to hang work products off of. const bundle = buildSyntheticBundle({ issueCount: 1, commentsPerIssue: 0, documentsPerIssue: 0 }); diff --git a/server/src/__tests__/productivity-review-service.test.ts b/server/src/__tests__/productivity-review-service.test.ts index 12b3bdd18c..a45e4502ed 100644 --- a/server/src/__tests__/productivity-review-service.test.ts +++ b/server/src/__tests__/productivity-review-service.test.ts @@ -505,6 +505,28 @@ describeEmbeddedPostgres("productivity review service", () => { expect(hold.held).toBe(false); }); + it("skips a long-active candidate while its assignee is paused and reviews it once unpaused", async () => { + const now = new Date("2026-04-28T12:00:00.000Z"); + const seeded = await seedAssignedIssue({ + status: "in_progress", + startedAt: new Date(now.getTime() - 7 * 60 * 60 * 1000), + }); + await db.update(agents).set({ status: "paused" }).where(eq(agents.id, seeded.coderId)); + const service = productivityReviewService(db); + + const pausedResult = await service.reconcileProductivityReviews({ now, companyId: seeded.companyId }); + + expect(pausedResult.created).toBe(0); + expect(pausedResult.skipped).toBe(1); + expect(await listProductivityReviews(seeded.companyId)).toHaveLength(0); + + await db.update(agents).set({ status: "idle" }).where(eq(agents.id, seeded.coderId)); + const unpausedResult = await service.reconcileProductivityReviews({ now, companyId: seeded.companyId }); + + expect(unpausedResult.created).toBe(1); + expect(await listProductivityReviews(seeded.companyId)).toHaveLength(1); + }); + it("creates a high-churn review even when every sampled run has a progress comment", async () => { const now = new Date("2026-04-28T12:00:00.000Z"); const seeded = await seedAssignedIssue(); diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index 347168911b..16dbd66207 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -7352,7 +7352,9 @@ export function issueService(db: Db) { responsibleUserId: null, requestDepth: clampIssueRequestDepth(undefined), originKind: "manual", - startedAt: row.status === "in_progress" ? new Date() : null, + // Imported in-progress work did not start at import time; fabricating + // startedAt here trips duration-based sweeps (e.g. productivity review). + startedAt: null, completedAt: row.status === "done" ? new Date() : null, cancelledAt: row.status === "cancelled" ? new Date() : null, monitorNotes: row.monitorNotes ?? null, diff --git a/server/src/services/productivity-review.ts b/server/src/services/productivity-review.ts index 199b122421..909682d675 100644 --- a/server/src/services/productivity-review.ts +++ b/server/src/services/productivity-review.ts @@ -896,6 +896,11 @@ export function productivityReviewService(db: Db, deps?: { enqueueWakeup?: Enque result.skipped += 1; continue; } + // A paused assignee cannot act on a review, so raising one only creates noise. + if (sourceAgent.status === "paused") { + result.skipped += 1; + continue; + } const evidence = await collectEvidence(candidate, sourceAgent, thresholds, now); if (!evidence) { result.skipped += 1;