Skip to content

P2: PR refresher can't see archived worker PRs (34 of 35 stuck OPEN); merged-worktree cleanup ignores running threads #1431

Closed

Description

@currentbits

Found in the T3 Orchestrator V2 deep dive (report: reports/T3 Code orchestrator deep dive.md on branch coder/i-want-you-to-research-t3-codes-orchestr-fae77a). T3 V2 now polls watched PRs on the server and wakes the agent on failed checks, review comments and conflicts (t3code #15057). Solenta already has most of this. The gap is that its poller never looks at the threads that matter most.

Bugs

  • P1: archived threads are skipped (verified). isPrRefreshCandidate starts with if (t.archived) return false; (electron/worktrees.js:4319), and a test pins it (electron/test/pr-refresh.test.js:384). sweepCrew archives finished workers (runner.js:1784-1820). So of the 35 worker PRs in the live store, 34 are archived and still OPEN, one is archived and merged, and three still hold a worktree. Not checked against GitHub: they may really be merged.
  • P2: cleanup can delete a running worker's checkout. maybeCleanupMergedWorktree (worktrees.js:5539-5595) checks for a dirty tree but never checks whether the thread is running. A worker fixing review comments with a clean tree when its PR merges loses its working directory mid-run.

What already exists

  • refreshPrStates / createPrStateRefresher: a serialized 5-minute pass (worktrees.js:4350-4547, wired in main.js:779-790).
  • Host-side gh running under the user's own auth.
  • Check bucketing, worktree reclaim on merge, and a post-merge re-verify hook.
  • Settle-on-merge, computed in the renderer (src/threadSettle.ts:33-39).

Design (~150–220 lines + ~150 test lines)

  1. Arm the watch. After createPr resolves in thread_pr (orchServer.js:678-686), write a main-process-only field on the worker: prWatch: {armedAt, notify: <lead id>, last: {state, mergeable, review, failing[]} | null}.
  2. Poll armed threads even when archived. Change the candidate check to if (t.archived && !t.prWatch) return false;. Give the existing archived/OPEN rows one state refresh with no notices, so they reach a terminal state and drop out.
  3. Fetch. For armed threads only, one gh pr view --json number,url,state,mergeable,reviewDecision,statusCheckRollup. Fall back through the existing isUnknownJsonField.
  4. Notify on entering a bad or final state. Compare against prWatch.last, which is what the lead was last told. Send at most one line per thread per pass, only when entering merged / closed / CONFLICTING / CHANGES_REQUESTED, or when the failing-check set changes. The first observation is silent unless it's already bad.
  5. Deliver through the runner. Add an injected opts.notify that main.js wires to runner.deliverNotice (runner.js:1282-1294). It skips when the lead is missing, trashed or archived. Line: [pr] Worker <id> ("title") PR #N <signal>. <url>. Send the worker the fix with thread_send; merging stays with the user. Persist last in the same save, so a crash can miss a wake but never duplicate one.
  6. Guard cleanup (P2). Inject isRunning(threadId) and skip cleanup while the thread is running.

Deliberately left out: reading comments, a 1-minute cadence, agent-performed merges, and watching plain UI PRs (add a per-thread toggle if someone wants that).

Check before shipping: T3 #15388 had wake turns keeping merged threads from settling. Confirm which message timestamp effectiveSettled measures from, so [pr] notice turns can't block settling.

Tests (extend pr-refresh.test.js, which already injects ghTryAsyncFn)

  • An armed archived worker is polled with the rich fields.
  • A pass-to-fail transition notifies once, then stays quiet.
  • CHANGES_REQUESTED and CONFLICTING notify.
  • A merge notifies and cleans up, but not while the thread is running.
  • Unarmed threads, and threads whose lead is archived, never notify.
  • The unknown-field fallback still works.

There's a soft dependency on #1426: until notices are durable, a crash can lose a PR wake. The next pass after boot detects the transition again.

Activity

  1. currentbits commented on Oct 4, 2026

    @currentbits
    OwnerAuthor

    GitHub reality check and repros at 615b38a0. Repro tests: research_notes/T3 orchestrator ACP UX and repros/repros/ on branch coder/i-want-you-to-research-t3-codes-orchestr-fae77a.

    • P1, reproduced. Read-only gh pr view on the 34 archived worker PRs the store calls OPEN: 29 merged, 3 closed, 2 actually open (appfeedback Click an image in a thread to view it full-size #78, cometx Failed thread load dead-ends on 'Select a thread' with no error or retry #82). They span 5 repos, not only solenta. The one archived plain thread marked OPEN is also merged.
    • P2, reproduced. A merge detected by the PR refresher deletes the worktree of a worker that is running.
    • Settle question (T3 #15388), answered. effectiveSettled (src/threadSettle.ts) reads no message timestamp. A merged thread settles regardless of time, and stays open only while working or quota-wait. So [pr] wake turns can't keep a merged thread unsettled. Inactivity for threads without a PR is measured from thread.updatedAt, which notice turns bump, so machine turns restart the 3-day clock there. Consider measuring from the last human message.
    • Merged worker threads never settle today only because of P1: their state is stuck at OPEN.

    Suggested split. Ship the P2 running-guard plus a one-time catch-up refresh of archived PR rows now; it's small and fixes 32 stale rows. The rows that are closed but not merged need a closed-PR rule for worktree reclaim. The armed PR watch with lead notifications follows later, after #1426 makes notices durable.

    The two PRs that are really open (appfeedback #78, cometx #82) may need the user's attention.

  2. added and removed on Oct 12, 2026
  3. currentbits commented on Oct 12, 2026

    @currentbits
    OwnerAuthor

    PR #1607 is open for review.

    Most of the design here had already shipped as electron/prWatch.js (#1493 D). This PR adds what was still missing:

    • archived threads are polled again;
    • a PR alert for an archived worker goes to its live lead;
    • maybeCleanupMergedWorktree won't delete a worktree while its thread is running, and the next refresh pass retries the cleanup.

    Deliberately not included: alerting the lead when a PR merges or closes.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions