Skip to content

feat: watch linked pull requests and wake the agent (#639) - #658

Merged
Tryanks merged 11 commits into
mainfrom
feat/pr-watch
Oct 8, 2026
Merged

Tryanks merged 11 commits into
mainfrom
feat/pr-watch

Conversation

@Tryanks

@Tryanks Tryanks commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

Summary

agent: watch_pull_request ─┐                      every 2 min after the last pass
user:  Watch for changes ──┼─▶ link.watch {started_at, head_sha, failed/passed checks,
                           │       remarks_through + remark_ids, conflicting, wakes, pending_wake}
                           │
pass ─▶ 1-point fingerprint (25 PRs/request) ─▶ plan per (project, PR)
          │ rate limited → skip, no failure     │ status moved / in flight → detail (1 pt)
          │                                     │ remarks moved / incomplete / 30 min → + activity
          │                                     │ no fingerprint → snapshot gate, 10-min backstop
          ▼
   one shared read ─▶ per thread: fold (core::pull_request_watch::evaluate)
                         ├─ news → pending_wake written WITH the watermark (one meta write)
                         │         → queued server-origin turn behind any running turn
                         │         → cleared on TurnAccepted, redelivered after restart
                         └─ quiet → watermark only
ends: merged (silent) · closed / 8 failed reads / 10 comment-only wakes (last message, then ends)
      · unwatch · Stop (+ stop barrier for late agent starts) · settle · archive
  • crates/core/src/pull_request_watch.rs: the watch, the fold and the exact wake/end texts (“Tcode”, no inbox wording).
  • crates/services/src/github/pull_request_watch.rs: fingerprint (exact upstream selection), detail and activity reads; the two deviations below.
  • crates/runtime/src/app/pull_request_watch.rs: pass, commit, delivery, acknowledgement, Stop/settle/archive hooks, user command and MCP handling; queue_automatic_turn is the one owner of “queue a host/child-authored turn and start the provider in the background”, now shared with orchestrate callbacks.
  • MCP: watch_pull_request, unwatch_pull_request; the instruction block gains the watching half.
  • UI: eye on watched rows (click stops, EyeOff on hover; plain glyph on phone), Watch for changes / Stop watching in the row menu, Waiting chip tooltip “Watching pull request #N”, one toast slot per thread, wake rows rendered as a disclosure, and “What the agent sees” (tools + instructions as the host reports them in SessionStatus.pull_request_tools, whenever the thread's provider would receive the tools — MCP-capable provider and the server mounted — so it is visible before the first turn).

Deviations from upstream, each with its own test: the 30-minute fingerprint reread also drops that pull request's cached review-thread tails, one timer for both (the_half_hour_reread_pages_cached_review_thread_tails_again); a list past ten pages is incomplete and never advances the watermark (a_list_past_ten_pages_is_incomplete).

Closes #639

Evidence

  • Before: a linked pull request could only be polled by the agent itself (shell loops), and nothing woke a thread when CI or a reviewer moved.
    After: desktop light — watched row (eye) and the Waiting chip; phone light — the thread list's Waiting line and the Pull requests sheet with the eye (synthetic sample/project profile, English):
    desktop light
    phone light, list
    phone light, sheet
  • Fold from fixtures (crates/core/src/pull_request_watch.rs): required late-passed job; rerun on new head; same-second and edited remarks; viewer ≠ author; conflict once; loop resets; message caps/snippets and parse-back.
  • Runtime (crates/runtime/src/app/pull_request_watch_tests.rs, real GitHubApi against the loopback HTTP fixture, scripted provider): 20 rate-limited passes wake nobody and end nothing (that a rate limit never counts toward the 8-failure end is proven only through the internal failures map, not by an observable end); two threads share one read with separate watermarks; a quiet fingerprint-backed pass makes only the fingerprint request, moved checks read the detail alone, a check still running is read again on a quiet pass ([FINGERPRINTS, DETAIL]), the 30-minute backstop rereads activity; the snapshot-gated 10-minute backstop; a wake reaches a non-resident thread with no client connected, and a restart between watermark persistence and acceptance redelivers the same wake once (recorded with origin server); during a real running turn, Stop discards a read in flight and an agent watch_pull_request issued before Stop but completing after it is refused, the provider receives only Interrupt, and a later user start succeeds; incomplete tails never advance the watermark while checks still report; the two deviations; 8 failed reads and closed end with a delivered last message, merged ends silently; MCP refusals (subagent, settled, archived, merged; saved closed allowed); settle ends the watch and un-settle does not restart it.
  • Live (this machine's gh login, private scratch repo Tryanks/tcode-watch-scratch, Claude Code): asked the agent to watch PR Add cross-platform CI and release packaging #1 → it called watch_pull_request; the thread showed Waiting; a pushed commit failed CI and within one pass the thread woke with Update on pull request #1 (…), which Tcode is watching for you:\n- Checks failed on 45ee7bc:\n - check https://github.com/…/job/113381710738 plus the toast "Add cross-platform CI and release packaging #1: checks failed" with Open thread; a comment by github-actions (a different account, posted by a dispatched workflow; no second human account was available) woke it with "- 1 new comment: github-actions: "Scratch review note from the bot account."". "What the agent sees" listed the five tools and the instruction block from the host. The PR was closed and its branch deleted afterwards.
  • Checks (final commit): cargo fmt --all --check clean; cargo clippy --workspace --all-targets --locked -- -D warnings clean; cargo nextest run --workspace --locked --no-fail-fast: 1046 tests run: 1046 passed, 13 skipped; cargo machete: no unused dependencies.
  • Stop only interrupts a running turn: validation refuses Stop on an idle thread as on main (an idle provider never answers, which left stopping set and the Stop button dimmed); interrupt() always ends the thread's watches and discards its undelivered wakes, and sends Interrupt only while a turn or background task runs. Re-watching over an ending watch keeps its undelivered last notice; unwatch, unlink and Stop still discard.
  • Tests changed, with the CONTRIBUTING step that settled each:
    • app::tests::computer_use_registrations_survive_stop_but_are_replaced_after_provider_shutdown — failed after the Stop fix; step 1 (driver): it stopped an idle provider, which the app never does (validation refuses it), so the fake session now has a turn in flight. Contract and assertions unchanged.
    • stop_discards_a_read_in_flight_and_refuses_a_late_agent_start (new in this PR) — rewritten to drive the app's order: a real user turn runs, the agent's broker start is issued, then Stop, then the executor runs, so the start really completes after Stop. Fails with the stop barrier removed.
    • the_half_hour_reread_pages_cached_review_thread_tails_again (new in this PR) — simplified to age only the last read, the one timer left. Fails with the tail drop removed.
    • Extended: reads_follow_the_fingerprint_and_its_half_hour_backstop (in-flight rule; fails without it), messages_list_ten_items_with_snippets_and_parse_back (an unclosed <!-- keeps the rest, as upstream's /<!--[\s\S]*?-->/g; fails on the old snippet).
    • Added: pull_requests::tests::the_agent_tools_are_disclosed_before_the_first_turn (Claude Code shown, pi not, before any registration; fails on the registration-gated status).
    • pull_requests_tests.rs takes the shared HTTP fixture from test_support (Clippy rejects loading the same file as two modules). No test deleted.
  • Gaps: mobile/Web builds run in CI only; the screenshots predate the review fixes — a retake would also show the collapsed “What the agent sees” header at the panel bottom, which is now reported before the first turn; the eye's click was exercised through the runtime command, not by pointer in the live session (an unfocused-window toast covered it).

Merge Danger

Door: two-way
Blast Radius: threads
The watch persists on SessionMeta.pull_requests[].watch (optional field; older metas read without it). A wake starts a provider turn on its own, including for a thread nobody has open; Stop, settle, archive and unwatch all end it. The wire gains Command::WatchPullRequest, RuntimeToast::PullRequestWatch and SessionStatus.pull_request_tools (noted in the unreleased protocol history). The fingerprint costs 1 GraphQL point per 25 watched PRs every 2 minutes; detail/activity only when it moves.

Tryanks added 11 commits October 8, 2026 22:56
A watch on a linked pull request records what its agent was told:
failed and passed checks, a remark watermark, conflict and the
comment-only wake count. The fold reports newly failed checks, the
required gate passing, remarks by anyone but the viewer and entering
conflict, and renders the exact wake and end messages. The GitHub reads
are a batched one-point fingerprint, a detail read and an activity read
whose lists past ten pages, or review-thread replies that cannot all be
read, are reported incomplete.
A pass every two minutes after the previous one reads each watched pull
request once per project, gated by the fingerprint, with the 10- and
30-minute backstops. A wake is a server-authored message queued behind
any running turn and persisted on the watch with its watermark; it is
cleared when the provider accepts it and delivered again after a
restart. Stop, settle, archive, unwatch, merge, close, eight failed
reads and ten comment-only wakes end a watch; a rate limit never does.
The agent gets watch_pull_request and unwatch_pull_request and the
watching half of the instruction block.
The Pull requests panel shows an eye on watched rows that stops the
watch, Watch for changes and Stop watching in the row menu, and what the
agent sees: the injected tools and instructions as the host sends them.
A watched thread shows Waiting with the pull request it watches; wakes
and ends raise one notice per thread, and a wake renders in the
transcript as a disclosure, never a user bubble.
# Conflicts:
#	crates/protocol/src/lib.rs
#	crates/ui/src/shell.rs
# Conflicts:
#	crates/protocol/src/lib.rs
#	crates/runtime/src/app/settlement.rs
#	crates/runtime/src/app/snapshots.rs
#	locales/en.yml
#	locales/zh-CN.yml
@Tryanks
Tryanks marked this pull request as ready for review October 8, 2026 18:03
@Tryanks
Tryanks merged commit 4ed9683 into main Oct 8, 2026
7 checks passed
@Tryanks
Tryanks deleted the feat/pr-watch branch October 8, 2026 18:03
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.

Source control: pull request watch that wakes the agent (A3)

1 participant