Skip to content

feat(inbox): a review you skipped comes back when someone asks again - #43

Merged
jtomaszewski merged 4 commits into
mainfrom
jtomaszewski/pr-1024-missing-from-inbox
Aug 25, 2026
Merged

feat(inbox): a review you skipped comes back when someone asks again#43
jtomaszewski merged 4 commits into
mainfrom
jtomaszewski/pr-1024-missing-from-inbox

Conversation

@jtomaszewski

Copy link
Copy Markdown
Contributor

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. settledAt is stamped by both paths that settle a row — the cockpit's mark reviewed/skipped, and the poll's own filing. updatedAt could 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 last REVIEW_REQUESTED_EVENT naming you. If that is newer than the stamp, the row goes back into the inbox: ready if it has a finished draft, awaiting if it does not, with settledAt and cerber's own filed note 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/skipped row. A push still does not.

Details

  • One GraphQL call, 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.
  • The same leash as the existing state checks: one call per artifact per 30 minutes, capped per poll. A row that is not settled costs nothing at all — there is a test asserting GitHub is never asked about one.
  • Team requests never count. stillRequested honours 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.
  • A read that fails changes nothing — no evidence either way, next poll retries.
  • Rows settled before the field existed are dated by the latest moment cerber can prove the decision came after: filed.at, else run.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.
  • mergeRunResult carries settledAt with 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.
  • New tests cover the poll end to end (reopens on a newer request; leaves the skip standing on an older one; never touches a sent row; clears filed; reopens a draft-less row as awaiting; 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.
  • Ran the new predicates against a real artifact on this machine — a skipped review whose author re-requested afterwards — with the real GitHub read: it resolves the legacy date from run.finishedAt, reads the later request, and would reopen it as ready.

Worth knowing

docs/lifecycle.md gains 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

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]>

Copilot AI 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.

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 settledAt to 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.

Comment thread src/server/index.ts
Comment thread docs/lifecycle.md Outdated
Comment thread src/server/daemon.ts
… 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]>

Copilot AI 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.

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

  • fetchLastReviewRequest only asks for the last 20 REVIEW_REQUESTED_EVENTs. If a PR has >20 review-request events (e.g. many re-requests to different reviewers), this can return null even though you were requested (just earlier than the last 20), which prevents reopenIfAskedAgain from 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]>
@jtomaszewski

jtomaszewski commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Second review pass came back with no new inline comments, but its suppressed one was right and is now fixed in 612e7d3.

fetchLastReviewRequest asked for the last 20 request events. That is not the same as all of them: a PR that cycled through a dozen reviewers can push the request that named you off the end, and the answer comes back null — no reopen, row stays in settled, which is precisely the bug this PR is about. Widened to last:100 (the page maximum), which costs exactly what 20 did — one call. Verified against this PR's own timeline.

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.

Copilot AI 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.

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.
 */

Comment thread src/core/gh.ts Outdated
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]>

Copilot AI 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.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated no new comments.

@jtomaszewski
jtomaszewski merged commit 19847f0 into main Aug 25, 2026
3 checks passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 0.25.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

jtomaszewski added a commit that referenced this pull request Aug 25, 2026
#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]>
jtomaszewski added a commit that referenced this pull request Aug 25, 2026
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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants