Keep Gitea threads watched across restarts and interruptions #68

Merged
dongar merged 1 commits from pr-open-handling into main 2026-09-29 17:37:25 +00:00
Collaborator

PR 2 of the state-machine review. Closes #58, closes #59.

Comment cursors. Each watched thread records the ID of the last comment taken up: board.last_comment for the plan issue (the field existed but was never updated) and pr.last_comment for the PR. The cursor is saved before the comment is acted on, so each comment is handled at most once, even across a crash. CommentPoller.resume_after(id) skips everything at or below it, so re-attaching needs no request, and an outage at boot can't cause a replay.

Changes (control.py, poller.py)

  • Boot re-attaches the watches (#59): the plan issue at an open gate, the PR in PR_OPEN, each from its cursor. Comments posted while the agent was down are delivered, as agreed. Approvals stay bound to a revision.
  • A restart in PR_OPEN resumes with "restarted with PR #N open", not the restart question (#58). A restart question already saved for PR_OPEN returns to it on either reply, instead of re-planning the milestone.
  • Review failures stay with the PR: a stopped or failed review response gets its own pending kind (review_stopped), and the reply returns to PR_OPEN. Previously it went back to milestone sign-off, where start milestone would open a second PR.
  • Comments ignored because of the state (e.g. a review comment while the agent waits on a question) don't move the cursor, so they're delivered once it's back in PR_OPEN. Previously they were lost.
  • A new plan revision still skips earlier comments, so an approve posted before rev-N can't approve it. Data saved before this change has no cursor and gets the same treatment on restart, so an old approval can't approve the current revision.

Tests (test_pr_open_handling.py, real CommentPoller against a fake Gitea, polled explicitly):

  • an approval posted during downtime is applied after the restart;
  • a handled approval isn't replayed;
  • a new revision ignores an earlier approval;
  • legacy data skips old comments;
  • a restart in PR_OPEN resumes and handles a review comment posted during downtime;
  • a failed review response returns to the PR, and a comment posted while waiting is delivered.

5 of the 6 fail on main; the revision test guards behaviour main already had. 455 unit tests and the integration suite (19 passed, 7 skipped) pass; lint and format clean.

Noticed, not changed here (filed separately): Gitea's inline review comments aren't seen by the poller, which reads issue comments only. Also, a Gitea approval that leads to a new plan post re-attaches the plan watch from inside that watch's own callback. The old poller's stop() then waits out its join timeout (~25 s) before continuing, which delays but doesn't break things.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE

PR 2 of the state-machine review. Closes #58, closes #59. **Comment cursors.** Each watched thread records the ID of the last comment taken up: `board.last_comment` for the plan issue (the field existed but was never updated) and `pr.last_comment` for the PR. The cursor is saved before the comment is acted on, so each comment is handled at most once, even across a crash. `CommentPoller.resume_after(id)` skips everything at or below it, so re-attaching needs no request, and an outage at boot can't cause a replay. **Changes** (`control.py`, `poller.py`) - **Boot re-attaches the watches (#59):** the plan issue at an open gate, the PR in `PR_OPEN`, each from its cursor. Comments posted while the agent was down are delivered, as agreed. Approvals stay bound to a revision. - **A restart in `PR_OPEN` resumes** with "restarted with PR #N open", not the restart question (#58). A restart question already saved for `PR_OPEN` returns to it on either reply, instead of re-planning the milestone. - **Review failures stay with the PR:** a stopped or failed review response gets its own pending kind (`review_stopped`), and the reply returns to `PR_OPEN`. Previously it went back to milestone sign-off, where `start milestone` would open a second PR. - **Comments ignored because of the state** (e.g. a review comment while the agent waits on a question) don't move the cursor, so they're delivered once it's back in `PR_OPEN`. Previously they were lost. - **A new plan revision still skips earlier comments**, so an `approve` posted before rev-N can't approve it. **Data saved before this change** has no cursor and gets the same treatment on restart, so an old approval can't approve the current revision. **Tests** (`test_pr_open_handling.py`, real `CommentPoller` against a fake Gitea, polled explicitly): - an approval posted during downtime is applied after the restart; - a handled approval isn't replayed; - a new revision ignores an earlier approval; - legacy data skips old comments; - a restart in `PR_OPEN` resumes and handles a review comment posted during downtime; - a failed review response returns to the PR, and a comment posted while waiting is delivered. 5 of the 6 fail on `main`; the revision test guards behaviour `main` already had. 455 unit tests and the integration suite (19 passed, 7 skipped) pass; lint and format clean. **Noticed, not changed here** (filed separately): Gitea's inline review comments aren't seen by the poller, which reads issue comments only. Also, a Gitea approval that leads to a new plan post re-attaches the plan watch from inside that watch's own callback. The old poller's `stop()` then waits out its join timeout (~25 s) before continuing, which delays but doesn't break things. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
dev-agent added 1 commit 2026-09-29 17:33:42 +00:00
Keep Gitea threads watched across restarts and interruptions
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m20s
3b6aae92c5
- Record a comment cursor per thread (board.last_comment, the field that
  already existed unused; pr.last_comment), advanced durably when a
  comment is taken up. CommentPoller.resume_after() skips comments at or
  below it, so re-attaching needs no request and cannot replay.
- Boot re-attaches the plan watch at an open gate and the review watch in
  PR_OPEN from their cursors, catching up on comments posted while the
  agent was down (#59). Data saved without a cursor skips what is there,
  so an old approval cannot approve the current revision.
- A restart in PR_OPEN resumes with a notice instead of the restart
  question (#58). A stored restart question for PR_OPEN returns to it on
  either reply instead of re-planning the milestone.
- A stopped or failed review response gets its own pending kind; the
  reply returns to PR_OPEN and resumes the watch from the cursor, so
  comments that arrived meanwhile are delivered. It used to return to
  milestone sign-off, where starting the milestone would open a second PR.
- Comments ignored because of the state leave the cursor alone, so they
  are delivered once the agent is back where they apply.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
dongar approved these changes 2026-09-29 17:37:22 +00:00
dongar merged commit 4e8ebea26a into main 2026-09-29 17:37:25 +00:00
dongar deleted branch pr-open-handling 2026-09-29 17:37:25 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dev/nisse#68