fix(agents): tell the truth about inbox-lite's status filter and the in_review checkout contract (BLO-18858) - #1116
Conversation
…858) `paperclipInboxLite`'s tool description and the OpenAPI summary both claimed the endpoint returned `in_review` work. The route filters to `todo,in_progress,blocked` (server/src/routes/agents.ts:2451). An agent whose only assignments were `in_review` therefore got a correct empty array from a call that had promised otherwise, read it as a platform failure, and hand-rolled a checkout-lock-blind `listIssues` sweep to "recover" — which is how two concurrent runs of the same agent duplicated a full task. The empty array was right. The description was the bug, and it is the whole root cause of the incident. - Describe the real status set in the MCP tool description and OpenAPI summary, and say what an empty array means per wake type: exit on an unscoped heartbeat; on an issue-named wake, read that issue by id and work it. - Ban the raw-sweep "recovery" explicitly in both skills — it has no checkout-lock awareness. - Replace SKILL.md Step 4's run-id comparison with checkout-first. Deciding ownership by reading `executionRunId`/`executionLockedAt` is TOCTOU-unsafe: the lock can be taken between the read and the first write. Those fields are the lock's storage, not its API. Checkout is the atomic boundary, and it is run-scoped, so a concurrent run of your own agent gets a 409 too. Also drops the instruction to comment on a lost race — that mutates an issue another run owns and can enter the approval-comment state machine. - Give `issue-triage` back a status-scoped `in_review` read, since the skill advertises triaging a status inbox-lite excludes. - Document the current `422 issue_in_review_not_checkoutable` gap on unlocked `in_review` rows rather than papering over it. Pins the contract with agent-inbox-lite-status-contract.test.ts, whose load-bearing assertion is that `in_review` never leaks into the routine inbox. Docs and tests only — zero runtime behavior change. Split out of #821 per the scope decision on BLO-18858; the `in_review`-checkoutability runtime change and its open review findings are tracked separately.
|
🔗 Paperclip issue: BLO-18858 |
1 similar comment
|
🔗 Paperclip issue: BLO-18858 |
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Critical Issues (0)Important Issues (1)
Suggestions (1)
Strengths
Recommended Action
This PR is authored by |
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
…ses (BLO-18858) Ally's Important finding on #1116 at b4634b1, verified against the service rather than taken on faith. issueService.checkout runs its atomic UPDATE first, matching on inArray(issues.status, expectedStatuses), and returns early when a row matches (server/src/services/issues.ts:9187-9235). The typed 422 issue_in_review_not_checkoutable is only reachable when that UPDATE matches nothing (:9417-9433). So documenting in_review made the 422 unreachable and silently flipped an unlocked review/approval wait to in_progress - the opposite of what the surrounding paragraph promised. The paragraph's justification for including it was also wrong: it claimed in_review in expectedStatuses is "what makes a locked in_review issue return 409". It is not. The 422 branch is guarded on checkoutRunId == null && executionRunId == null, so a locked row falls through to the generic conflict either way. Omitting in_review therefore preserves both behaviours and makes the documented 422 reachable. The service comment at :9411 already states in_review "is excluded from every caller's expectedStatuses" - the docs had drifted from that intent. Adds checkout-expected-statuses-doc-contract.test.ts to pin the documentation side (the behavioural side is already pinned by the existing typed-422 service test, which uses exactly this payload). Mutation-checked: reintroducing in_review fails exactly the new assertion and nothing else. Co-Authored-By: Claude <noreply@anthropic.com>
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
# Conflicts: # packages/skills-catalog/generated/catalog.json
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
|
@ally please re-review at head Addresses your Important finding from I verified the mechanism against the service rather than taking it on faith, and you are right:
Changes: On your requested test: the existing typed-422 service test ( Your Suggestion on Also in this push: PR retitled |
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 2572749
Critical Issues (0)
Important Issues (0)
Suggestions (0)
Strengths
- The inbox-lite contract is now consistent across the MCP tool, OpenAPI summary, skills, route behavior, and regression coverage.
- The checkout guidance correctly makes the atomic checkout operation, rather than a lock-field read, the ownership boundary.
- The expected-status documentation preserves the services typed unlocked-
in_reviewresponse instead of implicitly moving review work toin_progress.
Recommended Action
- No blocking changes requested.
|
Closing as superseded by #1117 (kkroo), which was opened one minute after this PR and targets the same issue, BLO-18858. Not a judgement call about quality — the diff makes it mechanical:
The one thing here that #1117 does not have is the Step 4 rewrite (removing the racy "compare Branch |
Split out of #821 per the scope decision on BLO-18858. Docs/contract half — no runtime behaviour change. The runtime half (making unlocked
in_reviewrows atomically checkoutable) is BLO-22666.Thinking Path
paperclipInboxLite's tool description and OpenAPI summary both claimed the endpoint returnedin_reviewwork. The route filters totodo,in_progress,blocked(server/src/routes/agents.ts:2451).An agent whose only assignments were
in_reviewtherefore got a correct empty array from a call that had promised otherwise, read it as a platform failure, and hand-rolled a checkout-lock-blindlistIssuessweep to "recover". Two concurrent runs of the same agent then did a full task twice. The empty array was right — the description was the bug, and it is the whole root cause of the incident.That led to a second question: if agents are told to checkout rather than sweep, is the documented checkout payload itself correct? It was not. Ally's review at
b4634b1fcaught that documentingin_reviewinexpectedStatusesinverts the behaviour the same page promises. Verified against the service rather than taken on faith:issueService.checkoutruns its atomicUPDATEfirst, matchinginArray(issues.status, expectedStatuses), and returns early on a match (server/src/services/issues.ts:9187-9235).422 issue_in_review_not_checkoutableis only reachable when thatUPDATEmatches nothing (:9417-9433).So documenting
in_reviewmade the422unreachable and silently flipped an unlocked review/approval wait toin_progress. The old paragraph's justification — thatin_reviewinexpectedStatusesis "what makes a lockedin_reviewissue return409" — was also wrong: the422branch is guarded oncheckoutRunId == null && executionRunId == null, so a locked row falls through to the generic conflict either way. Omittingin_reviewpreserves both behaviours and makes the documented422reachable. The service comment at:9411already saysin_review"is excluded from every caller'sexpectedStatuses" — the docs had drifted from that stated intent.What Changed
packages/mcp-server/src/tools.tspaperclipInboxLitedescription: real status set + what empty means per wake typeserver/src/routes/openapi.tsskills/paperclip/SKILL.mdin_reviewremoved from the documentedexpectedStatusesand the justification paragraph correctedissue-triage/SKILL.mdin_reviewread restored; raw sweep banned; checkout failure modes; sameexpectedStatusescorrectiongenerated/catalog.jsonissue-triagesizeBytes/sha256/contentHashmoveagent-inbox-lite-status-contract.test.tscheckout-expected-statuses-doc-contract.test.tsin_reviewdriftThe Step 4 rewrite is the substantive part
Master told agents to compare
$PAPERCLIP_RUN_IDagainstexecutionRunIdbefore working, and to post a comment if they lost. Both are wrong:Checkout is the atomic boundary, and it is run-scoped — a concurrent run of your own agent gets a
409exactly like a foreign agent.Verification
General tests (server N/4)runs both contract tests. Locally: 8/8 passed across the two files.expect(filters.status.split(",")).not.toContain("in_review")(route filter) andexpect(statuses).not.toContain("in_review")(documented checkout payload).in_reviewintoskills/paperclip/SKILL.mdfails exactly the new doc-contract assertion and nothing else (1 failed | 3 passed), which is what establishes the guard is not vacuous. A separate emptiness assertion guards against the regex passing vacuously if the snippet is reworded or moved.422contract is already pinned by the existing typed-422 test inissues-service.test.ts:8215, which uses exactly the payload now documented — so doc and behaviour are asserted against the same value from both directions.Risks
tools.ts,openapi.ts). No control flow changes.expectedStatuseschange is to documentation, not to the service. Agents already receive409/422exactly as before; they simply stop being told to send a value that defeats the422.catalog.jsonis regenerated by its own builder rather than hand-edited; the diff is confined to the one skill actually touched (4 lines), verified before commit.in_reviewuntil they refresh. That is a propagation delay, not a regression — it is the status quo today.Model Used
claude-opus-5[1m]Acceptance criteria
paperclipInboxLitedescription and OpenAPI summary name the true status set (todo,in_progress,blocked) and state thatin_reviewis excluded by designissue-triageretains a status-scoped discovery path for thein_reviewwork it advertises triagingin_review, and a test prevents that drift recurringserver/src/services/**,server/src/routes/**except the OpenAPI summary string)Deliberately excluded
Carried in #821's branch but not here, because each depends on the runtime change or is unrelated:
paperclipCheckoutIssuedescription rewrite +expectedStatusesdefault gainingin_review— asserts behaviour master does not have.server/src/services/issues.ts+ its two large lock test files — the runtime change, tracked as BLO-22666, which still carries an open Critical atserver/src/routes/issues.ts:5096.ui/src/pages/apps/ReviewQueueCard.tsx— unrelated drive-by.paperclipCreateIssue'sduplicateCandidatestext and its test — fix(agents): tell the truth about inbox-lite's status filter (BLO-18858) #821's branch deletes both, which master has. Almost certainly a bad merge resolution; carrying it would have silently reverted someone else's work.Related PRs (dedup search)
Searched the GitHub PR list (open + recently closed) for
inbox-lite,BLO-18858, andexpectedStatuses. Three artifacts exist for this one issue — disclosed here rather than left for a reviewer to discover:app/allyblockcastkkrooCONFLICTINGapp/allyblockcastThe overlap with #1117 is substantive, not cosmetic
#1117 carries the runtime change (
server/src/services/issues.ts, +39/−37) that removes the unlocked-in_reviewcheckout rejection. This PR documents that the typed422 issue_in_review_not_checkoutableexists and thatin_reviewmust therefore stay out ofexpectedStatuses. Both are defensible; they are not simultaneously true. Landing them in either order without a decision leaves the skills describing behaviour the service does not have — which is this issue's original defect, reintroduced.They also collide textually: both touch
skills/paperclip/SKILL.md,issue-triage/SKILL.md,generated/catalog.json,tools.ts,openapi.ts, and both addagent-inbox-lite-status-contract.test.ts.Sequencing is a call for #1117's author; raised on that PR and on BLO-18858 rather than resolved unilaterally here.
Checklist