From 8c6cd2bda2d6be66367d7d4f93b00c3efbefc214 Mon Sep 17 00:00:00 2001 From: David Blass Date: Mon, 11 May 2026 23:20:44 +0000 Subject: [PATCH] cancel + restart workflow run when @pullfrog mention is edited (#612) * cancel + restart workflow run when @pullfrog mention is edited - add `WorkflowRun.triggeringCommentId` (BigInt?, indexed) so the webhook handler can find the run that was fired by a given comment - thread `triggeringCommentId` through `reserveRun` / `triggerWorkflow` - factor `dispatchMentionRun` out of `issue_comment_created` so the same shape is reused on edit - replace the `issue_comment_edited` stub: re-evaluates the trigger gate, cancels prior runs (`octokit.rest.actions.cancelWorkflowRun` + DB status='cancelled'), then re-dispatches with a `previousRunsNote` appended to `eventInstructions` so the agent acknowledges the prior run/PR/artifacts in its summary - if the edit removes `@pullfrog`, cancel only (no restart) Co-authored-by: Cursor * thread previousRunsNote via dedicated payload field user prompt has precedence over eventInstructions, so stuffing the prior-runs note into eventInstructions made it vanish whenever the trigger comment contained an @pullfrog mention (which is always for the edit path). pass it as its own payload field and render it alongside the user's task so the agent actually sees it. * delete cancelled run's progress comment on edit-restart so the issue thread doesn't accumulate "This run was cancelled" stubs on every edit. only deletes for runs we actively cancel; runs that were already terminal (e.g. completed before the edit) keep their summary comment in the thread, and `previousRunsNote` links to it so the new agent can reference prior work. post-cleanup is race-safe: the action's `validateStuckProgressComment` swallows the 404 from the deleted comment and exits cleanly, so the old run's post step cannot clobber the new run's leaping comment. Co-authored-by: Cursor * also cancel + delete progress comment when triggering comment is deleted mirrors the edit-removes-@pullfrog path: when an @pullfrog comment that fired a run is hard-deleted, look up any prior runs by triggeringCommentId, GH-cancel running ones, and delete their leaping progress comments. skips trigger-gate re-eval (we're tearing down a run, not firing one) and performs no restart. reuses the existing cancelRunsForTriggeringComment helper; the returned previousRunsNote is discarded since no dispatch follows. Co-authored-by: Cursor * fix: move cancellation before trigger gate in issue_comment_edited cancelRunsForTriggeringComment now runs before the triggerEnabled check, so edits that remove @pullfrog still cancel in-flight runs even when the repo mention trigger is currently disabled (e.g. for non-collaborators). * anneal: scope cancel updates per-row + simplify edit gate - replace blanket updateMany on (triggeringCommentId, repoId) with per-row, status-guarded updates so a parallel handler's freshly-reserved run cannot be clobbered into cancelled by a racing edit delivery. - drop wasMention/isMention early-break in issue_comment_edited; always run cancelRunsForTriggeringComment (DB is the canonical "did this comment ever trigger a run" source). closes the missing-changes.body.from edge and lets us tear down a still-running prior run even if the admin disabled the mention trigger mid-flight. - buildPreviousRunsNote returns undefined (not "") when no link lines materialize. - doc cleanups + wiki/modes.md addendum noting issue_comment_edited / _deleted now drive cancel + restart. Co-authored-by: Cursor * address review feedback on cancel/restart semantics - guard workflow_run.completed update against status='cancelled' so a successful-but-uncancellable GH Actions job can't resurrect a cancelled row (and re-bill it) via the completed webhook. - bucket only status='completed' runs into `preserved` in cancelRunsForTriggeringComment; cancelled/failed prior runs have stubs as their progress comment, not summaries worth referencing. - emit previousRunsNote for the runId-null cancel case so the restarted agent always knows when it's superseding a prior dispatch. - drop the agent-forbidden `gh pr list` hint and soften 'was cancelled' to 'was signalled to cancel' in the note body. - post a fallback comment when the edit-path dispatch fails (prior run already torn down and progress comment already deleted). - symmetrize the delete-handler's pullfrog guard with the edit handler (key off hook.comment.user, not hook.sender). - trim misleading comments on the per-row DB update guard. --------- Co-authored-by: Cursor Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com> Co-authored-by: Colin McDonnell --- external.ts | 7 +++++++ utils/instructions.ts | 15 +++++++++++---- utils/payload.ts | 2 ++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/external.ts b/external.ts index 9a4d88a..052b4ee 100644 --- a/external.ts +++ b/external.ts @@ -273,6 +273,13 @@ export interface WriteablePayload { triggerer?: string | undefined; /** event-level instructions for this trigger type (flag-expanded server-side) */ eventInstructions?: string | undefined; + /** + * system-injected note about prior superseded runs (e.g. when the + * triggering @pullfrog comment is edited). rendered alongside the user's + * prompt rather than via eventInstructions so it survives user-prompt + * precedence. + */ + previousRunsNote?: string | undefined; /** event data from webhook payload - discriminated union based on trigger field */ event: PayloadEvent; /** timeout for agent run (e.g., "10m", "1h30m") - defaults to "1h" */ diff --git a/utils/instructions.ts b/utils/instructions.ts index 3f14d42..07a48fc 100644 --- a/utils/instructions.ts +++ b/utils/instructions.ts @@ -32,6 +32,7 @@ function buildRuntimeContext(ctx: InstructionsContext): string { "~pullfrog": _, prompt: _p, eventInstructions: _ei, + previousRunsNote: _prn, event: _e, ...payloadRest } = ctx.payload; @@ -146,17 +147,23 @@ In case of conflict between instructions, follow this precedence (highest to low // section builders // --------------------------------------------------------------------------- -// the user's task: blockquoted user prompt, or event-level instructions for auto-triggers +// the user's task: blockquoted user prompt, or event-level instructions for auto-triggers. +// `previousRunsNote` is system-injected context (e.g. prior runs superseded by a +// comment edit); it's appended regardless of which branch wins so it survives +// user-prompt precedence over eventInstructions. function buildTaskSection(ctx: PromptContext): string { + const previousRunsNote = ctx.payload.previousRunsNote?.trim() ?? ""; + if (ctx.userQuoted) { + const parts = [ctx.userQuoted, previousRunsNote].filter(Boolean); return `************* YOUR TASK ************* -${ctx.userQuoted}`; +${parts.join("\n\n")}`; } const eventInstructions = ctx.payload.eventInstructions ?? ""; - if (eventInstructions) { - const parts = [ctx.eventTitle, eventInstructions].filter(Boolean); + if (eventInstructions || previousRunsNote) { + const parts = [ctx.eventTitle, eventInstructions, previousRunsNote].filter(Boolean); return `************* YOUR TASK ************* ${parts.join("\n\n")}`; diff --git a/utils/payload.ts b/utils/payload.ts index fd526ae..13003a9 100644 --- a/utils/payload.ts +++ b/utils/payload.ts @@ -21,6 +21,7 @@ export const JsonPayload = type({ "triggerer?": "string | undefined", "eventInstructions?": "string", + "previousRunsNote?": "string", "event?": "object", "timeout?": "string | undefined", "progressComment?": type({ @@ -157,6 +158,7 @@ export function resolvePayload( // it's not a common use case but GITHUB_ACTOR can be a user when the workflow is manually triggered by a user through GitHub Actions UI (!isPullfrog(process.env.GITHUB_ACTOR) ? process.env.GITHUB_ACTOR : undefined), eventInstructions: jsonPayload?.eventInstructions, + previousRunsNote: jsonPayload?.previousRunsNote, event, timeout: inputs.timeout ?? jsonPayload?.timeout, cwd: resolveCwd(inputs.cwd),