feat(inbox): a review you skipped comes back when someone asks again - #43
Conversation
A skip settles a PR for good: `SETTLED_BY_YOU` keeps the poll from dragging the row back on every push, which is right — cerber does not get to overrule a decision you made. But it treated every later review request as the same one you had already answered, and a request that was withdrawn and re-added is not: your skip answered the request that was open when you made it, and cannot have answered one that came after. The row stayed invisible while someone waited. The poll now does filing's mirror image. Settling stamps `settledAt` — the decision has to be dated as it is made, because `updatedAt` moves every time you merely open a settled review and it refreshes. For rows GitHub is asking about right now, `reopenIfAskedAgain` reads the last `REVIEW_REQUESTED_EVENT` naming you (one GraphQL call, on the same 30-minute-per-artifact leash as the state checks) and, when it is newer than the stamp, puts the row back: `ready` if it has a draft, `awaiting` if it does not, with `settledAt` and cerber's own `filed` note cleared. From there the ordinary rules apply — the freshness guard re-drafts it if the head moved and leaves the draft alone if it did not. Team requests never count: `stillRequested` honours them because refusing to file work away is the safe side of that question, and this one undoes a decision of yours, where the safe side is doing nothing unless somebody named you. Rows settled before the field existed fall back to the latest moment cerber can prove the decision came after — `filed.at`, or `run.finishedAt`, since you cannot skip a draft that does not exist yet. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
There was a problem hiding this comment.
Pull request overview
This PR fixes an inbox blind spot where a PR you previously marked reviewed/skipped could remain hidden in settled even after someone later re-requested your review, by stamping the settle time (settledAt) and having the daemon poll reopen settled rows when a newer REVIEW_REQUESTED_EVENT exists.
Changes:
- Add
settledAtto the artifact schema and stamp it when a row is settled (by you or by daemon filing). - Add a daemon “reopen if asked again” check using a single GraphQL query to fetch the latest review-request timestamp for the current user.
- Add/extend tests covering reopen behavior, legacy fallback dating, and timestamp comparison correctness; update lifecycle docs to describe the new transition.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/server/index.ts | Stamps settledAt on PATCH settle actions. |
| src/server/guards.test.ts | Adds coverage asserting settle actions are dated. |
| src/server/daemon.ts | Implements reopen-on-new-request logic and legacy settle dating helpers. |
| src/server/daemon.test.ts | Adds end-to-end daemon tests for reopening settled rows. |
| src/server/chat.test.ts | Updates artifact test helper to include settledAt. |
| src/runner/review.ts | Exports SETTLED_BY_YOU; initializes artifacts with settledAt. |
| src/runner/review.test.ts | Updates artifact test helper to include settledAt. |
| src/runner/review-merge.test.ts | Verifies settledAt is preserved/cleared appropriately across run merges. |
| src/runner/chat.test.ts | Updates artifact test helper to include settledAt. |
| src/core/state.test.ts | Updates artifact test helper to include settledAt. |
| src/core/send.test.ts | Updates artifact test helper to include settledAt. |
| src/core/revise.test.ts | Updates artifact test helper to include settledAt. |
| src/core/refresh.ts | Ensures settledAt travels with user-owned status during merges. |
| src/core/refresh.test.ts | Updates artifact test helper to include settledAt. |
| src/core/gh.ts | Adds GraphQL query + helpers to fetch last review-request time for a user. |
| src/core/gh.test.ts | Adds unit tests for lastRequestOf. |
| src/core/autosend.test.ts | Updates artifact test helper to include settledAt. |
| src/core/artifact.ts | Adds settledAt to the artifact schema with documentation. |
| docs/lifecycle.md | Documents the new “asked again” transition and settledAt semantics. |
| CLAUDE.md | Updates daemon behavior description to include reopen-on-request. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
… doc fixes Review feedback on #43. `filed` outranks the status wherever the queue tags a row (`rowTag`, `requestTag`), because "reviewed" on a row cerber filed would read as a click nobody made. That stops being true the moment you click: a row you marked yourself was settled by you, whatever cerber had concluded about it earlier, so the settle now clears the note — the same rule the poll's reopen already follows. Also: say plainly what a new request can do to a settled row rather than trailing off mid-sentence, and stop the failed-read comment claiming the next poll retries. It does not — the leash is spent before the call on purpose, so gh being down cannot turn every settled row into a retry on every poll. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 20 changed files in this pull request and generated no new comments.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
src/core/gh.ts:400
fetchLastReviewRequestonly asks for the last 20REVIEW_REQUESTED_EVENTs. If a PR has >20 review-request events (e.g. many re-requests to different reviewers), this can returnnulleven though you were requested (just earlier than the last 20), which preventsreopenIfAskedAgainfrom reopening a settled row and reintroduces the “stuck in settled” behavior for busy PRs.
Consider increasing the window (e.g. to 100) so the “when was I last requested” answer remains reliable without changing the 1-call design.
timelineItems(last:20,itemTypes:[REVIEW_REQUESTED_EVENT]){
nodes{... on ReviewRequestedEvent{createdAt requestedReviewer{__typename ... on User{login}}}}
}
}
…open Review feedback on #43. The query asked for the last 20 `REVIEW_REQUESTED_EVENT`s, which is not the same as "every one of them": a PR that cycled through a dozen reviewers can push the request that named *you* off the end, and the answer would come back "nobody asked" — silently restoring the stuck-in-settled behaviour this change exists to remove. 100 is the page maximum and costs exactly what 20 did: one call. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
|
Second review pass came back with no new inline comments, but its suppressed one was right and is now fixed in 612e7d3.
The three inline threads from the first pass are answered and resolved: the filing note now clears when you settle a row yourself, the truncated lifecycle sentence is finished, and the failed-read comment no longer claims a retry the leash does not allow. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
src/core/gh.ts:405
- This docstring claims the GraphQL query “holds the lot in one call whatever the PR's history looks like”, but the query is explicitly limited to the last 100 events. Please soften the wording (or implement pagination) so the behavior is accurately described for very busy PRs.
* GraphQL rather than the REST timeline on purpose: this runs on rows the queue
* already holds, poll after poll, and the timeline of a busy PR is hundreds of
* events across several pages. Filtered to the one event type, the last page
* holds the lot in one call whatever the PR's history looks like.
*/
Review feedback on #43. The rationale claimed the query holds every request event on the PR; it holds the last 100, which is a window and not a guarantee. Say so, and say why it is not paginated: one call per extra page on every settled row on every poll, to cover a PR that does not realistically exist — and the failure mode is the conservative one, a row left settled rather than one wrongly reopened. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
|
🎉 This PR is included in version 0.25.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
#43 shipped the other half of this story: a review you skipped comes back when someone asks again. It touches the same files, and it makes one of the notes here false — "a new push or review request does not reopen it" was true when written and is not now. Only the push half still holds, so that is all the note claims. The reopen is also a new way for a row to move, and this branch exists to explain why rows move. A status change says one moved; it cannot say that somebody asked again after you settled it, which is the whole reason a settle is allowed to be undone. So `reopenIfAskedAgain` writes that down, with a test. Also `settledAt: null` in the history test's artifact factory, and both sides of the lifecycle §5 conflict kept — they were separate additions at the same anchor, not competing ones. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The full service specification — schemas, lifecycle, tool policy, the send path — precise enough to reimplement cerber from, written against the code and tests (which win on disagreement; known divergences are recorded in its Appendix B rather than silently resolved). It already describes the review history (#42) and the settled-row reopen rule (#43), so it should land after them. README and CLAUDE.md point at it, CLAUDE.md with the keep-it-true rule that stops it rotting. Co-authored-by: Claude Fable 5 <[email protected]>
The problem
You skip a PR. Later its author asks you for a review again — they withdrew the request while you were looking, pushed more work, and re-requested. The PR never comes back. It sits in settled, invisible, while someone waits on you, and the only way to notice is to go looking for it by hand.
This is not the inbox being over-eager in reverse: a skip is meant to survive a push, and that rule is right. The gap is narrower. Your skip answered the review request that was open at the moment you made it. It cannot have answered one that arrived afterwards — and nothing in cerber could tell those two apart, because every read GitHub offers about a review request says only whether one is open, never when it was made.
Found the plain way: a PR requested again yesterday afternoon that was still missing from the inbox this morning.
The fix
Settling now records when.
settledAtis stamped by both paths that settle a row — the cockpit's mark reviewed/skipped, and the poll's own filing.updatedAtcould not stand in for it: merely opening a settled review refreshes it onto the new head, which moves that field forward long after the decision it would be dating.Each poll then does filing's mirror image (
reopenIfAskedAgain). For rows GitHub is asking about right now — only ever rows already in the awaiting search — it reads the timestamp of the lastREVIEW_REQUESTED_EVENTnaming you. If that is newer than the stamp, the row goes back into the inbox:readyif it has a finished draft,awaitingif it does not, withsettledAtand cerber's ownfilednote cleared. From there the ordinary rules take over — the freshness guard re-drafts it if the head moved since the run actually read the code, and leaves the existing draft alone if it did not.This is the only thing that reopens a
reviewed/skippedrow. A push still does not.Details
timelineItems(last:20, itemTypes:[REVIEW_REQUESTED_EVENT])— one request regardless of how long the PR's history is, where the REST timeline would be several pages on a busy PR.stillRequestedhonours them because refusing to file work away is the safe side of that question; this one undoes a decision of yours, where the safe side is doing nothing unless somebody named you.filed.at, elserun.finishedAt(you cannot have skipped a draft that did not exist yet). Both are lower bounds, so the cost of being wrong is one row returning once — against every pre-existing settled row staying unreachable forever.mergeRunResultcarriessettledAtwith the status it dates, so a settle that lands mid-run keeps both halves of itself and a forced re-review clears both.Verification
pnpm typecheck,pnpm test(471 tests, 20 new),pnpm build— all green.filed; reopens a draft-less row asawaiting; asks GitHub nothing about unsettled rows; leaves the row alone when GitHub is unreachable), the pure predicates including the milliseconds-vs-seconds comparison trap, the legacy fallbacks,lastRequestOf's filtering, the PATCH stamp, and the run-merge.run.finishedAt, reads the later request, and would reopen it asready.Worth knowing
docs/lifecycle.mdgains an "Asked again" section, and the transitions table, §4's guard notes and the quick answers now say plainly that a settled row can come back and for exactly one reason.CLAUDE.md's daemon paragraph matches.No cockpit UI was added for this: the row simply reappears in the inbox, and the daemon logs
your review was requested again on … — back in the inbox.🤖 Generated with Claude Code