Repository navigation
fix(agent-work): preserve stale-AgentWork rows still owed a claim release (#1350) - #1389
Conversation
npm audit --omit=dev (blocking CI gate) failed on main and on this PR head: body-parser 2.3.0 was high (GHSA-27r3-jxmh-p828, DoS via trailing bytes after a compressed body), hono 4.13.7 and ip-address 10.7.2 were moderate. npm audit fix --package-lock-only --include=prod bumps body-parser to 2.4.0, hono to 4.13.13, ip-address to 10.7.4 and qs to 6.16.1; npm also re-resolved the dev tree (vite, rolldown, @oxc-project/types and @rolldown/* bindings) inside the same existing ranges. package.json and every range are unchanged; package-lock.json was already identical to main before this commit, so the failure pre-dates this PR. Local validation at this change state: npm ci --include=dev, npm run audit (0 vulnerabilities), typecheck, lint (1 pre-existing warning) and vitest all exit 0.
…1350) The claim-path cleanup's fenced delete re-checked the state family and the pending-release boundary, but not the ownership fields the scan matched (`issueId`, `agentName`). A row reassigned between the scan and the delete kept its id, state and marker, so the fence consumed it and the newly owned row was deleted instead of being left to its new owner. The delete now carries the scanned ownership fields as well, so the fence matches the scan on every conjunct that identifies the row. Tests cover the reassignment race, the concurrent state settle (operator release), the scan's issue scope, and the pending boundary; each fence conjunct is mutation-covered. Also correct the comment that credited the per-issue mutation guard for the fence's safety: the operator agent-work routes do not take that guard, so the fence itself is the load-bearing protection.
|
Addressed the review feedback on Major — the delete fence omitted the scanned ownership fields. The fence re-checked The claims that were reported as holding were left as they were and re-verified: the scan still excludes both pending shapes ( Added coverage for the race the finding describes — a concurrent reassignment between the scan and the delete leaves the row in place, and the delete is pinned to carry the scanned owner. Two further gaps a reviewer session found in the same code are closed in the same commit: the fence's Local validation on The |
Superseded by a newer automated review for this pull request.
|
Takeover handoff — head State assessment
Independent local validation (re-run by this takeover, not inherited; workspace
Limitation: this workspace has no Postgres, so the DB-backed integration path was not exercised locally (CI's Database integration job covers it). Remaining external verification (explicitly pending, not claimed green here)
No merge is performed by this run; the PR is left open for operator verification and human review. |
|
Courier: already addressed (run courier-b7ad227788dfe837b5764bb83bb0e709) Took over PR #1389 (misospace/dispatch, head 75351d4, branch courier/misospace/dispatch/issue-1350). No code change was required: all feedback present on the PR is already addressed at that head. (1) The AI review's Major finding on d1c0a62 (delete fence omitted the scanned ownership fields, so a row reassigned between scan and delete could still be deleted) is fixed in 75351d4 - the delete where now carries issueId and agentName alongside the state family and staleClaimReleasePending; the separate reviewer session's three items (state-conjunct test, issueId-conjunct test + test fake honouring where.issueId, corrected comment about the operator routes not taking the mutation guard) are also folded into 75351d4. (2) The AI review on head 75351d4 is clean/approved; get_review_threads reports 0 open threads and there are no unresolved comments. (3) CI on head: all check runs green except 'npm audit', which is pre-existing on main and tracked in #1386 - this diff touches only src/lib/agent-work.ts and src/lib/agent-work.test.ts with no dependency/lockfile change. (4) The PR body's out-of-scope exposure (delete fence does not re-read the lease set) is filed as open issue #1390. (5) Head verified published: remote ref courier/misospace/dispatch/issue-1350 sha == local HEAD == PR head sha; PR open and non-draft; working tree clean; no merge performed. Independent local validation re-run by this takeover (cwd /workspace, revision 75351d4, deps via npm ci): npm run lint = 0 errors + 1 pre-existing unrelated warning (src/app/login/page.tsx:39); npm run typecheck = clean; npx vitest run src/lib/agent-work.test.ts src/app/api/issues/claim/route.test.ts src/lib/stale-work.test.ts = 3 files passed / 187 tests passed; npm run test = 206 files passed / 5 skipped, 5322 passed / 0 failed, 27 skipped, 1 todo (149.5s). Additionally reproduced the PR's mutation-coverage claim independently: removing each delete-fence conjunct (state, agentName, issueId, staleClaimReleasePending) in turn makes src/lib/agent-work.test.ts fail with assertion failures (baseline 66 passed), with the file restored and the tree clean afterwards. Limitation: no Postgres in this workspace, so the DB integration path was not exercised locally. Pending external verification left explicitly pending: human review by joryirving, and the pre-existing npm audit CI failure (#1386). A concise takeover handoff comment with the settled decisions, check receipts and remaining external verification was posted on PR #1389. |
…udit accounting (#1350) The doc comment claimed "Live writers never leave the marker on an active-shaped row (the sweep pairs it with the STALE mark), so the boundary is defence in depth on top of the state filter". That is false: the operator agent-work reassign route rewrites a row to `state: "CLAIMED"` without clearing `staleClaimReleasePending`, so a sweep-marked `STALE` row can arrive at the claim-path cleanup as an ACTIVE-SHAPED row that still owes its release — and the state filter alone would not keep it. The marker conjunct is load-bearing. The same comment also referred to "the other callers" of this function. There is exactly one production caller (`src/app/api/issues/claim/route.ts`), and it does take the per-issue mutation guard. The parties that do not take the guard are the concurrent writers this cleanup races with, which is what the fence has to survive. Neither correction changes behaviour; both make the recorded rationale match the code. The stranded-release gap the reassign route opens (the marker resurrected onto an active-shaped row is inert for the sweep: its reclaim arms require `staleClaimReleasePending: false`, its retry arm requires `state: "STALE"`) is pre-existing and filed as #1391. Adds a mixed-case regression test: with one candidate consumed and one refused by the fence, the returned count and the audit's id list must name the consumed row only. The accounting claim was previously pinned only in the all-consumed case, so a regression back to reporting the scanned ids would have passed. Local validation: `npx vitest run src/lib/agent-work.test.ts src/app/api/issues/claim/route.test.ts src/lib/stale-work.test.ts src/app/api/agent-work/route.test.ts` (4 files, 225 passed); `npm run test` (5323 passed, 0 failed, 27 skipped, 1 todo); `npm run lint` (0 errors, 1 pre-existing unrelated warning in src/app/login/page.tsx); `npm run typecheck` (clean).
|
Takeover #2 — head advanced to State assessment
Independent local validation (re-run by this takeover on
Limitation: no Postgres in this workspace, so the DB-backed integration path (real generated SQL, cascade of Remaining external verification (explicitly pending, not claimed green here)
Review limitation: the reviewer session used the same configured model family (MiniMax-M3.1-Flash) as the coordinator, so it provides no model-family diversity. No merge is performed by this run; the PR is left open for operator verification and human review. |
…ction invariant (#1350) A reviewer session on the final diff raised five items against the claim-path cleanup's fence; four are fixed here and the fifth is answered on the PR. - The comments named a "self-heal" route family that does not exist and omitted the writers that actually race the cleanup: the operator agent-work routes (release / release-all / reassign) and the worker agent-work start / checkpoint / finish paths. They also claimed that a pending row always belongs to the sweep's retry arm, which is false for an ACTIVE-SHAPED pending row (the retry arm requires state STALE), so the comment now distinguishes the two shapes and says plainly that this cleanup is the repository's only `agentWork.deleteMany`. - The delete's `issueId` conjunct was not exercised by any test, so dropping it passed the suite. A new test moves a row to another issue between the scan and the delete and pins that the fence refuses it. - The "a deletion is never left without its audit row" invariant was unpinned. A new test asserts that the delete and the audit row share one transaction client, that the audit write follows the delete, and that a failing audit write surfaces instead of the cleanup reporting a successful deletion. No behaviour change.
|
Takeover #3 — head advanced to State assessment
Answer to the remaining item — the per-row CAS statement count. Not changed, deliberately: a bulk Independent local validation (re-run by this takeover, cwd
The Limitation: no Postgres in this workspace, so the DB-backed integration path (real generated SQL, the Follow-up filed
Remaining external verification (explicitly pending, not claimed green here)
Review limitation: every review session on this PR (the AI PR reviewer and the separate coordinator-run sessions) used the same configured model family (MiniMax-M3.1-Flash) as the coordinator, so no model-family diversity was available. No merge is performed by this run; the PR is left open for operator verification and human review. |
…c reference (#1350) The claim-path cleanup's safety comment enumerated the writers it races with but omitted one: the operator unclaim path calls `releaseAgentWorkByAgentAndIssue` (`src/lib/lease.ts`) AFTER its guarded block, so it writes `state: "RELEASED"` on AgentWork rows without the per-issue mutation guard — the same unguarded shape as the operator agent-work routes. It is now named, so the fence's coverage of that shape is not an accident. The comment also stated the stale-work sweep "cannot interleave with the claim path"; a maintainer force-claim can override that guard, so the claim now says "normally", and records that the fence's pending conjunct refuses a row that became pending after the scan either way. `docs/ownership-reconciliation.md` described the cleanup as deleting every AgentWork row with no matching active lease and pointed at a stale line number. It now states the pending-release boundary and the fence, and the reference is corrected.
|
Takeover #4 — head State assessment at head
What this takeover changed and why The AI review on
Also fixed: Not changed, deliberately (each is either settled or filed): the per-row CAS (N statements) — a bulk delete returns a count, not ids, and the audit must name exactly the rows the fence consumed; the lease-set re-read gap (#1390); the operator-reassign leftover marker (#1391); the live-PostgreSQL rollback test (#1392). Remaining external verification — left explicitly pending
No merge performed by this run; the PR is left open, non-draft, with the current head pushed. |
Reason: Failing check: npm audit (failure) Outcome: no_progress Worker executions: 5 Enqueue rows: 8 Enqueues by lane:
Final failure: Agent reported failed; bounded at 5 fix attempts (PR_FIX_MAX_ATTEMPTS=5), parked instead of re-queuing (#1133). Failing run(s):
Last attempt: CI check "npm audit" failed (failure) on this PR. Error from the job log:
npm audit reportbody-parser 2.0.0 - 2.3.0 fast-uri 3.0.0 - 3.1.8 hono 4.12.4 - 4.13.10 ip-address <=10.7.3 4 vulnerabilities (3 moderate, 1 high) To address all issues, run: Full log: https://github.com/misospace/dispatch/actions/runs/40049264660/job/120738002702 Fix the root cause the log shows. Posted automatically by Dispatch on 2026-10-24T01:46:11.593Z |
…sue-1385 fix(automation): surface failed sync responses on repo detail page
joryirving
left a comment
There was a problem hiding this comment.
Secondary hard-blocker review on exact head 93934430e0bb28780e251de86e21882540b45f07: APPROVE. The claim-path cleanup now excludes rows still owed a release and fences each delete on the scanned state family, issue, owner, and pending marker. It reports/audits only rows actually consumed, and keeps delete plus audit in one transaction. The final diff against current main is limited to the intended three files. I updated the branch to include #1387's dependency audit fix; CI is rerunning on this exact head. The PostgreSQL rollback integration gap is documented and tracked separately in #1392, not a blocker for this bounded fix.
The merge-base changed after approval.
There was a problem hiding this comment.
AI Automated Review
Full PR review.
Analysis engine: MiniMax-M3.1-Flash (anthropic) — primary route · pr-reviewer-action v3.7.0
Recommendation: approve; the linked issue is addressed correctly and the exact head passes the repository’s full CI gates.
Linked Issue Fit
- The pending-release invariant is implemented consistently with
activeStateFamilies: both scan and delete requirestaleClaimReleasePending: false, preserving rows that still owe a release. - The delete fence includes id, issue, owner, executing state, and pending boundary; tests cover pending, reassigned, released, moved, and mixed-accounting rows.
- Listed claims held: 3/3; repository search also confirmed this is the only production
agentWork.deleteManypath.
Required Checks
- check for breaking API changes in updated dependencies: satisfied — the lockfile upgrades were inspected, and the exact head’s tests, build, Docker builds, and npm audit succeeded without an observed API break.
- run full test suite after upgrade: satisfied — the full test suite and all reported CI checks succeeded on the exact head.
Tool Harness Findings
- Repository inspection confirmed the canonical pending boundary, the sole delete call site, the claim-route caller, the Node 24 host pins, and the updated dependency graph.
Unknowns or Needs Verification
- Upstream changelogs were not retrieved before the tool budget was exhausted; exact-head Node 24 CI/build evidence is strong runtime compatibility evidence, but it is not a substitute for release-note review.
…gn rewrites a sweep-marked row (#1391) (#1393) * fix(automation): surface failed sync responses on repo detail page (#1385) * chore(deps): refresh lock within ranges to clear npm audit gate npm audit --omit=dev (blocking CI gate) failed on main and on this PR head: body-parser 2.3.0 was high (GHSA-27r3-jxmh-p828, DoS via trailing bytes after a compressed body), hono 4.13.7 and ip-address 10.7.2 were moderate. npm audit fix --package-lock-only --include=prod bumps body-parser to 2.4.0, hono to 4.13.13, ip-address to 10.7.4 and qs to 6.16.1; npm also re-resolved the dev tree (vite, rolldown, @oxc-project/types and @rolldown/* bindings) inside the same existing ranges. package.json and every range are unchanged; package-lock.json was already identical to main before this commit, so the failure pre-dates this PR. Local validation at this change state: npm ci --include=dev, npm run audit (0 vulnerabilities), typecheck, lint (1 pre-existing warning) and vitest all exit 0. * fix(agent-work): settle stale-claim-release marker on operator reassign (#1391) * fix(agent-work): give the reassigned row a live lease horizon (#1391) The operator reassign rewrite to CLAIMED inherited the lease expiry the preceding RELEASED write had stamped (now), so the row the new owner received carried an already-expired lease — a shape no claim path produces. The sweep's reclaim arm fires on lastHeartbeatAt < cutoff OR leaseExpiresAt < cutoff, so an expiry that is not ahead of the heartbeat lets the expiry arm fire at the same instant as the heartbeat arm instead of strictly after it. The handover is a fresh claim, so it now carries the claim path's lease horizon (AGENT_WORK_LEASE_DURATION_MS, the value startAgentWork and checkpointAgentWork stamp): the expiry stays ahead of the heartbeat and the reclaim stays governed by the heartbeat window, exactly like a row from startAgentWork. The horizon moves into an exported constant so the claim path and the handover cannot drift. Addresses the AI review findings on PR #1393 (lease expiry on the CLAIMED rewrite, and the regression test not asserting it). The staleClaimReleasePending settlement from bd3bd51 is unchanged. Co-authored-by: Courier <courier@localhost> * fix(automation): surface failed/in-progress per-repo sync responses (#1324) (#1384) * fix(automation): surface failed per-repo sync responses (#1324) * fix(automation): surface failed sync responses on repo detail page (#1385) * chore(deps): refresh lock within ranges to clear npm audit gate npm audit --omit=dev (blocking CI gate) failed on main and on this PR head: body-parser 2.3.0 was high (GHSA-27r3-jxmh-p828, DoS via trailing bytes after a compressed body), hono 4.13.7 and ip-address 10.7.2 were moderate. npm audit fix --package-lock-only --include=prod bumps body-parser to 2.4.0, hono to 4.13.13, ip-address to 10.7.4 and qs to 6.16.1; npm also re-resolved the dev tree (vite, rolldown, @oxc-project/types and @rolldown/* bindings) inside the same existing ranges. package.json and every range are unchanged; package-lock.json was already identical to main before this commit, so the failure pre-dates this PR. Local validation at this change state: npm ci --include=dev, npm run audit (0 vulnerabilities), typecheck, lint (1 pre-existing warning) and vitest all exit 0. --------- Co-authored-by: Courier <courier@localhost> Co-authored-by: Jory Irving <46251616+joryirving@users.noreply.github.com> * fix(agent-work): preserve stale-AgentWork rows still owed a claim release (#1350) (#1389) * fix(automation): surface failed sync responses on repo detail page (#1385) * chore(deps): refresh lock within ranges to clear npm audit gate npm audit --omit=dev (blocking CI gate) failed on main and on this PR head: body-parser 2.3.0 was high (GHSA-27r3-jxmh-p828, DoS via trailing bytes after a compressed body), hono 4.13.7 and ip-address 10.7.2 were moderate. npm audit fix --package-lock-only --include=prod bumps body-parser to 2.4.0, hono to 4.13.13, ip-address to 10.7.4 and qs to 6.16.1; npm also re-resolved the dev tree (vite, rolldown, @oxc-project/types and @rolldown/* bindings) inside the same existing ranges. package.json and every range are unchanged; package-lock.json was already identical to main before this commit, so the failure pre-dates this PR. Local validation at this change state: npm ci --include=dev, npm run audit (0 vulnerabilities), typecheck, lint (1 pre-existing warning) and vitest all exit 0. * fix(agent-work): preserve stale-AgentWork rows still owed a claim release (#1350) * fix(agent-work): fence the claim-path cleanup on scanned ownership (#1350) The claim-path cleanup's fenced delete re-checked the state family and the pending-release boundary, but not the ownership fields the scan matched (`issueId`, `agentName`). A row reassigned between the scan and the delete kept its id, state and marker, so the fence consumed it and the newly owned row was deleted instead of being left to its new owner. The delete now carries the scanned ownership fields as well, so the fence matches the scan on every conjunct that identifies the row. Tests cover the reassignment race, the concurrent state settle (operator release), the scan's issue scope, and the pending boundary; each fence conjunct is mutation-covered. Also correct the comment that credited the per-issue mutation guard for the fence's safety: the operator agent-work routes do not take that guard, so the fence itself is the load-bearing protection. * fix(agent-work): correct the pending-boundary rationale and pin the audit accounting (#1350) The doc comment claimed "Live writers never leave the marker on an active-shaped row (the sweep pairs it with the STALE mark), so the boundary is defence in depth on top of the state filter". That is false: the operator agent-work reassign route rewrites a row to `state: "CLAIMED"` without clearing `staleClaimReleasePending`, so a sweep-marked `STALE` row can arrive at the claim-path cleanup as an ACTIVE-SHAPED row that still owes its release — and the state filter alone would not keep it. The marker conjunct is load-bearing. The same comment also referred to "the other callers" of this function. There is exactly one production caller (`src/app/api/issues/claim/route.ts`), and it does take the per-issue mutation guard. The parties that do not take the guard are the concurrent writers this cleanup races with, which is what the fence has to survive. Neither correction changes behaviour; both make the recorded rationale match the code. The stranded-release gap the reassign route opens (the marker resurrected onto an active-shaped row is inert for the sweep: its reclaim arms require `staleClaimReleasePending: false`, its retry arm requires `state: "STALE"`) is pre-existing and filed as #1391. Adds a mixed-case regression test: with one candidate consumed and one refused by the fence, the returned count and the audit's id list must name the consumed row only. The accounting claim was previously pinned only in the all-consumed case, so a regression back to reporting the scanned ids would have passed. Local validation: `npx vitest run src/lib/agent-work.test.ts src/app/api/issues/claim/route.test.ts src/lib/stale-work.test.ts src/app/api/agent-work/route.test.ts` (4 files, 225 passed); `npm run test` (5323 passed, 0 failed, 27 skipped, 1 todo); `npm run lint` (0 errors, 1 pre-existing unrelated warning in src/app/login/page.tsx); `npm run typecheck` (clean). * fix(agent-work): correct the fence's stated racers and pin its transaction invariant (#1350) A reviewer session on the final diff raised five items against the claim-path cleanup's fence; four are fixed here and the fifth is answered on the PR. - The comments named a "self-heal" route family that does not exist and omitted the writers that actually race the cleanup: the operator agent-work routes (release / release-all / reassign) and the worker agent-work start / checkpoint / finish paths. They also claimed that a pending row always belongs to the sweep's retry arm, which is false for an ACTIVE-SHAPED pending row (the retry arm requires state STALE), so the comment now distinguishes the two shapes and says plainly that this cleanup is the repository's only `agentWork.deleteMany`. - The delete's `issueId` conjunct was not exercised by any test, so dropping it passed the suite. A new test moves a row to another issue between the scan and the delete and pins that the fence refuses it. - The "a deletion is never left without its audit row" invariant was unpinned. A new test asserts that the delete and the audit row share one transaction client, that the audit write follows the delete, and that a failing audit write surfaces instead of the cleanup reporting a successful deletion. No behaviour change. * fix(agent-work): complete the cleanup's racer list and correct its doc reference (#1350) The claim-path cleanup's safety comment enumerated the writers it races with but omitted one: the operator unclaim path calls `releaseAgentWorkByAgentAndIssue` (`src/lib/lease.ts`) AFTER its guarded block, so it writes `state: "RELEASED"` on AgentWork rows without the per-issue mutation guard — the same unguarded shape as the operator agent-work routes. It is now named, so the fence's coverage of that shape is not an accident. The comment also stated the stale-work sweep "cannot interleave with the claim path"; a maintainer force-claim can override that guard, so the claim now says "normally", and records that the fence's pending conjunct refuses a row that became pending after the scan either way. `docs/ownership-reconciliation.md` described the cleanup as deleting every AgentWork row with no matching active lease and pointed at a stale line number. It now states the pending-release boundary and the fence, and the reference is corrected. --------- Co-authored-by: Courier <courier@localhost> Co-authored-by: Jory Irving <46251616+joryirving@users.noreply.github.com> --------- Co-authored-by: Courier <courier@localhost> Co-authored-by: Jory Irving <46251616+joryirving@users.noreply.github.com>
Closes #1350
Summary
The pre-claim cleanup
findAndReleaseStaleAgentWorkForIssue(called fromsrc/app/api/issues/claim/route.ts) hard-deletedCLAIMED/IN_PROGRESSAgentWork rows whose agent has no active lease, with no awareness of the sweep's pending-release marker. A row the stale-work sweep had marked and left pending its claim release could therefore be hard-deleted by a claim attempt, dropping the release the sweep decided on.Change
Mirrors the sweep's canonical pending boundary (
activeStateFamiliesinsrc/lib/stale-work.ts, #1316/#1334) into the claim-path cleanup:src/lib/agent-work.ts): the candidatefindManynow carriesstaleClaimReleasePending: falsealongside the state filter, so a row still owed a claim release is never a cleanup candidate in any shape (a marked STALE row is already excluded by the state filter; the conjunct additionally refuses an active-shaped row carrying the marker).issueId,agentName). A row that changed shape between the scan and the delete — a writer outside the per-issue mutation guard, e.g. the operator agent-work routes (release / release-all / reassign), the operator unclaim path and the worker agent-work start / checkpoint / finish paths — is left for whoever now owns that shape.stale_agentwork_cleanupaudit notes report exactly the rows deleted, and no audit row is written when the fence consumed nothing (heightens the existing "a deletion is never left without its audit row" posture — and never claims a deletion that did not happen).Scope note: park/BLOCKED preservation and lease handling were already covered by prior work (#1316, #1334) and are unchanged. The claim route is this function's only production caller and runs the cleanup inside the per-issue
IssueMutationGuard; the concurrent writers it races with (operator agent-work routes, operator unclaim path, worker start/checkpoint/finish) do not take that guard, so the fence — not the guard — is the load-bearing protection.Addressed review feedback
The AI review on
d1c0a62requested changes with one Major finding: the delete fence omitted the scanned ownership fields, so a row reassigned between the scan and the delete could still be deleted even though its owner changed. Fixed in75351d4:issueId,agentName) in addition to the state family and the pending boundary, so it matches the scan on every conjunct that identifies the row, and a reassigned row is left to its new owner.stateconjunct of the fence had no behavioral coverage: added a test whose concurrent write settles the row toRELEASED(the operator release route's shape) and asserts the fence consumes nothing and the row survives.issueIdconjunct was unverified: the test fake now honourswhere.issueId, and a new test pins that a row belonging to another issue is never a candidate.A fresh reviewer session on the diff at
75351d4(same model family — MiniMax-M3.1-Flash — so no provider diversity was available) raised three further items, all fixed inb7d6bd9:state: "CLAIMED"without clearingstaleClaimReleasePending, so a sweep-markedSTALErow can arrive at the claim-path cleanup as an active-shaped row that still owes its release — and the state filter alone would not keep it. The marker conjunct is load-bearing, not redundant; the comment now says so.src/app/api/issues/claim/route.ts), and it does take the per-issue mutation guard; the parties that do not take the guard are the concurrent writers this cleanup races with. The comment now names them correctly.A third reviewer session on the diff at
b7d6bd9(same model family — MiniMax-M3.1-Flash — so no provider diversity was available) raised five further items. Four are fixed in496abdc; the fifth is answered on the PR thread:state: "STALE", so such a row belongs to no sweep arm at all. The comment now distinguishes the two shapes, and states the premise the "no path drops the owed release" claim rests on — this cleanup is the repository's onlyagentWork.deleteMany(the sweep only marks rows STALE).$transactionhas no rollback semantics, so moving the deletes out of the transaction (or writing the audit after the commit) kept the suite green. A new test pins the transaction's shape — the delete and the audit row go through the same transaction client, the audit write follows the delete, and a failing audit write surfaces instead of the cleanup reporting a successful deletion. The real rollback needs a live PostgreSQL and is not covered by CI's Database integration job (a fixed file list that excludes this path); it is filed as test(agent-work): prove the claim-path cleanup's delete+audit rollback against a real PostgreSQL #1392.issueIdconjunct was not exercised by any test (no scenario moved a row between issues), so dropping it passed the suite — the mutation-coverage claim below was overstated for that one conjunct. A new test moves a row to another issue between the scan and the delete and pins that the fence refuses it; removing the conjunct now fails the suite.A fourth reviewer session on the diff at
496abdc(same model family — MiniMax-M3.1-Flash — so no provider diversity was available) returned SHIP with no blocker or major finding, and re-confirmed the three load-bearing claims (pending rows are never consumed; the fence protects the delete; the accounting is honest) and that every scan and fence conjunct has behavioural coverage. It raised two documentation-accuracy items, both fixed in699d00d:releaseAgentWorkByAgentAndIssue(src/lib/lease.ts) after its guarded block, so it writesstate: "RELEASED"on AgentWork rows without the per-issue mutation guard — the same unguarded shape as the operator agent-work routes. The comment now names it, so the fence's coverage of that shape is not an accident.src/app/api/issues/claim/route.ts), so the claim now reads "normally", and the comment records that the fence's pending conjunct refuses a row that became pending after the scan either way.The same session flagged that
docs/ownership-reconciliation.mdstill described the cleanup as deleting every AgentWork row with no matching active lease (no pending boundary) and pointed at a stale line number. Fixed in699d00d: the doc now states the pending-release boundary and the fence, and the reference is corrected.Validation (local, on commit
699d00d)npx vitest run src/lib/agent-work.test.ts src/app/api/issues/claim/route.test.ts src/lib/stale-work.test.ts src/app/api/agent-work/route.test.ts— 4 files passed, 227 passednpm run test(full suite) — 206 files passed / 5 skipped; 5325 passed, 0 failed, 27 skipped, 1 todo (147s)npm run lint— 0 errors, 1 pre-existing warning insrc/app/login/page.tsx(unrelated, unchanged since Fix: eliminate silently swallowed errors via .catch(() => {}) #744)npm run typecheck— cleanstate,agentName,issueId,staleClaimReleasePending) makessrc/lib/agent-work.test.tsfail with assertion failures, not crashes — baseline 69 passed, failures 4 / 3 / 4 / 3 respectivelyLimitation: this workspace has no Postgres, so the DB-backed integration path (real generated SQL, the
AgentWorkHistorycascade, transaction commit/rollback) was not exercised locally. CI's Database integration job covers the schema and its own integration files, but not this cleanup's transaction — that gap is #1392.Remaining external verification
699d00d; no merge is performed by this run.npm auditcheck on this PR is pre-existing and unrelated to this diff (no dependency changes): it reproduces onmainand is tracked in Security Audit gate red on main: 3 prod advisories (body-parser DoS high, hono, ip-address) #1386.idalone), is deliberately out of scope for this fix, and is filed as fix(agent-work): re-read the lease set inside the cleanup transaction so a lease taken mid-flight protects the row #1390.CLAIMED, so the owed release stays owed on an active-shaped row that the sweep cannot retire (its reclaim arms requirestaleClaimReleasePending: false, its retry arm requiresstate: "STALE"). Pre-existing and outside this diff's scope; filed as fix(agent-work): settle the stale-release marker when an operator reassign rewrites a sweep-marked row #1391.