fix(server): keep a successor's issue lock when a finalizing native run releases its checkout (#19156)

`release_checkout` now clears `checkoutRunId` and the execution lock only when they belong to the committing run, so a successor that claimed the issue while its predecessor was finalizing keeps its claim and passes reviewed-chat attestation.

`attestReviewedExternalChatRun` reports the first failed check as a closed label. Setup failures store it in `resultJson.reviewedChatBinding.denialReason`, and Sentry run-failure diagnostics export it as `execution.reviewedChatBindingDenialReason` (no IDs or text). The error message is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Devin FoleyandClaude Opus 5.5 authored and GitHub committed 2026-11-04 17:49:03 -08:00
1 parent 6fa54d5577
commit 7edbc1fc07
9 files changed
+271 -34

No files matched your search

@@ -1540,13 +1540,17 @@ async function materializeDecisionEffect(input: {
}
if (effect.kind === "release_checkout") {
await notifyUnsafeWorkspaceExportRecovery(input.tx, { companyId: input.companyId, issueId: input.issue.id });
// Release only this run's own locks. A successor can claim execution
// without changing statusVersion, so a preserve decision committed after
// that claim must not clear it. Each SET reads the pre-update row.
const ownsExecution = sql`${issues.executionRunId} = ${input.runId}::uuid`;
const [released] = await input.tx
.update(issues)
.set({
checkoutRunId: null,
executionRunId: null,
executionAgentNameKey: null,
executionLockedAt: null,
checkoutRunId: sql`case when ${issues.checkoutRunId} = ${input.runId}::uuid then null else ${issues.checkoutRunId} end`,
executionRunId: sql`case when ${ownsExecution} then null else ${issues.executionRunId} end`,
executionAgentNameKey: sql`case when ${ownsExecution} then null else ${issues.executionAgentNameKey} end`,
executionLockedAt: sql`case when ${ownsExecution} then null else ${issues.executionLockedAt} end`,
updatedAt: new Date(),
})
.where(
@@ -1555,13 +1559,20 @@ async function materializeDecisionEffect(input: {
eq(issues.companyId, input.companyId),
),
)
.returning({ id: issues.id });
.returning({
id: issues.id,
checkoutRunId: issues.checkoutRunId,
executionRunId: issues.executionRunId,
});
if (!released) throw new Error("native_checkout_not_released");
return {
effectKind: effect.kind,
targetType: "issue_checkout",
targetId: released.id,
payload: { checkoutRunId: null, executionRunId: null },
payload: {
checkoutRunId: released.checkoutRunId,
executionRunId: released.executionRunId,
},
};
}
if (effect.kind === "record_recovery") {