Stop cancelled productivity review loops (#5210)
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies. > - Productivity review reconciliation creates manager-owned review issues when assigned work shows no-comment, long-active, or high-churn patterns. > - SplatImmo hit a loop because productivity-review issues were auto-cancelled while the source issue still matched the same trigger. > - The service already snoozed recently completed reviews, but cancelled reviews were ignored for that snooze check. > - This pull request treats recently cancelled productivity reviews as terminal snooze evidence. > - The benefit is that cancelling a review now suppresses immediate recreation without disabling useful future productivity reviews. ## What Changed - Renamed the recent-review lookup to terminal-review semantics and included `cancelled` alongside `done`. - Added a regression test proving a recently cancelled productivity review produces `snoozed` instead of creating another review. ## Verification - `pnpm exec vitest run server/src/__tests__/productivity-review-service.test.ts` passes: 1 file, 12 tests. - Queried the SplatImmo Paperclip instance for existing `Review productivity` issues: 500 `issue_productivity_review` issues found, all already `cancelled`, 0 active. ## Risks - Low risk: this only affects the reconciliation branch after a terminal productivity-review issue exists. - Operators who cancel a productivity review now get the same default 6-hour quiet window as completed reviews; after that window, persistent evidence can still create a fresh review. ## Model Used - OpenAI Codex coding agent, GPT-5 class model, tool-enabled code editing and local command execution. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Yanis Ismail <yanis.ismail@emissive.fr> Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
0ecae2cd7e
commit
24bd860280
|
|
@ -497,6 +497,35 @@ describeEmbeddedPostgres("productivity review service", () => {
|
|||
expect(reviews).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("treats a recently cancelled review as a snooze window", async () => {
|
||||
const now = new Date("2026-04-28T12:00:00.000Z");
|
||||
const seeded = await seedAssignedIssue();
|
||||
await insertRuns({
|
||||
companyId: seeded.companyId,
|
||||
agentId: seeded.coderId,
|
||||
issueId: seeded.issueId,
|
||||
count: 10,
|
||||
now,
|
||||
});
|
||||
const service = productivityReviewService(db);
|
||||
await service.reconcileProductivityReviews({ now, companyId: seeded.companyId });
|
||||
const [review] = await listProductivityReviews(seeded.companyId);
|
||||
await db
|
||||
.update(issues)
|
||||
.set({ status: "cancelled", updatedAt: now })
|
||||
.where(eq(issues.id, review!.id));
|
||||
|
||||
const result = await service.reconcileProductivityReviews({
|
||||
now: new Date(now.getTime() + 30 * 60 * 1000),
|
||||
companyId: seeded.companyId,
|
||||
});
|
||||
const reviews = await listProductivityReviews(seeded.companyId);
|
||||
|
||||
expect(result.snoozed).toBe(1);
|
||||
expect(result.created).toBe(0);
|
||||
expect(reviews).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("reports and logs soft-stop holds for open no-comment reviews", async () => {
|
||||
const now = new Date("2026-04-28T12:00:00.000Z");
|
||||
const seeded = await seedAssignedIssue();
|
||||
|
|
|
|||
|
|
@ -260,7 +260,7 @@ export function productivityReviewService(db: Db, deps?: { enqueueWakeup?: Enque
|
|||
.then((rows) => rows[0] ?? null);
|
||||
}
|
||||
|
||||
async function findRecentResolvedProductivityReview(
|
||||
async function findRecentTerminalProductivityReview(
|
||||
companyId: string,
|
||||
sourceIssueId: string,
|
||||
thresholds: ProductivityReviewThresholds,
|
||||
|
|
@ -275,7 +275,7 @@ export function productivityReviewService(db: Db, deps?: { enqueueWakeup?: Enque
|
|||
eq(issues.companyId, companyId),
|
||||
eq(issues.originKind, PRODUCTIVITY_REVIEW_ORIGIN_KIND),
|
||||
eq(issues.originId, sourceIssueId),
|
||||
eq(issues.status, "done"),
|
||||
inArray(issues.status, ["done", "cancelled"]),
|
||||
gt(issues.updatedAt, cutoff),
|
||||
),
|
||||
)
|
||||
|
|
@ -807,7 +807,7 @@ export function productivityReviewService(db: Db, deps?: { enqueueWakeup?: Enque
|
|||
result.skipped += 1;
|
||||
continue;
|
||||
}
|
||||
if (await findRecentResolvedProductivityReview(candidate.companyId, candidate.id, thresholds, now)) {
|
||||
if (await findRecentTerminalProductivityReview(candidate.companyId, candidate.id, thresholds, now)) {
|
||||
result.snoozed += 1;
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue