mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 18:31:34 +01:00
feat(watchdogs): add experimental self-mode watchdogs
- Add enableWatchdogEverything experimental instance setting and UI toggle. - Add issue_watchdogs.mode and accept mode in the watchdog endpoint. - Wake task assignees directly for stopped self-mode watchdogs. - Add first-instruction guidance for assignment-shaped wakes. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
9574cad3e8
commit
7b4722e26c
21 files changed
+554
-4
No files matched your search
@@ -37,6 +37,7 @@ describe("instance settings service", () => {
|
||||
enableIssuePlanDecompositions: true,
|
||||
enableExperimentalFileViewer: true,
|
||||
enableTaskWatchdogs: true,
|
||||
enableWatchdogEverything: false,
|
||||
enableCloudSync: true,
|
||||
enableBuiltInAgents: true,
|
||||
enableBetaSkills: false,
|
||||
@@ -80,6 +81,14 @@ describe("instance settings service", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults enableWatchdogEverything to false for empty and legacy stored settings", () => {
|
||||
expect(normalizeExperimentalSettings(undefined).enableWatchdogEverything).toBe(false);
|
||||
expect(normalizeExperimentalSettings({}).enableWatchdogEverything).toBe(false);
|
||||
expect(
|
||||
normalizeExperimentalSettings({ enableTaskWatchdogs: true }).enableWatchdogEverything,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults enableSmokeLab to false for empty and legacy stored settings", () => {
|
||||
expect(normalizeExperimentalSettings(undefined).enableSmokeLab).toBe(false);
|
||||
expect(normalizeExperimentalSettings({}).enableSmokeLab).toBe(false);
|
||||
|
||||
@@ -225,11 +225,12 @@ describeEmbeddedPostgres("issue watchdog routes", () => {
|
||||
watchdogAgentId: firstAgentId,
|
||||
instructions: "Check screenshots and tests.",
|
||||
status: "active",
|
||||
mode: "subtask",
|
||||
});
|
||||
|
||||
const updated = await request(app)
|
||||
.put(`/api/issues/${issueId}/watchdog`)
|
||||
.send({ agentId: secondAgentId, instructions: "Be skeptical." });
|
||||
.send({ agentId: secondAgentId, instructions: "Be skeptical.", mode: "self" });
|
||||
|
||||
expect(updated.status, JSON.stringify(updated.body)).toBe(200);
|
||||
expect(updated.body.id).toBe(created.body.id);
|
||||
@@ -238,11 +239,19 @@ describeEmbeddedPostgres("issue watchdog routes", () => {
|
||||
watchdogAgentId: secondAgentId,
|
||||
instructions: "Be skeptical.",
|
||||
status: "active",
|
||||
mode: "self",
|
||||
});
|
||||
|
||||
// Omitting mode on a later update keeps the stored mode.
|
||||
const modeKept = await request(app)
|
||||
.put(`/api/issues/${issueId}/watchdog`)
|
||||
.send({ agentId: secondAgentId, instructions: "Be skeptical." });
|
||||
expect(modeKept.status, JSON.stringify(modeKept.body)).toBe(200);
|
||||
expect(modeKept.body).toMatchObject({ id: created.body.id, mode: "self" });
|
||||
|
||||
const read = await request(app).get(`/api/issues/${issueId}/watchdog`);
|
||||
expect(read.status, JSON.stringify(read.body)).toBe(200);
|
||||
expect(read.body).toMatchObject({ id: created.body.id, watchdogAgentId: secondAgentId });
|
||||
expect(read.body).toMatchObject({ id: created.body.id, watchdogAgentId: secondAgentId, mode: "self" });
|
||||
|
||||
const detail = await request(app).get(`/api/issues/${issueId}`);
|
||||
expect(detail.status, JSON.stringify(detail.body)).toBe(200);
|
||||
@@ -280,6 +289,7 @@ describeEmbeddedPostgres("issue watchdog routes", () => {
|
||||
expect(actionNames.filter((action) => action.startsWith("issue.watchdog_"))).toEqual([
|
||||
"issue.watchdog_created",
|
||||
"issue.watchdog_updated",
|
||||
"issue.watchdog_updated",
|
||||
"issue.watchdog_removed",
|
||||
]);
|
||||
expect(actionNames).toContain("issue.task_watchdog_triggered");
|
||||
|
||||
@@ -144,13 +144,19 @@ describeEmbeddedPostgres("task watchdog scheduler", () => {
|
||||
});
|
||||
}
|
||||
|
||||
async function seedWatchdog(companyId: string, issueId: string, agentId: string) {
|
||||
async function seedWatchdog(
|
||||
companyId: string,
|
||||
issueId: string,
|
||||
agentId: string,
|
||||
overrides: Partial<typeof issueWatchdogs.$inferInsert> = {},
|
||||
) {
|
||||
const [row] = await db.insert(issueWatchdogs).values({
|
||||
companyId,
|
||||
issueId,
|
||||
watchdogAgentId: agentId,
|
||||
instructions: "Verify stopped work.",
|
||||
status: "active",
|
||||
...overrides,
|
||||
}).returning();
|
||||
return row;
|
||||
}
|
||||
@@ -303,6 +309,68 @@ describeEmbeddedPostgres("task watchdog scheduler", () => {
|
||||
expect(watchdog?.triggerCount).toBe(2);
|
||||
});
|
||||
|
||||
it("self mode posts the review on the watched issue and wakes the agent directly on it", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const agentId = await seedAgent(companyId);
|
||||
const sourceId = await seedIssue(companyId, {
|
||||
identifier: "WDOG-SELF",
|
||||
status: "done",
|
||||
assigneeAgentId: agentId,
|
||||
});
|
||||
await seedWatchdog(companyId, sourceId, agentId, {
|
||||
mode: "self",
|
||||
instructions: "Verify the report exists and passes lint.",
|
||||
});
|
||||
const { service, wakes } = createService();
|
||||
|
||||
const result = await service.reconcileTaskWatchdogs({ companyId });
|
||||
|
||||
expect(result).toMatchObject({ checked: 1, triggered: 1 });
|
||||
expect(result.watchdogIssueIds).toEqual([sourceId]);
|
||||
|
||||
// No separate watchdog sub-task in self mode.
|
||||
const watchdogIssues = await db
|
||||
.select()
|
||||
.from(issues)
|
||||
.where(and(eq(issues.companyId, companyId), eq(issues.originKind, "task_watchdog")));
|
||||
expect(watchdogIssues).toHaveLength(0);
|
||||
|
||||
expect(wakes).toHaveLength(1);
|
||||
expect(wakes[0]?.agentId).toBe(agentId);
|
||||
expect(wakes[0]?.opts?.reason).toBe("task_watchdog_self_review");
|
||||
expect(wakes[0]?.opts?.idempotencyKey).toMatch(/^task_watchdog:[^:]+:task_watchdog_stop:/);
|
||||
expect(wakes[0]?.opts?.contextSnapshot).toMatchObject({
|
||||
issueId: sourceId,
|
||||
taskId: sourceId,
|
||||
wakeReason: "task_watchdog_self_review",
|
||||
taskWatchdogSelfReview: {
|
||||
watchedIssueId: sourceId,
|
||||
watchedIssueIdentifier: "WDOG-SELF",
|
||||
goalInstructions: "Verify the report exists and passes lint.",
|
||||
},
|
||||
});
|
||||
// Self wakes must not carry the restricted task-watchdog mutation scope key.
|
||||
expect((wakes[0]?.opts?.contextSnapshot as Record<string, unknown>).taskWatchdog).toBeUndefined();
|
||||
|
||||
const comments = await db.select().from(issueComments).where(eq(issueComments.issueId, sourceId));
|
||||
expect(comments).toHaveLength(1);
|
||||
expect(comments[0]?.body).toContain("Self-watchdog review triggered");
|
||||
expect(comments[0]?.body).toContain("Verify the report exists and passes lint.");
|
||||
|
||||
const [watchdog] = await db.select().from(issueWatchdogs).where(eq(issueWatchdogs.issueId, sourceId));
|
||||
expect(watchdog?.lastObservedFingerprint).toMatch(/^task_watchdog_stop:/);
|
||||
expect(watchdog?.lastReviewedFingerprint).toBe(watchdog?.lastObservedFingerprint);
|
||||
expect(watchdog?.watchdogIssueId).toBeNull();
|
||||
expect(watchdog?.triggerCount).toBe(1);
|
||||
|
||||
// The same stopped state is reviewed-at-trigger: no re-wake, no comment churn.
|
||||
const second = await service.reconcileTaskWatchdogs({ companyId });
|
||||
expect(second).toMatchObject({ checked: 1, triggered: 0, alreadyReviewed: 1 });
|
||||
expect(wakes).toHaveLength(1);
|
||||
const commentsAfter = await db.select().from(issueComments).where(eq(issueComments.issueId, sourceId));
|
||||
expect(commentsAfter).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("does not trigger while a non-watchdog descendant has live work", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const sourceId = await seedIssue(companyId, { identifier: "WDOG-2", status: "in_progress" });
|
||||
|
||||
@@ -5518,6 +5518,7 @@ export function issueRoutes(
|
||||
const { watchdog, created } = await taskWatchdogsSvc.upsertForIssue(issue.companyId, issue.id, {
|
||||
agentId: req.body.agentId,
|
||||
instructions: req.body.instructions,
|
||||
mode: req.body.mode,
|
||||
actor: {
|
||||
agentId: actor.agentId,
|
||||
userId: actor.actorType === "user" ? actor.actorId : null,
|
||||
@@ -5538,6 +5539,7 @@ export function issueRoutes(
|
||||
identifier: issue.identifier,
|
||||
watchdogId: watchdog.id,
|
||||
watchdogAgentId: watchdog.watchdogAgentId,
|
||||
watchdogMode: watchdog.mode ?? "subtask",
|
||||
instructionsChanged: (existingWatchdog?.instructions ?? null) !== (watchdog.instructions ?? null),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -239,6 +239,7 @@ import {
|
||||
type SessionCompactionPolicy,
|
||||
} from "@paperclipai/adapter-utils";
|
||||
import {
|
||||
isAssignmentShapedPaperclipWakeReason,
|
||||
readPaperclipSkillSyncPreference,
|
||||
UNMANAGED_BACKGROUND_TASK_LIVENESS_REASON,
|
||||
UNMANAGED_BACKGROUND_TASK_STOP_REASON,
|
||||
@@ -4957,6 +4958,8 @@ export async function buildPaperclipWakePayload(input: {
|
||||
: [],
|
||||
executionStage: Object.keys(executionStage).length > 0 ? executionStage : null,
|
||||
taskWatchdog: (input.contextSnapshot.taskWatchdog ?? null) as unknown,
|
||||
taskWatchdogEverything: (input.contextSnapshot.taskWatchdogEverything ?? null) as unknown,
|
||||
taskWatchdogSelfReview: (input.contextSnapshot.taskWatchdogSelfReview ?? null) as unknown,
|
||||
skillTest: (input.contextSnapshot.paperclipSkillTest ?? null) as unknown,
|
||||
continuationSummary: safeContinuationSummary
|
||||
? {
|
||||
@@ -12427,6 +12430,29 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
} else {
|
||||
delete context.paperclipSkillTest;
|
||||
}
|
||||
// Experimental "watchdog everything": on assignment-shaped wakes for an
|
||||
// issue with no active watchdog, the wake payload instructs the assignee
|
||||
// to first triage whether the task needs a completion-goal watchdog and,
|
||||
// if so, register it on itself in self mode before doing the work.
|
||||
delete context.taskWatchdogEverything;
|
||||
if (
|
||||
issueContext &&
|
||||
issueContext.assigneeAgentId === agent.id &&
|
||||
issueContext.originKind !== "task_watchdog" &&
|
||||
issueContext.status !== "done" &&
|
||||
issueContext.status !== "cancelled" &&
|
||||
isAssignmentShapedPaperclipWakeReason(readNonEmptyString(context.wakeReason)) &&
|
||||
(await instanceSettings.getExperimental()).enableWatchdogEverything === true
|
||||
) {
|
||||
const activeWatchdog = await taskWatchdogs.getActiveForIssue(agent.companyId, issueContext.id);
|
||||
if (!activeWatchdog) {
|
||||
context.taskWatchdogEverything = {
|
||||
watchedIssueId: issueContext.id,
|
||||
watchedIssueIdentifier: issueContext.identifier,
|
||||
selfAgentId: agent.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
const paperclipWakePayload = await buildPaperclipWakePayload({
|
||||
db,
|
||||
companyId: agent.companyId,
|
||||
|
||||
@@ -217,6 +217,7 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta
|
||||
enableIssuePlanDecompositions: parsed.data.enableIssuePlanDecompositions ?? false,
|
||||
enableExperimentalFileViewer: parsed.data.enableExperimentalFileViewer ?? false,
|
||||
enableTaskWatchdogs: parsed.data.enableTaskWatchdogs ?? false,
|
||||
enableWatchdogEverything: parsed.data.enableWatchdogEverything ?? false,
|
||||
enableCloudSync: parsed.data.enableCloudSync ?? false,
|
||||
enableExternalObjects: parsed.data.enableExternalObjects ?? false,
|
||||
enableSmokeLab: parsed.data.enableSmokeLab ?? false,
|
||||
@@ -250,6 +251,7 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta
|
||||
enableCases: false,
|
||||
enableConferenceRoomChat: false,
|
||||
enableTaskWatchdogs: false,
|
||||
enableWatchdogEverything: false,
|
||||
enableIssuePlanDecompositions: false,
|
||||
enableExperimentalFileViewer: false,
|
||||
enableCloudSync: false,
|
||||
|
||||
@@ -48,6 +48,8 @@ type ActorFields = {
|
||||
export type IssueWatchdogUpsertInput = {
|
||||
agentId: string;
|
||||
instructions?: string | null;
|
||||
// Omitted keeps the existing mode on update and defaults to "subtask" on create.
|
||||
mode?: "subtask" | "self" | null;
|
||||
actor?: ActorFields;
|
||||
};
|
||||
|
||||
@@ -232,6 +234,10 @@ function normalizeInstructions(value: string | null | undefined): string | null
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
export function issueWatchdogMode(row: Pick<IssueWatchdogRow, "mode">): "subtask" | "self" {
|
||||
return row.mode === "self" ? "self" : "subtask";
|
||||
}
|
||||
|
||||
export function summarizeIssueWatchdog(row: IssueWatchdogRow): IssueWatchdogSummary {
|
||||
return {
|
||||
id: row.id,
|
||||
@@ -240,6 +246,7 @@ export function summarizeIssueWatchdog(row: IssueWatchdogRow): IssueWatchdogSumm
|
||||
watchdogAgentId: row.watchdogAgentId,
|
||||
instructions: row.instructions,
|
||||
status: row.status as IssueWatchdogSummary["status"],
|
||||
mode: issueWatchdogMode(row),
|
||||
watchdogIssueId: row.watchdogIssueId,
|
||||
lastObservedFingerprint: row.lastObservedFingerprint,
|
||||
lastReviewedFingerprint: row.lastReviewedFingerprint,
|
||||
@@ -650,6 +657,77 @@ function buildStoppedFingerprintComment(input: {
|
||||
].filter((line) => line !== "").join("\n");
|
||||
}
|
||||
|
||||
function buildSelfReviewComment(input: {
|
||||
stopFingerprint: string;
|
||||
stoppedLeaves: TaskWatchdogStoppedLeaf[];
|
||||
pendingInteractionsByIssueId: TaskWatchdogPendingInteractionsByIssueId;
|
||||
goalInstructions: string | null;
|
||||
}) {
|
||||
const shortId = (id: string) => id.length > 8 ? `${id.slice(0, 8)}…` : id;
|
||||
const leafLines = input.stoppedLeaves.slice(0, 12).map((leaf) => {
|
||||
const interactionKinds = new Map(
|
||||
(input.pendingInteractionsByIssueId[leaf.issueId] ?? []).map((wait) => [wait.id, wait.kind]),
|
||||
);
|
||||
const waits = [
|
||||
...leaf.pendingInteractionIds.map((id) => `${interactionKinds.get(id) ?? "interaction"} ${shortId(id)}`),
|
||||
...leaf.pendingApprovalIds.map((id) => `approval ${shortId(id)}`),
|
||||
];
|
||||
return `- ${leaf.identifier ?? leaf.issueId}: ${leaf.status}${waits.length > 0 ? ` (pending ${waits.join(", ")})` : ""}`;
|
||||
});
|
||||
const more = input.stoppedLeaves.length > leafLines.length
|
||||
? `\n- ...and ${input.stoppedLeaves.length - leafLines.length} more stopped leaves`
|
||||
: "";
|
||||
return [
|
||||
"Self-watchdog review triggered: this task's subtree has no live execution path. The assignee is being woken on this task to verify the completion goal.",
|
||||
"",
|
||||
`Stopped fingerprint: \`${input.stopFingerprint}\``,
|
||||
...(input.goalInstructions
|
||||
? ["", "Completion goal under verification:", markdownQuote(input.goalInstructions)]
|
||||
: []),
|
||||
"",
|
||||
"Stopped leaves:",
|
||||
...(leafLines.length > 0 ? leafLines : ["- No open leaf issues (subtree is terminal)."]),
|
||||
more,
|
||||
].filter((line) => line !== "").join("\n");
|
||||
}
|
||||
|
||||
function markdownQuote(text: string) {
|
||||
return text.split("\n").map((line) => `> ${line}`).join("\n");
|
||||
}
|
||||
|
||||
// Self mode wakes the watchdog agent directly on the watched issue. This
|
||||
// context deliberately does NOT use the `taskWatchdog` key: that key activates
|
||||
// the restricted task-watchdog mutation scope for the run, which is wrong here
|
||||
// — in self mode the watchdog agent is the issue's own assignee and operates
|
||||
// under its normal permissions.
|
||||
function selfWatchdogWakeContext(input: {
|
||||
watchdog: IssueWatchdogRow;
|
||||
sourceIssue: IssueRow;
|
||||
classification: Extract<TaskWatchdogClassifierResult, { state: "stopped" }>;
|
||||
}) {
|
||||
return {
|
||||
issueId: input.sourceIssue.id,
|
||||
taskId: input.sourceIssue.id,
|
||||
wakeReason: "task_watchdog_self_review",
|
||||
source: "task_watchdog_self",
|
||||
taskWatchdogSelfReview: {
|
||||
watchdogId: input.watchdog.id,
|
||||
watchedIssueId: input.sourceIssue.id,
|
||||
watchedIssueIdentifier: input.sourceIssue.identifier,
|
||||
watchedIssueTitle: input.sourceIssue.title,
|
||||
stopFingerprint: input.classification.stopFingerprint,
|
||||
goalInstructions: input.watchdog.instructions,
|
||||
stoppedLeaves: input.classification.stoppedLeaves,
|
||||
pendingInteractions: input.classification.pendingInteractionsByIssueId,
|
||||
pendingApprovals: Object.fromEntries(Object.entries(input.classification.stopSnapshot.waitsByIssueId)
|
||||
.filter(([, waits]) => waits.pendingApprovalIds.length > 0)
|
||||
.map(([issueId, waits]) => [issueId, waits.pendingApprovalIds])),
|
||||
},
|
||||
resumeIntent: true,
|
||||
followUpRequested: true,
|
||||
};
|
||||
}
|
||||
|
||||
function stoppedFingerprintMetadata(input: {
|
||||
sourceIssueId: string;
|
||||
stopFingerprint: string;
|
||||
@@ -792,6 +870,7 @@ async function updateIssueWatchdogRow(
|
||||
.set({
|
||||
watchdogAgentId: input.agentId,
|
||||
instructions: normalizeInstructions(input.instructions),
|
||||
mode: input.mode ?? issueWatchdogMode(existing),
|
||||
status: "active",
|
||||
updatedByAgentId: input.actor?.agentId ?? null,
|
||||
updatedByUserId: input.actor?.userId ?? null,
|
||||
@@ -831,6 +910,7 @@ export async function upsertIssueWatchdogForIssue(
|
||||
issueId,
|
||||
watchdogAgentId: input.agentId,
|
||||
instructions: normalizeInstructions(input.instructions),
|
||||
mode: input.mode ?? "subtask",
|
||||
status: "active",
|
||||
createdByAgentId: input.actor?.agentId ?? null,
|
||||
createdByUserId: input.actor?.userId ?? null,
|
||||
@@ -1435,6 +1515,93 @@ export function taskWatchdogService(db: Db, deps: TaskWatchdogServiceDeps = {})
|
||||
return created;
|
||||
}
|
||||
|
||||
// Self mode: no watchdog sub-task. Post the review context on the watched
|
||||
// issue itself and wake the watchdog agent (normally the assignee) directly
|
||||
// on it to verify the completion goal. The fingerprint is marked reviewed at
|
||||
// trigger time so each stopped state produces exactly one verification wake
|
||||
// — a simple task that genuinely finished never churns.
|
||||
async function triggerSelfReview(input: {
|
||||
watchdog: IssueWatchdogRow;
|
||||
sourceIssue: IssueRow;
|
||||
classification: Extract<TaskWatchdogClassifierResult, { state: "stopped" }>;
|
||||
runId: string | null;
|
||||
}) {
|
||||
const { watchdog, sourceIssue, classification } = input;
|
||||
await issuesSvc.addComment(
|
||||
sourceIssue.id,
|
||||
buildSelfReviewComment({
|
||||
stopFingerprint: classification.stopFingerprint,
|
||||
stoppedLeaves: classification.stoppedLeaves,
|
||||
pendingInteractionsByIssueId: classification.pendingInteractionsByIssueId,
|
||||
goalInstructions: watchdog.instructions,
|
||||
}),
|
||||
{ runId: input.runId },
|
||||
{
|
||||
authorType: "system",
|
||||
metadata: stoppedFingerprintMetadata({
|
||||
sourceIssueId: sourceIssue.id,
|
||||
stopFingerprint: classification.stopFingerprint,
|
||||
waitsByIssueId: classification.stopSnapshot.waitsByIssueId,
|
||||
resumed: true,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const now = new Date();
|
||||
await db
|
||||
.update(issueWatchdogs)
|
||||
.set({
|
||||
lastObservedFingerprint: classification.stopFingerprint,
|
||||
lastObservedStopSnapshot: classification.stopSnapshot,
|
||||
lastReviewedFingerprint: classification.stopFingerprint,
|
||||
lastReviewedStopSnapshot: classification.stopSnapshot,
|
||||
lastTriggeredAt: now,
|
||||
triggerCount: sql`${issueWatchdogs.triggerCount} + 1`,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(eq(issueWatchdogs.id, watchdog.id));
|
||||
|
||||
await logActivity(db, {
|
||||
companyId: sourceIssue.companyId,
|
||||
actorType: "system",
|
||||
actorId: "system",
|
||||
agentId: watchdog.watchdogAgentId,
|
||||
runId: input.runId,
|
||||
action: "issue.task_watchdog_triggered",
|
||||
entityType: "issue",
|
||||
entityId: sourceIssue.id,
|
||||
details: {
|
||||
source: "task_watchdogs.evaluate",
|
||||
mode: "self",
|
||||
watchdogId: watchdog.id,
|
||||
stopFingerprint: classification.stopFingerprint,
|
||||
stopSnapshot: classification.stopSnapshot,
|
||||
stoppedLeaves: classification.stoppedLeaves,
|
||||
},
|
||||
});
|
||||
|
||||
const context = selfWatchdogWakeContext({ watchdog, sourceIssue, classification });
|
||||
const wake = deps.enqueueWakeup
|
||||
? await deps.enqueueWakeup(watchdog.watchdogAgentId, {
|
||||
source: "automation",
|
||||
triggerDetail: "system",
|
||||
reason: "task_watchdog_self_review",
|
||||
payload: context,
|
||||
contextSnapshot: context,
|
||||
idempotencyKey: taskWatchdogWakeIdempotencyKey(watchdog.id, classification.stopFingerprint),
|
||||
requestedByActorType: "system",
|
||||
requestedByActorId: null,
|
||||
})
|
||||
: null;
|
||||
|
||||
return {
|
||||
state: "triggered" as const,
|
||||
classification,
|
||||
watchdogIssueId: sourceIssue.id,
|
||||
wakeupRunId: wake?.id ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
async function evaluateWatchdog(row: IssueWatchdogRow, opts: { runId?: string | null } = {}) {
|
||||
const watchdog = await markTerminalWatchdogIssueReviewed(row, opts);
|
||||
const sourceIssue = await db
|
||||
@@ -1452,6 +1619,15 @@ export function taskWatchdogService(db: Db, deps: TaskWatchdogServiceDeps = {})
|
||||
return { state: classification.state, reason: classification.reason, classification };
|
||||
}
|
||||
|
||||
if (issueWatchdogMode(watchdog) === "self") {
|
||||
return triggerSelfReview({
|
||||
watchdog,
|
||||
sourceIssue,
|
||||
classification,
|
||||
runId: opts.runId ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
const existingWatchdogIssueId = watchdog.watchdogIssueId ?? (await findTaskWatchdogIssue(
|
||||
watchdog.companyId,
|
||||
sourceIssue.id,
|
||||
|
||||
Reference in new issue
Block a user