Skip to content

fix(server): a watched PR's merge wakes the agent before the thread settles - #75

Draft
maschwenk wants to merge 1 commit into
mainfrom
fix/pr-watch-merge-wake
Draft

maschwenk wants to merge 1 commit into
mainfrom
fix/pr-watch-merge-wake

Conversation

@maschwenk

Copy link
Copy Markdown
Owner

an agent of mine queued a 2-PR stack on GitHub's merge queue (private work repo), called watch_pull_request on both, and ended its turn with "once the stack merges, i'll check that the deployed service includes both commits". the queue merged both. 21 seconds later T3 settled the thread and detached the agent's session. it never got another turn, and i only noticed ~4 hours later when i asked why the thread went quiet

what happened, from the thread's event log

(quotes trimmed to drop internal names)

time (UTC) event
14:07:58 agent watches both PRs, already in the merge queue
14:08:09 agent ends its turn: "once the stack merges, i'll check that the deployed service includes both commits"
14:08:50 two "checks passed" wakes for PR-branch checks that were already green. the second turn ends "no change … i'll pick up when the queue merges them"
14:13:37 GitHub merges both PRs
14:13:55 PR sync stores both snapshots as merged. both links still have a live watch
14:13:58 thread.settled from server:auto-settle, source auto_pull_request_merged. settling also ends both watches and detaches the session ("Thread settled.")

three things lined up:

  1. a merge ended the watch silently. PullRequestWatchReactor wakes the agent when a watched PR closes, but on merge it just did record(target, null), on both paths (its own host read, and the merged snapshot the sync reactor usually stores first). the watch result tells agents "T3 Code wakes you on the next change", so assuming a merge counts is pretty reasonable
  2. a merged PR's watch stopped holding auto-settle. a live watch counts as awaited background work, which blocks auto-settle. upstream PR 26395 exempted watches whose snapshot says merged, because of (1): the reactor would only end them silently on its next 2 min sweep anyway. so the moment the sync stored merged nothing held the thread
  3. the reply check (PR 48) read the wrong reply. it only reads the latest run's last reply, which was the vague second "checks passed" turn. that reads like "waiting on the merge queue, nothing after". the reply that actually promised post-merge work was one run earlier. i can't show the haiku verdict, the server logs from then have rotated, but the check is on by default and the thread settled

fixing (3) alone wouldnt help. a held thread still never gets a turn because nothing wakes it

what changed

  • a merge wakes the agent once, like a close already does, then the watch ends. both paths. text: "Pull request #N (url) merged, so T3 Code stopped watching it. Do anything you planned for after the merge, such as checking the deploy, then end your turn." summary #N: merged, stopped watching
  • a merged PR's watch holds auto-settle again until that wake lands. drops the merged exemption in pullRequestWatchTasks. without this the settle wins the race every time: it runs on the link update (3s after the sync here) while the reactor sweeps every 2 min, and the orchestrator refuses a wake on a settled thread ("The pull request watch ended or its thread settled while it was read."). once the wake commits, its run holds the thread. when that run ends the watch is gone and the normal settle path takes over, including the reply check, which now reads the post-merge reply
  • a refused wake still ends the watch. with the hold above, a watch whose wake can never be delivered (e.g. the thread's provider instance was removed, the case AppLoidx raised on upstream PR 25181) would keep the thread unsettled forever. finished falls back to a silent end, same as giveUp already does. close shares the helper so it gets the fallback too
  • endsWithoutRead checks settled and subagent threads before merged, since the orchestrator refuses wakes on those
  • the watching section of docs/user/source-control.md says a merge wakes the agent and holds the thread until then

tradeoffs

proof

  • orchestrationV2PendingBackgroundWork.test.ts: the existing watch table now expects its merged row to hold. fails on main, passes here
  • runtimeLayer.test.ts, new case with 3 rows: merge seen by a host read, merge seen on the link snapshot, and a merge on a thread whose provider instance is gone. the first two fail on main (no wake). the third passes on main (silent end) and fails here if i take the fallback out (the watch stays on)
  • replayed the real thread's 6 PR links as of 14:13:55 through derivePendingBackgroundWork with the latest run completed. main: [], holds auto-settle false, so it settles. this branch: both merged watch tasks, holds true

written with claude opus 5.5 in claude code

…ettles

A watched PR that merged ended its watch with no message, and since
upstream PR 26395 a merged PR's watch no longer held auto-settle. So the
thread settled as soon as the PR sync stored the merged snapshot, and an
agent that ended its turn planning post-merge work (check the deploy,
open the next PR) never got another turn.

A merge now wakes the agent once, like a close already did, on both the
host-read and link-snapshot paths. A merged PR's watch holds auto-settle
until that wake lands, and a refused wake ends the watch silently so it
cannot hold the thread forever.
@maschwenk maschwenk added the coder-task-generated Created from a coding task label Nov 13, 2026
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:M labels Nov 13, 2026
@github-actions

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Codex Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Codex Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Codex Live turn WebSocket decoded 21.5 KiB 21.5 KiB 0 B (0.0%) 24.4 KiB ✅
Codex Live turn messages 1 1 0 (0.0%) 8 ✅
Claude Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Claude Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Claude Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Claude Live turn WebSocket decoded 21.9 KiB 21.9 KiB 0 B (0.0%) 24.4 KiB ✅
Claude Live turn messages 1 1 0 (0.0%) 8 ✅

Baseline: ce933e8 · PR result: a7cf5aa · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

coder-task-generated Created from a coding task size:M vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant