Skip to content

fix(notifications): acknowledge each resolved thread instead of the whole repository - #1121

Merged
max-sixty merged 3 commits into
mainfrom
fix/issue-1120
Sep 1, 2026
Merged

fix(notifications): acknowledge each resolved thread instead of the whole repository#1121
max-sixty merged 3 commits into
mainfrom
fix/issue-1120

Conversation

@tend-agent

@tend-agent tend-agent commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

tend-notifications Step 4 acknowledged the whole repository with PUT /repos/{owner}/{repo}/notifications, bounding the call one second below a deferred thread's updated_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 individually rule, 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:01Z with before=2026-08-31T17:40:00Z, and returned six threads — including two whose updated_at had already reached their final values nine minutes earlier, so the divergence is not the snapshot being read back later:

thread PR unread since prql-bot review thread updated_at
25394372529 PRQL/prql#6262 17:21:47 17:40:21 17:40:43
25394267616 PRQL/prql#6259 17:15:10 17:44:11 17:44:34

Both became unread before the cutoff and were bumped past it by the bot's own reviews, which do not re-notify. List notifications documents before as "Only show notifications updated before the given time", and the repository-wide PUT documents last_read_at as "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:

$ gh api "notifications?all=true&before=2026-08-31T06:00:00Z&per_page=5" --paginate --slurp \
    | jq '[add // [] | .[] | .updated_at] | sort | {n: length, newest: .[-1]}'
{ "n": 494, "newest": "2026-08-31T05:53:40Z" }

PUT /repos/max-sixty/tend/notifications with last_read_at=2026-08-31T17:00:00Z returned 205 Reset Content, and the one thread updated after it stayed unread across a minute of rechecks:

{"id":"25394889617","last_read_at":null,"unread":true,"updated_at":"2026-08-31T17:55:10Z"}

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 < $CUTOFF would have dropped both threads above, which the run did own and did handle. Step 1 now records the observation — a returned thread's updated_at can 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

@tend-agent tend-agent left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread plugins/tend-ci-runner/skills/notifications/SKILL.md Outdated
Comment thread plugins/tend-ci-runner/skills/notifications/SKILL.md Outdated
Comment thread plugins/tend-ci-runner/skills/notifications/SKILL.md Outdated
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.
@tend-agent

Copy link
Copy Markdown
Collaborator Author

Pushed 0cc0f79, which takes the three inline suggestions (one with a wording change, noted in the thread) and the stale reference.

  • Step 3's stale cutoff reference — fixed. The bullet on a subject with no readable target now reads "leaving it unresolved would hand it to every later poll to re-examine", which is the reason that survives deleting the cutoff.
  • Test docstring — rewritten onto the same ground as the skill: repository-wide marks by timestamp rather than by outcome, so it acts on threads the run never examined. No claim about which timestamp either endpoint bounds on.
  • PR description — rewritten. The evidence section now reports what the run shows and what it doesn't, rather than asserting a mechanism.
  • notifications-check.sh — the generated comment carried the same shape of claim ("Newer activity never enters the snapshot"); it now says the cutoff bounds the snapshot and the run can only acknowledge what the snapshot returned. Regtest outputs regenerated.

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 17:50:01Z with before=17:40:00Z, and both threads had reached their final updated_at nine minutes earlier — so before returned threads past the bound as of the request. That doesn't vindicate the mechanism I originally wrote either; it just means neither reading is settled by the evidence available. Which is the argument for the change the review asked for: the rules now hold either way.

uv run pytest is 880 passed, pre-commit run --all-files clean.

@tend-agent tend-agent left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread generator/src/tend/templates/notifications-check.sh Outdated
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.
@max-sixty
max-sixty merged commit 5883146 into main Sep 1, 2026
10 checks passed
@max-sixty
max-sixty deleted the fix/issue-1120 branch September 1, 2026 04:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tend-notifications: repo-wide ack marks post-cutoff threads read, so deferred items are dropped instead of carried

2 participants