Repository navigation
feat(github)!: 3.0.0, rename gh-pr-events to agentic-utils-github and catch up after sleep - #12
Merged
Merged
Conversation
brtkwr
force-pushed
the
feat/gh-pr-events-catchup
branch
3 times, most recently
from
October 14, 2026 12:58
8c53642 to
c1ae5a3
Compare
… catch up after sleep Renames the plugin to agentic-utils-github (MCP server pr-events, folder plugins/github, state dir ~/.claude/state/pr-events, no migration). The GH_PR_EVENTS_* env vars keep their names. gh webhook forward exits whenever its connection drops (it never retries a wrapped close 1006), and GitHub deletes its webhook and delivery log with it, so a sleep or network change used to drop every subscription in the repo. The daemon now restarts a forwarder that exits (backing off, giving up after 10 failures in a row per repo), restarts all forwarders when a 5s timer fires over 30s late, and opens a per-PR gap at the last moment each PR had everything; live events don't close it, and nor does a catch-up that finishes after the PR fell behind again. Once a forwarder is back it catches each PR up from the PR API from the gap's start (issue events, comments, review comments, reviews, head moves, CI including runs started in the gap), replayed through the live path and marked as recovered, and tells the PR's sessions if that keeps failing. Live events are never deduped against each other; recovered ones are skipped by id, or for PR actions by kind and time. stop() starts no forwarder once it begins. Every PR event now starts with the PR's name (owner/repo#n), since Claude Code cuts notification lines at the terminal width and renders no links. A PR closed without merging stays subscribed for 24h in case it's reopened. BREAKING CHANGE: the plugin, MCP server, tool names, channel flag and state dir are renamed; reinstall and subscribe again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E9K1xesQ7PxPwiV32EYsKm
brtkwr
force-pushed
the
feat/gh-pr-events-catchup
branch
from
October 14, 2026 13:28
c1ae5a3 to
cb5000b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
3.0.0: the plugin is renamed from
agentic-utils-gh-pr-eventstoagentic-utils-github, its MCP server fromgh-pr-eventstopr-events, and its folder toplugins/github. It now shows asplugin:agentic-utils-github:pr-events, the tools aremcp__plugin_agentic-utils-github_pr-events__subscribe_pr/__unsubscribe_pr, and the channel flag is--channels plugin:agentic-utils-github@agentic-utils. The default state dir moves to~/.claude/state/pr-events, with no migration: sessions subscribe again once. The daemon check now looks foragentic-utils-github/orplugins/github/in the daemon's command line. TheGH_PR_EVENTS_*environment variables keep their names, so existing settings such asGH_PR_EVENTS_BEAT_DIRkeep working. Uninstall the old plugin: its sessions keep their own daemon, which can hold a repo's forward.After a laptop sleep, or any network change, the plugin now picks up where it left off. It restarts its forwarders and recovers what each subscribed PR missed from GitHub's API. Before this, every subscription in the repo was silently dropped.
What I found first
A suspend test on a private scratch repo:
gh webhook forwardwith a local receiver, SIGSTOPped for 60-100s, with a PR comment posted mid-stop.websocket: close 1006 (abnormal closure)and exits 1. gh-webhook v0.2.0 has a retry for 1006, but it checks for the raw close error, the error arrives wrapped, and so the retry never runs.GET hooks/{id}/deliveriesreturns 404.So replaying from the delivery log can't work after a real sleep. This PR catches up from the PR API instead.
What changed
Forwarder exits are restarted: retries go 1s, then doubling to 60s. Failures are counted per repo, whoever starts the next attempt, and after 10 in a row (about 6 min) the repo's subscriptions are dropped and their sessions told.
Sleep detection: a 5s timer firing more than 30s late restarts every forwarder.
Gaps: a forwarder exit, a sleep, or a daemon restart opens a gap for each PR in the repo at the last moment it had everything. Live events that arrive meanwhile, such as during the restarted forwarder's start, don't close it. The gap is persisted, so a daemon restart keeps it.
Catch-up runs from the start of the gap once the forwarder is back. It covers:
Each item goes through the live path, prefixed
(recovered after a gap), followed by a mergeability recheck. Eachgh apicall times out after 60s. After 5 failed attempts the PR's sessions get achannel_errornaming the PR, the time it couldn't recover from, and why, and the gap stays open. A catch-up that finishes after the PR fell behind again (a second sleep) leaves the new gap open: a per-PR generation counter is bumped each time it falls behind. At startup the daemon checksgh --versionand reports a gh older than 2.48 (which lacks--slurp).No duplicates, no drops: each PR keeps its recently delivered events in
sessions.json(key#seen, which a 2.0 daemon skips). They are evicted by age, and saved once per catch-up.Every PR event starts with the PR's name (
owner/repo#n: …). Claude Code shows a notification as one line cut at the terminal width, with no markdown or links rendered (checked live: a markdown link shows raw), so the URL was being cut off. The full URL still follows, for Claude.A PR closed without merging stays subscribed for 24h (
GH_PR_EVENTS_REOPEN_HOURS) in case it's reopened. Before, closing unsubscribed it, so a reopen went unseen. A reopen clears the window, and so does a merge, which unsubscribes at once as before. If the PR isn't reopened in time, its sessions are told and it's unsubscribed. The window is persisted, so it survives a daemon restart.Clean stop:
stop()sets a flag first and clears pending restarts and retries, so no forwarder starts after it begins.README Limits and How it works, plus the invariant in
server.ts, updated. Bumped to 2.1.0 in both manifests.Tests
Ten new daemon tests using the fake gh-webhook and a fake gh that serves API pages from files, including
Linkpaging and a mode that rejects--slurp. Nothing contacts GitHub.--slurp;Test harness:
daemon()now waits for a "ready" line the daemon logs once it has loadedsessions.jsonand installed its signal handlers. It used to wait only fordaemon.json, which is written at election, before either. That raced two older tests on macOS, where a SIGTERM with no handler kills even a stopped process.Mutation checks: with live dedupe, with live events closing the gap, without the stop flag, or without the generation check, the matching test fails.
Limits
The PR API doesn't cover everything a webhook does: dismissed reviews, resolved threads, and the reasons for auto-merge being switched off or a PR leaving the merge queue are not recovered. Neither are check suites from other apps on a head pushed and replaced within the gap. If a dying forwarder hands over a PR action after a catch-up recovered it, it arrives twice (live PR actions are never dropped as repeats). Catching up needs gh 2.48+.
🤖 Generated with Claude Code
https://claude.ai/code/session_01E9K1xesQ7PxPwiV32EYsKm