From fd2c75112245cb29a4bb8f7292219551020b2a66 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 28 Jul 2026 21:19:41 +0000 Subject: [PATCH] fix(watchdogs): follow self-review reassignment Retarget self-mode watchdogs to the watched issue's current invokable assignee before classification and wake dispatch. Co-Authored-By: Paperclip --- .../task-watchdogs-scheduler.test.ts | 21 +++++++++++++++++++ server/src/services/task-watchdogs.ts | 15 ++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/server/src/__tests__/task-watchdogs-scheduler.test.ts b/server/src/__tests__/task-watchdogs-scheduler.test.ts index 4e5854299ae..f74f1a4fba6 100644 --- a/server/src/__tests__/task-watchdogs-scheduler.test.ts +++ b/server/src/__tests__/task-watchdogs-scheduler.test.ts @@ -407,6 +407,27 @@ describeEmbeddedPostgres("task watchdog scheduler", () => { expect(watchdog?.triggerCount).toBe(1); }); + it("retargets a self-mode watchdog to the current assignee before waking", async () => { + const companyId = await seedCompany(); + const formerAssigneeId = await seedAgent(companyId, { name: "Former Assignee" }); + const currentAssigneeId = await seedAgent(companyId, { name: "Current Assignee" }); + const sourceId = await seedIssue(companyId, { + identifier: "WDOG-SELF-REASSIGNED", + status: "done", + assigneeAgentId: currentAssigneeId, + }); + await seedWatchdog(companyId, sourceId, formerAssigneeId, { mode: "self" }); + const { service, wakes } = createService(); + + const result = await service.reconcileTaskWatchdogs({ companyId }); + + expect(result).toMatchObject({ checked: 1, triggered: 1 }); + expect(wakes).toHaveLength(1); + expect(wakes[0]?.agentId).toBe(currentAssigneeId); + const [watchdog] = await db.select().from(issueWatchdogs).where(eq(issueWatchdogs.issueId, sourceId)); + expect(watchdog?.watchdogAgentId).toBe(currentAssigneeId); + }); + it("retries a self review when the wake was not queued", async () => { const companyId = await seedCompany(); const agentId = await seedAgent(companyId); diff --git a/server/src/services/task-watchdogs.ts b/server/src/services/task-watchdogs.ts index 76343944147..0c599a350b8 100644 --- a/server/src/services/task-watchdogs.ts +++ b/server/src/services/task-watchdogs.ts @@ -1667,7 +1667,7 @@ export function taskWatchdogService(db: Db, deps: TaskWatchdogServiceDeps = {}) } async function evaluateWatchdog(row: IssueWatchdogRow, opts: { runId?: string | null } = {}) { - const watchdog = await markTerminalWatchdogIssueReviewed(row, opts); + let watchdog = await markTerminalWatchdogIssueReviewed(row, opts); const sourceIssue = await db .select() .from(issues) @@ -1677,6 +1677,19 @@ export function taskWatchdogService(db: Db, deps: TaskWatchdogServiceDeps = {}) return { state: "skipped" as const, reason: "watched_issue_not_applicable" }; } + if (issueWatchdogMode(watchdog) === "self" && watchdog.watchdogAgentId !== sourceIssue.assigneeAgentId) { + if (!sourceIssue.assigneeAgentId) { + return { state: "skipped" as const, reason: "self_watchdog_issue_unassigned" }; + } + await assertWatchdogAgentInvokable(db, watchdog.companyId, sourceIssue.assigneeAgentId); + watchdog = await db + .update(issueWatchdogs) + .set({ watchdogAgentId: sourceIssue.assigneeAgentId, updatedAt: new Date() }) + .where(eq(issueWatchdogs.id, watchdog.id)) + .returning() + .then((rows) => rows[0] ?? watchdog); + } + const input = await collectClassifierInput(watchdog.companyId, watchdog); const classification = classifyTaskWatchdogSubtree(input); if (classification.state !== "stopped") {