fix(notifications): acknowledge each resolved thread instead of the whole repository - #1121
Conversation
tend-agent
left a comment
There was a problem hiding this comment.
The per-thread acknowledgement is the right shape — it matches the ack to the decision and deletes the arithmetic rather than refining it. Two things about the reasoning now attached to it, plus one stale reference.
The API-semantics claims contradict GitHub's docs. List notifications documents before as "Only show notifications updated before the given time", and both mark-as-read endpoints document last_read_at as "Describes the last point that notifications were checked. Anything updated since this time will not be marked as read." Both bound on updated_at, not on when a thread became unread — the opposite of what Step 1 and Step 4 now assert.
The PR body's own evidence points the same way. 494 threads returned under before with none past the cutoff is what an updated_at filter produces; a filter keyed on when a thread became unread would have let bumped threads through, which is the phenomenon the table describes. And the last_read_at probe (PUT at 17:00:00Z, a thread at 17:55:10Z staying unread) is consistent with either reading, so it doesn't separate them.
That inverts the stated failure mode: under the documented semantics, a deferred thread bumped to a later updated_at is more protected by a cutoff below it, not less. The reported run is then explained by snapshot staleness alone — both threads' updated_at were below the cutoff when the snapshot was taken and moved afterwards, which is exactly what the new Step 1 paragraph exists to warn about.
None of this changes the verdict on the change itself: acknowledging exactly the threads that reached an outcome beats any timestamp cutoff under either reading. But a skill is read as ground truth by every later session, and this one now carries a mechanism that a reader who checks the docs will find wrong — which invites restoring the repo-wide call on the grounds that the stated reason for banning it doesn't hold. The inline suggestions restate both rules on ground that does. The test docstring and the PR description carry the same claim and want the same correction.
Stale cutoff reference. In Step 3, the bullet on a subject with no readable target still reads "leaving it unresolved would pin the Step 4 cutoff permanently" — the cutoff this PR deletes. The rule survives; its reason is now that the thread stays unread and is re-examined by every later poll instead.
The skill claimed `before` and `last_read_at` bound on when a thread became unread rather than on `updated_at`. GitHub documents the opposite for both, and the observation behind the claim does not settle which is right — so a later reader who checks the docs finds the stated reason wrong and has grounds to restore the repository-wide call. State each rule on ground that holds under either reading: the run's scope is the snapshot's membership, and the repository-wide ack is banned because it marks by timestamp rather than by outcome.
|
Pushed
One correction to the review's reasoning, in the second inline thread: the reported run is not explained by snapshot staleness. The check step ran at
|
tend-agent
left a comment
There was a problem hiding this comment.
The three inline threads and the stale Step 3 reference are all addressed, and the Step 1 rewrite is the right call: it records the observation and stops short of a mechanism the evidence doesn't settle, so nothing in the skill now depends on which timestamp either endpoint bounds on.
One new claim came in with the increment, in the pre-check comment — inlined verbatim into every adopter's tend-notifications.yaml, so it's the same class of statement the rest of the PR is cleaning up. Inline. Applying the suggestion needs a uv run pytest --regtest-reset for the four _regtest_outputs copies.
The pre-check comment read "the cutoff bounds the snapshot below, which is passed to the agent". Read with "which" attaching to the snapshot it is false: generate_notifications appends only steps.check.outputs.cutoff and steps.check.outputs.conflict_count to the prompt, and $NOTIFS is consumed for count and discarded. The agent re-runs the query itself in the skill's Step 1, so the set it can acknowledge is its own snapshot's membership. Say that the cutoff is the value passed, that the agent takes its own snapshot with it, and that the run therefore acknowledges only threads it examined. Regtest outputs regenerated for the four notifications copies.
tend-notificationsStep 4 acknowledged the whole repository withPUT /repos/{owner}/{repo}/notifications, bounding the call one second below a deferred thread'supdated_at. That makes the deferral a timestamp comparison GitHub controls rather than a decision the run makes: a repository-wide ack marks by timestamp, not by outcome, so it acts on threads the run never examined — and REST has no "mark unread" to walk an overshoot back. The thread it can drop is exactly the one the cutoff exists to spare: a subject whose dedicated workflow is still in flight, and which that workflow has since bumped.Step 4 now PATCHes each thread that reached an outcome, one call per thread, same-repository or not, and leaves deferred and unresolved threads untouched. That deletes the cutoff arithmetic and the
Never acknowledge a same-repository thread individuallyrule, whose real invariant — never acknowledge before an outcome — is now stated directly. Nothing in the rewritten skill rests on a claim about which timestamp either endpoint bounds on.Evidence
The report (#1120) attributed this to two GitHub bugs. The divergences it saw are real but narrower than "the endpoints are broken", and one of its suggested changes would make things worse.
What the reported run shows. The snapshot was taken by the check step at
17:50:01Zwithbefore=2026-08-31T17:40:00Z, and returned six threads — including two whoseupdated_athad already reached their final values nine minutes earlier, so the divergence is not the snapshot being read back later:prql-botreviewupdated_atBoth became unread before the cutoff and were bumped past it by the bot's own reviews, which do not re-notify. List notifications documents
beforeas "Only show notifications updated before the given time", and the repository-wide PUT documentslast_read_atas "Anything updated since this time will not be marked as read" — under those semantics neither thread should have been returned by the snapshot, and neither should have been marked read by the PUT. Both were.The boundary behaves as documented when nothing was bumped. Neither probe reproduces the divergence in isolation:
PUT /repos/max-sixty/tend/notificationswithlast_read_at=2026-08-31T17:00:00Zreturned205 Reset Content, and the one thread updated after it stayed unread across a minute of rechecks:Neither probe covers the case that diverged — a thread bumped after it became unread and before the call — so the underlying semantics stay unsettled, and this PR does not try to settle them. Acknowledging exactly the threads that reached an outcome needs no timestamp reasoning at all, which is why it is the right shape under either reading.
Why the report's other suggestion is not taken. Filtering the snapshot client-side on
updated_at < $CUTOFFwould have dropped both threads above, which the run did own and did handle. Step 1 now records the observation — a returned thread'supdated_atcan be past the cutoff — and says to take the snapshot's membership as the run's scope rather than re-deriving it.Closes #1120 — automated triage