* fix(action): flip wasUpdated on substantive MCP write tools (#563) Review/Respond/etc. agents that submit a `create_pull_request_review`, `create_issue_comment`, or `update_pull_request_body` and exit without calling `report_progress` were being marked as workflow failures by the strict completion check in handleAgentResult. Extend the set of tools that flip toolState.wasUpdated so a substantive user-visible artifact satisfies the check. The isReviewMode bypass is retained for IncrementalReview's non-substantive path. Flag is set BEFORE patchWorkflowRunFields / deleteProgressComment in each tool so a best-effort cleanup failure does not undo the signal. * fix(action): use finalSummaryWritten for stranded progress cleanup The stranded-progress-comment cleanup at the end of main() previously fired only when toolState.wasUpdated was false (or the tracker was the last writer). With wasUpdated now set by additional MCP write tools (create_issue_comment, update_pull_request_body), an agent that produced a substantive artifact via one of those tools and skipped report_progress would leave the placeholder "Leaping into action" comment intact — the post-script then converted it into an error message on a successful run. Key the cleanup off finalSummaryWritten instead. That flag is only set when report_progress actually wrote the progress comment, so it cleanly distinguishes "comment is finalized" from "agent did other work but never touched the progress comment". * refactor(mcp): extract markSubstantiveArtifact() helper replaces 4 inline `ctx.toolState.wasUpdated = true` flips in CreateCommentTool, UpdatePullRequestBodyTool, and CreatePullRequestReviewTool with a single helper in mcp/server.ts. JSDoc on the helper documents the contract (call BEFORE downstream patch/cleanup; gates the strict completion check and stranded-comment cleanup) so future MCP write tool authors only need to grep for one symbol. no behavioral change. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): only flip finalSummaryWritten after non-skipped write Previously the flag was set unconditionally on any non-plan call, including paths where reportProgress skipped (silent events, deleted comment, no issue/PR target). The cleanup check in main.ts is safeguarded by toolState.progressComment so the bug doesn't manifest today, but aligning the flag with actual writes matches the wasUpdated pattern and the design intent in the cleanup plan. * refactor(mcp): inline markSubstantiveArtifact helper --------- Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com> Co-authored-by: David Blass <david@arktype.io> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
pullfrog[bot]
parent
6db4a6d02e
commit
4c1413d925
@@ -705,20 +705,21 @@ export async function main(): Promise<MainResult> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// clean up stranded progress comments. two cases:
|
// clean up stranded progress comments. the comment is stale unless
|
||||||
// 1. wasUpdated=false: nothing wrote to the comment ("Leaping into action" orphan)
|
// report_progress wrote a final summary to it — three sub-cases all reduce
|
||||||
// 2. tracker published a checklist but the agent never wrote a final summary
|
// to !finalSummaryWritten:
|
||||||
// (hasPublished=true, finalSummaryWritten=false).
|
// 1. nothing wrote to the comment ("Leaping into action" orphan)
|
||||||
// in both cases, delete the comment so it doesn't linger with stale content.
|
// 2. tracker published a checklist but the agent never finalized it
|
||||||
// wasUpdated is intentionally NOT set here — cleanup is not a real progress update.
|
// 3. the agent produced a substantive artifact via another MCP write tool
|
||||||
// uses finalSummaryWritten (not todoTracker.enabled) so cleanup survives API failures
|
// (create_issue_comment, update_pull_request_body, reply_to_review_comment)
|
||||||
// in report_progress where cancel() ran but the write didn't succeed.
|
// and skipped report_progress — wasUpdated is true, but the progress
|
||||||
const trackerWasLastWriter = todoTracker?.hasPublished && !toolState.finalSummaryWritten;
|
// comment itself was never touched.
|
||||||
if (
|
// create_pull_request_review owns its own deletion (see action/mcp/review.ts),
|
||||||
toolContext &&
|
// so progressComment is already null by the time we get here for that path.
|
||||||
toolState.progressComment &&
|
// uses finalSummaryWritten (not todoTracker.enabled or wasUpdated) so cleanup
|
||||||
(!toolState.wasUpdated || trackerWasLastWriter)
|
// survives API failures in report_progress where cancel() ran but the write
|
||||||
) {
|
// didn't succeed, and isn't fooled by writes to *other* artifacts.
|
||||||
|
if (toolContext && toolState.progressComment && !toolState.finalSummaryWritten) {
|
||||||
await deleteProgressComment(toolContext).catch((error) => {
|
await deleteProgressComment(toolContext).catch((error) => {
|
||||||
log.debug(`stranded progress comment cleanup failed: ${error}`);
|
log.debug(`stranded progress comment cleanup failed: ${error}`);
|
||||||
});
|
});
|
||||||
|
|||||||
+8
-4
@@ -91,6 +91,8 @@ export function CreateCommentTool(ctx: ToolContext) {
|
|||||||
body: bodyWithFooter,
|
body: bodyWithFooter,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ctx.toolState.wasUpdated = true;
|
||||||
|
|
||||||
if (result.data.node_id) {
|
if (result.data.node_id) {
|
||||||
await patchWorkflowRunFields(ctx, { summaryCommentNodeId: result.data.node_id });
|
await patchWorkflowRunFields(ctx, { summaryCommentNodeId: result.data.node_id });
|
||||||
}
|
}
|
||||||
@@ -110,6 +112,8 @@ export function CreateCommentTool(ctx: ToolContext) {
|
|||||||
body: bodyWithFooter,
|
body: bodyWithFooter,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ctx.toolState.wasUpdated = true;
|
||||||
|
|
||||||
if (commentType === "Plan") {
|
if (commentType === "Plan") {
|
||||||
if (result.data.node_id) {
|
if (result.data.node_id) {
|
||||||
await patchWorkflowRunFields(ctx, { planCommentNodeId: result.data.node_id });
|
await patchWorkflowRunFields(ctx, { planCommentNodeId: result.data.node_id });
|
||||||
@@ -366,10 +370,6 @@ export function ReportProgressTool(ctx: ToolContext) {
|
|||||||
}
|
}
|
||||||
const result = await reportProgress(ctx, reportParams);
|
const result = await reportProgress(ctx, reportParams);
|
||||||
|
|
||||||
if (!params.target_plan_comment) {
|
|
||||||
ctx.toolState.finalSummaryWritten = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.action === "skipped") {
|
if (result.action === "skipped") {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -378,6 +378,10 @@ export function ReportProgressTool(ctx: ToolContext) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!params.target_plan_comment) {
|
||||||
|
ctx.toolState.finalSummaryWritten = true;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
...result,
|
...result,
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ export function UpdatePullRequestBodyTool(ctx: ToolContext) {
|
|||||||
body: bodyWithFooter,
|
body: bodyWithFooter,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ctx.toolState.wasUpdated = true;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
number: result.data.number,
|
number: result.data.number,
|
||||||
|
|||||||
@@ -537,6 +537,8 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) {
|
|||||||
reviewedSha: actuallyReviewedSha,
|
reviewedSha: actuallyReviewedSha,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ctx.toolState.wasUpdated = true;
|
||||||
|
|
||||||
// a submitted review obsoletes the progress comment — the review IS the
|
// a submitted review obsoletes the progress comment — the review IS the
|
||||||
// durable artifact. owned here (not in main.ts) so cleanup is atomic with
|
// durable artifact. owned here (not in main.ts) so cleanup is atomic with
|
||||||
// submission and survives any path out of the run (success, timeout,
|
// submission and survives any path out of the run (success, timeout,
|
||||||
|
|||||||
+8
-8
@@ -19,14 +19,14 @@ export async function handleAgentResult(ctx: HandleAgentResultParams): Promise<M
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Review and IncrementalReview modes intentionally never set wasUpdated:
|
// IncrementalReview's non-substantive path exits cleanly without
|
||||||
// the prompt forbids report_progress (the review IS the durable record),
|
// submitting any review, so no MCP write tool flips wasUpdated and the
|
||||||
// and IncrementalReview's non-substantive path produces no review at
|
// strict completion check below would otherwise fail the run. The
|
||||||
// all by design. wasUpdated staying false is also load-bearing for the
|
// isReviewMode skip is load-bearing for that path: the agent's exit
|
||||||
// stranded-comment cleanup in main.ts which deletes the "Leaping into
|
// code is the completion signal, not a progress-comment write.
|
||||||
// action" orphan via `(!wasUpdated || trackerWasLastWriter)`. Skip the
|
// (Review mode that submits a real review now flips wasUpdated via
|
||||||
// strict completion check for these modes — the agent's exit code is
|
// create_pull_request_review, so the skip is redundant for the
|
||||||
// the completion signal, not a progress-comment write.
|
// substantive-review path but kept for symmetry with IncrementalReview.)
|
||||||
// See plans/review_progress_comment_cleanup_b0120f6c.plan.md.
|
// See plans/review_progress_comment_cleanup_b0120f6c.plan.md.
|
||||||
const mode = ctx.toolState.selectedMode;
|
const mode = ctx.toolState.selectedMode;
|
||||||
const isReviewMode = mode === "Review" || mode === "IncrementalReview";
|
const isReviewMode = mode === "Review" || mode === "IncrementalReview";
|
||||||
|
|||||||
Reference in New Issue
Block a user