@@ -19863,6 +19863,7 @@ function issueReviewCommentState(
1986319863 number: number,
1986419864 fallbackBodies: readonly string[] = [],
1986519865): {
19866+ comments: Record<string, unknown>[];
1986619867 reviewComment: Record<string, unknown> | undefined;
1986719868 leaseComment: Record<string, unknown> | undefined;
1986819869 leaseComments: Record<string, unknown>[];
@@ -19885,6 +19886,7 @@ function issueReviewCommentState(
1988519886 : []),
1988619887 ];
1988719888 return {
19889+ comments,
1988819890 reviewComment,
1988919891 leaseComment: leaseComments[0],
1989019892 leaseComments,
@@ -20603,6 +20605,11 @@ function postReviewStartStatusComment(options: {
2060320605 );
2060420606 const body = renderReviewStartStatusComment(leaseOptions);
2060520607 const payload = writeCommentPayload(options.item.number, body);
20608+ // Every acquisition POSTs a fresh comment: the lowest-server-id election
20609+ // needs distinct ids per contender, so refreshing a leftover placeholder in
20610+ // place would let two racing workers both validate ownership of the same
20611+ // comment. Superseded placeholders are swept when the durable review
20612+ // comment is published instead.
2060620613 const createArgs = [
2060720614 "api",
2060820615 `repos/${targetRepo()}/issues/${options.item.number}/comments`,
@@ -20730,6 +20737,71 @@ function reapExpiredDedicatedReviewStartLeases(
2073020737 }
2073120738}
2073220739
20740+ const REVIEW_PLACEHOLDER_BODY_PATTERN = /^ClawSweeper status: review started\./i;
20741+
20742+ export function supersededReviewPlaceholderCommentIds(options: {
20743+ number: number;
20744+ comments: readonly Record<string, unknown>[];
20745+ keepCommentIds: ReadonlySet<number>;
20746+ nowMs?: number;
20747+ }): number[] {
20748+ const nowMs = options.nowMs ?? Date.now();
20749+ const ids: number[] = [];
20750+ for (const comment of options.comments) {
20751+ const id = commentId(comment);
20752+ if (id === null || options.keepCommentIds.has(id)) continue;
20753+ if (!canPatchReviewComment(comment)) continue;
20754+ const body = (commentBody(comment) ?? "").trimStart();
20755+ // Placeholder bodies start with the status line; the durable review
20756+ // comment never does, and its marker is an extra guard against deletion.
20757+ if (!REVIEW_PLACEHOLDER_BODY_PATTERN.test(body)) continue;
20758+ if (body.includes(reviewCommentMarker(options.number))) continue;
20759+ // An unexpired lease may belong to a racing worker on a newer revision;
20760+ // only provably superseded placeholders (expired lease or marker-less
20761+ // legacy body) are swept after the durable review comment is published.
20762+ const marker = body.match(/<!--\s*clawsweeper-review-status:started\b([^>]*)-->/i);
20763+ if (marker) {
20764+ const expiresAtMs = Date.parse(marker[1]?.match(/\blease_expires_at=([^\s>]+)/i)?.[1] ?? "");
20765+ if (Number.isFinite(expiresAtMs) && expiresAtMs >= nowMs) continue;
20766+ }
20767+ ids.push(id);
20768+ }
20769+ return ids;
20770+ }
20771+
20772+ function cleanupSupersededReviewPlaceholderComments(options: {
20773+ number: number;
20774+ // Pre-mutation snapshot from the apply flow; the sweep must not refetch the
20775+ // comment list after the durable-comment mutation (API-budget invariant).
20776+ comments: readonly Record<string, unknown>[];
20777+ keepCommentIds: ReadonlySet<number>;
20778+ }): void {
20779+ const ids = supersededReviewPlaceholderCommentIds({
20780+ number: options.number,
20781+ comments: options.comments,
20782+ keepCommentIds: options.keepCommentIds,
20783+ });
20784+ for (const id of ids) {
20785+ try {
20786+ ghObservedMutationCommand({
20787+ identity: `review_placeholder_sweep:${options.number}:${id}`,
20788+ args: ["api", `repos/${targetRepo()}/issues/comments/${id}`, "--method", "DELETE"],
20789+ });
20790+ console.error(
20791+ `[apply] deleted superseded review placeholder comment ${id} for #${options.number}`,
20792+ );
20793+ } catch (error) {
20794+ if (error instanceof GitHubRuntimeBudgetError) throw error;
20795+ // A failed sweep must never fail the publish; the next apply retries it.
20796+ console.error(
20797+ `[apply] could not delete superseded review placeholder comment ${id} for #${options.number}: ${
20798+ error instanceof Error ? error.message : String(error)
20799+ }`,
20800+ );
20801+ }
20802+ }
20803+ }
20804+
2073320805function pullRequestHeadSha(number: number): string {
2073420806 const pull = asRecord(ghJson<unknown>(["api", `repos/${targetRepo()}/pulls/${number}`]));
2073520807 const sha = asRecord(pull.head).sha;
@@ -26891,6 +26963,7 @@ function applyDecisionsCommandInner(args: Args, runtimeBudget: GitHubRuntimeBudg
2689126963 if (!headBefore || headBefore !== headAfter || headAfter !== initialReviewHeadSha) {
2689226964 return {
2689326965 comment: refreshed.reviewComment,
26966+ comments: refreshed.comments,
2689426967 leaseComments: refreshed.leaseComments,
2689526968 headSha: headAfter,
2689626969 lease: null,
@@ -26901,18 +26974,22 @@ function applyDecisionsCommandInner(args: Args, runtimeBudget: GitHubRuntimeBudg
2690126974 if (item.kind === "issue" && reportReviewRevision && headAfter !== reportReviewRevision) {
2690226975 return {
2690326976 comment: refreshed.reviewComment,
26977+ comments: refreshed.comments,
2690426978 leaseComments: refreshed.leaseComments,
2690526979 headSha: headAfter,
2690626980 lease: null,
2690726981 preserve: false,
2690826982 blockReason: `live issue source revision ${headAfter} differs from reviewed revision ${reportReviewRevision}`,
2690926983 };
2691026984 }
26911- return reviewStartLeaseStateForComments(
26912- refreshed.leaseComments,
26913- refreshed.reviewComment,
26914- headAfter,
26915- );
26985+ return {
26986+ ...reviewStartLeaseStateForComments(
26987+ refreshed.leaseComments,
26988+ refreshed.reviewComment,
26989+ headAfter,
26990+ ),
26991+ comments: refreshed.comments,
26992+ };
2691626993 } catch (error) {
2691726994 if (error instanceof GitHubRuntimeBudgetError) throw error;
2691826995 const detail = trimMiddle(
@@ -26921,6 +26998,7 @@ function applyDecisionsCommandInner(args: Args, runtimeBudget: GitHubRuntimeBudg
2692126998 );
2692226999 return {
2692327000 comment: undefined,
27001+ comments: [] as Record<string, unknown>[],
2692427002 leaseComments: [],
2692527003 headSha: "",
2692627004 lease: null,
@@ -28902,6 +28980,25 @@ function applyDecisionsCommandInner(args: Args, runtimeBudget: GitHubRuntimeBudg
2890228980 allowedSelfMutationUpdatedAts.add(syncedCommentUpdatedAt);
2890328981 }
2890428982 syncReasons.push("updated durable Codex review comment");
28983+ // The durable review comment is now published, so stale "review
28984+ // started" placeholders from failed earlier attempts are clutter.
28985+ const placeholderKeepCommentIds = new Set<number>();
28986+ const syncedCommentId = commentId(syncedComment);
28987+ if (syncedCommentId !== null) placeholderKeepCommentIds.add(syncedCommentId);
28988+ // Closures assign the active lease, so read it through a cast to
28989+ // defeat TypeScript's stale null narrowing at this use site.
28990+ const heldMutationLease = activeApplyMutationLease as {
28991+ itemNumber: number;
28992+ lease: AcquiredReviewStartLease;
28993+ } | null;
28994+ if (heldMutationLease?.itemNumber === number) {
28995+ placeholderKeepCommentIds.add(heldMutationLease.lease.commentId);
28996+ }
28997+ cleanupSupersededReviewPlaceholderComments({
28998+ number,
28999+ comments: latestLeaseState.comments,
29000+ keepCommentIds: placeholderKeepCommentIds,
29001+ });
2890529002 } catch (error) {
2890629003 const commentAuthError = isGitHubRequiresAuthenticationError(error);
2890729004 if (!commentAuthError && !isLockedConversationCommentError(error)) throw error;
0 commit comments