Skip to content

Fix stale execution-lock ownership and serialized verification (BLO-21557) - #1054

Open
kkroo wants to merge 27 commits into
masterfrom
release/blo-21557-pr911-reviewed
Open

Fix stale execution-lock ownership and serialized verification (BLO-21557)#1054
kkroo wants to merge 27 commits into
masterfrom
release/blo-21557-pr911-reviewed

Conversation

@kkroo

@kkroo kkroo commented Aug 5, 2026

Copy link
Copy Markdown

Thinking Path

  • Paperclip coordinates agent work through issues, heartbeat runs, and guarded issue ownership.
  • Issue mutations use checkoutRunId and executionRunId to prevent concurrent runs from overwriting one another.
  • PR fix(issues): a never-started run must not lock out its own assignee (BLO-20321) #911 fixed never-started queued owners that could permanently lock an assignee out of its own issue, then PR fix(issues): validate owners on non-active release (BLO-21607) #1005 closed a release-path ownership gap.
  • The original PR could not be approved under its author identity, so BLO-21557 requires an independently authored replacement preserving the reviewed behavior.
  • This pull request reopens that work from release/blo-21557-pr911-reviewed, refreshed onto current master without weakening the two-owner checks.
  • Ally found a stale reviewRequest race on the refreshed head; commit 69b6818f extends the execution snapshot CAS to every derived executionState write.

Linked Issues or Issue Description

What Changed

Verification

  • pnpm exec vitest run server/src/__tests__/issue-execution-policy-routes.test.ts -t "pins the execution snapshot": 1 passed.
  • pnpm exec vitest run server/src/__tests__/issue-stale-execution-lock-routes.test.ts -t "stale writer" --no-file-parallelism --maxWorkers=1: 2 passed, covering both commit orders.
  • pnpm --filter @paperclipai/server typecheck: passed.
  • Git ancestry confirms reviewed post-fix head e2a06589ab4e80d7f59b439a2fefe33a7192380a and release fix 9205faeef39bc4573d117f976a5cfdd9b0e2cc98 are ancestors of the PR head.
  • Required GitHub CI and fresh Ally review on the exact mergeable head remain mandatory before merge.

Risks

  • Concurrency-sensitive behavior: weakening owner classification or transaction boundaries could reintroduce an ownership steal, strand, or stale-stage overwrite. Route and database regressions cover these boundaries.
  • Conflict-resolution risk: the branch was refreshed onto newer master; the resolution retained both the reviewed ownership coverage and newer stale-sweeper coverage.
  • No migration or public API-shape change.

Model Used

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
  • I have searched GitHub for duplicate or related PRs and linked them above
  • I have either linked existing issues or described the issue in-PR
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • UI screenshots are not applicable; this is server-only
  • Documentation changes are not applicable; there is no public contract change
  • I have considered and documented risks above
  • All Paperclip CI gates are green
  • Ally has cleared the exact current head
  • I will address all reviewer comments before requesting merge

allyblockcast Bot and others added 24 commits August 1, 2026 09:20
…BLO-20321)

`queued` and `scheduled_retry` runs are non-terminal, so the terminal-only
staleness test treated a run that had never executed as a live owner. The
assignee's own PATCH/release on its own issue was answered `409 Issue run
ownership conflict`.

That made WIP monotonic. Checkout adds WIP without holding a lock; parking or
closing removes WIP and needs the lock — and the lock was held by the very queue
backlog being drained. The deeper the queue, the slower each issue's run
arrives, so the drain rate falls as the backlog grows. Measured on the CTO
agent: 47 of 59 in_progress issues were immutable to their own assignee.

Adds isReapableHeartbeatRunRow as the single source of truth for "is this lock
owner reapable": terminal OR missing OR (queued/scheduled_retry AND
startedAt IS NULL). Gating on startedAt as well as status means a run that has
actually begun is never reaped, so the race protection the guard exists for is
preserved.

Applied at the two guards that decide the assignee's own write:
  - adoptStaleCheckoutRun — runs FIRST when checkoutRunId is set, and a
    non-stale verdict there throws the 409 before the other is consulted, so
    fixing only the second would have left the fix unreachable for the common
    shape (both locks pointing at one never-started run).
  - clearStaleExecutionLock — backs assertCheckoutOwner (PATCH) and checkout.
  - release's own copy of the test.

Both adoption paths already call cancelStaleIssueContextRuns, so the superseded
run is cancelled by the write and cannot start later against a status the
assignee has since changed.

Scope: the change sits strictly downstream of authorization —
assertAgentIssueMutationAllowed runs decideIssueAccess and the
assignee-identity check before ever calling assertCheckoutOwner — so the authz
boundary is untouched and a non-assignee without a grant is still refused.
checkout's startedAt-preserving adoption branch is deliberately left
terminal-only; see the comment there.

Tests: six cases in issue-stale-execution-lock-routes.test.ts. The four
behavioural ones fail on master with the exact reported 409; the two protection
cases (live `running` owner still 409s, peer still refused) pass on master, so
they are genuine regression guards.

Co-Authored-By: Claude <noreply@anthropic.com>
…20260801

# Conflicts:
#	server/src/routes/issues.ts
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Adoption of an issue's checkout lock is authorised by EITHER limb of

  if ((!stale && !sameAgentRetry) || !actorLive) return { adopted: null, ... }

but the reap gate this branch added below it (d2a0cf7, 43ecd77) was written
as an unconditional postcondition, which makes it an implicit conjunction over
both limbs:

  if (!(await cancelNeverStartedOwnerRun(tx, existingRun, cancellation)) || ...)

cancelNeverStartedOwnerRun returns false for any run that is not reapable. A
same-agent retry adopts from its own parent run, which is still `running` and
therefore never reapable, so the gate returned false and the retry was answered
409 Issue run ownership conflict. The sameAgentRetry limb became dead code,
silently regressing BLO-6869 (0ba33fa).

Gate the reap on `stale` so it is demanded only where it is meaningful.
Staleness-authorised adoption still MUST reap, so a never-started owner cannot
start later against a state the adopter has since changed; retry-authorised
adoption supersedes a live parent, which is exactly what master does today.
Race protection is untouched: a divergent live execution owner is still refused
above by executionOwnerIsAllowed.

Caught by Verify serialized server suites (1/4) — the first execution of that
job on this branch. issues-service.test.ts is only in the serialized shard, and
verify_serialized_server is needs: [policy, general_tests] with no
`if: always()`, so every earlier red general_tests skipped it and the defect sat
across three heads for ~4.5h behind 16 green checks.

Tests: issues-service.test.ts + issue-stale-execution-lock-routes.test.ts,
--no-file-parallelism --maxWorkers=1: 205/205 pass, EXIT=0. The BLO-6869 case
fails without this change with the exact reported 409.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Staff Engineer <staff-engineer@paperclip.local>
…r911-reviewed

# Conflicts:
#	server/src/__tests__/issue-stale-execution-lock-routes.test.ts
@allyblockcast

allyblockcast Bot commented Aug 5, 2026

Copy link
Copy Markdown

🔗 Paperclip issue: BLO-21557

1 similar comment
@allyblockcast

allyblockcast Bot commented Aug 5, 2026

Copy link
Copy Markdown

🔗 Paperclip issue: BLO-21557

@cursor

cursor Bot commented Aug 5, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@allyblockcast

allyblockcast Bot commented Aug 5, 2026

Copy link
Copy Markdown

Hey @kkroo! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

  • Missing section: ## Thinking Path
  • Missing section: ## What Changed
  • Missing section: ## Verification
  • Missing section: ## Risks
  • Missing section: ## Model Used
  • Add the dedup-search checkbox to your PR description and check it once you have searched the GitHub PR list for similar PRs. See the PR template at .github/PULL_REQUEST_TEMPLATE.md and CONTRIBUTING.md → "Before You Start: Search First".

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: d2a6b82

Critical Issues (0)

Important Issues (1)

  • [pr-review-toolkit + native-codex] server/src/routes/issues.ts:9822 — The new execution-state/policy compare-and-swap is applied only when transition.decision is present, but this route also derives and writes executionState from the earlier existing snapshot for reviewRequest updates. If request A prepares a reviewRequest patch, request B commits the current participant's decision, and then A enters svc.update, A takes the non-decision branch without either expected-state predicate and can overwrite B's completed/advanced state with the stale pending stage. This can resurrect an already-decided stage or leave status and execution state inconsistent.
    • Apply the expected execution-state/policy predicates whenever the route writes a derived executionState, not only when it records a decision, and add a concurrent reviewRequest versus approval regression covering both commit orders.

Suggestions (1)

  • [CI] The review check currently fails because the PR description omits the repository's required Thinking Path, What Changed, Verification, Risks, Model Used, and dedup-search sections. Update the description so the quality gate can pass.

Strengths

  • Never-started owner cancellation and ownership transfer occur under the same issue/run row locks, making dispatch claim and stale-owner reaping mutually exclusive.
  • Divergent live checkout and execution owners remain fail-closed, while independently reapable owners can be recovered transactionally.
  • Release validates both ownership columns for agent and board callers and cancels exact queued owners before clearing the issue.
  • Decision-bearing updates atomically pin both execution state and policy and persist the decision row in the same transaction.

Recommended Action

  1. Extend the execution-state CAS to non-decision derived state writes before merge.
  2. Update the PR description to satisfy the required quality gate.

@allyblockcast

allyblockcast Bot commented Aug 5, 2026

Copy link
Copy Markdown

@ally please review exact head d2a6b8222bd0a53cbea7d6a028ccd536208631fa, focusing on the stale execution-lock two-owner invariants, non-in_progress release validation from #1005, conflict resolution against current master, and serialized execution-policy verification. The PR-template review gate now passes without changing the head.

@allyblockcast

allyblockcast Bot commented Aug 5, 2026

Copy link
Copy Markdown

@ally please re-review exact head 69b6818f, focusing on the execution-state/policy CAS now applied to non-decision reviewRequest writes and the regressions covering both review-request/approval commit orders. Focused tests pass and server typecheck is clean.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 69b6818

Prior Findings Dispositioned (1)

  • prior:d2a6b82 important 1 — fixed — server/src/routes/issues.ts:9811 — Every derived executionState write now carries the route snapshot's execution-state and execution-policy preconditions; server/src/__tests__/issue-stale-execution-lock-routes.test.ts:1773 verifies that either a review request or a decision wins and the stale writer receives 409.

Critical Issues (0)

Important Issues (1)

  • [gstack/review + native-codex] server/src/routes/issues.ts:9811 — The new snapshot predicate still omits status, even though the execution-participant exception is authorized only from an in_review snapshot at server/src/routes/issues.ts:4747. A concurrent release can move the issue to todo and clear ownership without changing executionState or executionPolicy; the stale participant decision or review-request write then still satisfies both JSON predicates and can overwrite the newer status or attach review state to a released issue.
    • Include expectedCurrentStatus: existing.status whenever the route writes a derived executionState, and add release-vs-decision and release-vs-reviewRequest regressions covering both commit orders.

Strengths

  • The prior review-request-versus-decision race is now serialized at the database write, with both commit orders covered.
  • Execution state and policy are compared in the final UPDATE predicate rather than only before the transaction blocks.
  • The stale-owner tests exercise divergent owners, live-owner refusal, and never-started-run cancellation across checkout, patch, and release paths.

Recommended Action

  1. Pin the status snapshot for execution-state-derived writes and cover concurrent release before merge.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 35ac42f

Prior Findings Dispositioned (1)

  • prior:69b6818 important 1 — fixed — server/src/routes/issues.ts:9814 — Every derived executionState write now pins the authorizing status snapshot, and server/src/__tests__/issue-stale-execution-lock-routes.test.ts:1908 verifies that release winning first rejects both stale decision and stale review-request writes.

Critical Issues (0)

Important Issues (2)

  • [native-codex] server/src/services/issues.ts:9634 — Stale-lock checkout can overwrite a concurrent reassignment. clearStaleExecutionLock() commits and releases the issue row lock before this retry; if another request assigns the still-eligible issue to agent B in that gap, the retry pins status and run IDs but not assigneeAgentId, then writes agent A over B's newer assignment.
    • Keep stale-lock clearing and adoption in one transaction, or include the originally observed assignee in the retry CAS predicate. Add a regression that pauses between clear and retry while another writer reassigns the issue.
  • [native-codex] server/src/routes/issues.ts:9811 — Run ownership is checked only during route authorization, not at the final write. A request from run A can pass isCurrentIssueExecutionRun; after a force-release and checkout transfers both ownership pointers to run B without changing execution state/policy, A's ordinary patch has no snapshot preconditions at all, while an execution-state patch pins only status/state/policy. The superseded run can therefore mutate the newly owned issue.
    • Carry the authorized checkoutRunId and executionRunId snapshots into svc.update() and include both in the final SQL predicate, or re-authorize under an issue-row lock. Cover ownership transfer between authorization and write for both ordinary and execution-state patches.

Strengths

  • The prior release-versus-derived-state race is now closed with status, execution-state, and execution-policy predicates in the final SQL update.
  • Owner run rows are locked in deterministic order, and never-started owner cancellation uses guarded writes.
  • The expanded concurrency suite covers divergent live owners, stale decision writers, dispatch races, and both release ordering outcomes.

Recommended Action

  1. Close the stale-clear/reassignment window before merge.
  2. Pin authorization-relevant run ownership through the final issue update.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants