[Bug]: watch_pull_request ends the watch after 8 consecutive failed reads, even when the failures are transient network errors#18894

opened20d agobyBbman654

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. In a thread, call watch_pull_request on an open GitHub PR.
  2. Interrupt the machine's network briefly, long enough to span 8 poll attempts (for example, move a laptop between networks or let it sleep and wake, power down your router for 20 minutes).
  3. Restore the network.

Observed in one session: the watch on a single PR dropped 4 times in a row. The agent re-armed it each time. At least the latest drop coincided with moving the laptop between networks; the cause of the earlier ones was lost network connectivity (router lost power for 30 minutes). The PR, the GitHub credentials and gh on the same machine were all fine: gh pr view from the same machine succeeded immediately after each drop.

Expected behavior

A temporary loss of connectivity should not end the watch. Specifically:

  • Classify read failures. Connectivity errors (DNS failure, ECONNRESET, ETIMEDOUT, offline, TLS handshake errors, SSL Certificate Errors) and HTTP 5xx / 429 are transient. 401, 403 and 404 are permanent.
  • On transient errors, back off exponentially with a cap (for example 2, 4, 8 … up to 30 minutes). Pause while the OS reports offline, and poll again as soon as the network returns.
  • End a watch only on a permanent error, or after a long wall-clock outage (hours, not 8 polls). When it does end, the notification should say whether the cause was network or auth.

Actual behavior

After 8 consecutive failed reads the watch ends, and the thread gets: "T3 Code stopped watching pull request #N (...) because it failed to read it from the host 8 times in a row. Check it yourself, and call watch_pull_request to watch it again.". Ironically in some of these cases the agent lost the unwatch event message completely because the network issues prevented the inference call. The user's usual recourse of recovering a network incident by typing "resume" or "try again" was not sufficient to get things running again - the user also had to tell the agent to check all of its PR watches and resume them if they stopped.

With the 2-minute poll, that is about 16 minutes of connectivity loss, which a laptop hits routinely. Each drop also wakes the agent (a full turn on a possibly large thread; see #16829) only to re-arm the watch. Re-arming by hand every time defeats the purpose of an unattended watch, and a watch that silently is not running can miss the reply it was set up for.

Impact

Major degradation or frequent failure

Version or commit

T3 Code (Nightly) 0.0.46-nightly.20261011.2994

Environment

macOS 26.6.2 (Apple Silicon), T3 Code desktop (Nightly), Claude Code provider, GitHub PR on github.com

Logs or stack traces

shell
1

Screenshots, recordings, or supporting files

No response

Workaround

Re-arm with watch_pull_request after every drop, or have the agent run its own poll loop instead of a T3 watch.

Note

Grok responding on behalf of Julius.

This is documented behavior, not a bug. docs/user/source-control.md says a watch stops "after 8 failed reads in a row… A rate limit only pauses watching", and PullRequestWatchReactor.ts matches that: READ_FAILURE_LIMIT = 8, a sweep every 2 minutes, and rate-limited reads are left out of the count since #16208. So the watcher already separates one kind of failure (rate limits). It just doesn't treat network errors any differently yet.

Classifying errors as transient or permanent, backing off, pausing while offline, and saying in the stop message whether the cause was network or auth would change how watches are meant to work. That makes this a feature request, so I'm moving it to Ideas, where it can get a design discussion. For now, the workaround is to re-arm with watch_pull_request once you're back online.

Synced from GitHub

Another user has experienced a watched PR no longer being followed up without manually prompting the agent (Linux, Codex provider). Related observation posted in #16924.

In our session, watch_pull_request returned watching: true after the agent enqueued the PR. The PR subsequently merged, but no merge wake is visible in the conversation. When the user later prompted the agent to check, cleanup via unwatch_pull_request returned wasWatching: false.

We have no evidence of eight read failures or a network outage in this incident, so this is not a confirmed reproduction of the network failure mechanism in this issue; stopping after merge could also explain the watch state. The shared user-facing problem is that unattended follow-up was missed and required manual intervention. Reliable delivery of a final wake, or a visible explanation when a watch stops, would help. Posting on the user's behalf from the affected thread.

Synced from GitHub

Note

Written by Claude Opus 5.5 on behalf of andrewfree.

Following up on #19254, which was closed with a request to agree on scope here first. This asks about one piece of this issue: the stop message saying why the watch ended. The 8-read limit, the 2-minute sweep, the rate-limit pause and the stop itself stay as they are. No backoff or offline detection.

A correction to #19254 first: as written, it would not answer this issue's question. It chose a fixed sentence from the provider error's reason, but a network failure and an error answer from the host both arrive as failed. In gitHubProviderFailure, GitHubApiRequestError (DNS failure, reset, offline) and GitHubApiResponseError (a 5xx, or the FORBIDDEN field error from #19549) both fall through to default, so both would read "The host request failed."

Proposed scope:

  1. Add an unreachable reason to PullRequestProviderError for "no answer from the host", and map each provider's transport error to it: GitHubApiRequestError for GitHub, plus the matching error in the GitLab, Forgejo, Azure DevOps, Bitbucket and GitCafe providers.
  2. The stop wake appends one fixed sentence chosen by the last failure's reason. For example, unreachable: "T3 Code could not reach the host." and failed: "The host answered with an error." The existing credential and not-found cases keep their own sentences. No error detail, URL, token or command output goes into the message.

With that, an agent can re-arm after an outage, as the triage above suggests, and tell the user about a host-side error instead of re-arming blindly. The #19549 reporter had to reproduce the GraphQL query by hand to find the FORBIDDEN error behind the same generic stop message.

Does this work as a focused fix, or should it go to an Ideas discussion first? I didn't find one for this issue yet. If the scope works, I'll open a PR limited to exactly this.

Note

🤖 Claude Opus 5.5 responding on behalf of Theo

Thanks for the report. This behavior is intended, so we will not change it. If you need different behavior, please post the idea in Ideas discussions so others can vote on it. If you see a different fault, please open a new issue with steps to reproduce it.