Repository navigation
Description
Before submitting
- I searched existing issues; related reports and their differences are listed below.
- I included a deterministic reproduction and live timing evidence.
Area
apps/server
Summary
A watched GitHub PR received its first checks-passed wake. After review, the agent marked the PR ready, which triggered another CI run on the same head SHA. The agent ended its turn explicitly awaiting that run. The new run started and succeeded entirely between two watcher sweeps. T3 subsequently read the final state but never woke the thread again.
The result is polling-phase dependent: if the evaluator observes the intermediate pending state it emits another completion wake; if it misses pending, the same new run's completion is suppressed.
Steps to reproduce
Live sequence
- Use a GitHub Actions workflow triggered on both
pull_request.openedandpull_request.ready_for_review, with a quick check such asvalidate. - Open a draft PR and call
watch_pull_request. Let its initial checks pass and wait for the checks-passed notification. - Without pushing a commit, mark the PR ready. Have the agent end its turn awaiting the newly triggered CI run, keeping the existing watch active.
- The new run must start and finish between two watcher reads. A slower runner that lets T3 observe pending can mask the problem.
- Observe through the next full detail read. The snapshot becomes passing, but no completion notification or new agent run is created.
The live incident below was an existing authorized workflow, not a newly created public test PR. It was investigated using read-only GitHub queries, persisted watch events and server traces. No additional message, rewatch or runtime restart was injected into the measured interval.
Deterministic reproduction (no server, provider, runner or npm install required)
Requires a Node version exposing node:module.stripTypeScriptTypes (tested with Node 24.21.0). Save this as repro.mjs:
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { stripTypeScriptTypes } from 'node:module';
const source = readFileSync(process.argv[2], 'utf8');
// Fixtures never enter the DateTime-dependent no-checks branch.
const js = stripTypeScriptTypes(source).replace(
'import * as DateTime from "effect/DateTime";', ''
);
const { evaluatePullRequestWatch: evaluate } = await import(
'data:text/javascript;base64,' + Buffer.from(js).toString('base64')
);
const watch = {
startedAt: '2026-11-11T00:00:00Z', headSha: 'same-head',
failedChecks: [], passed: false, passedChecks: [], owedChecks: [],
remarksThrough: '2026-11-11T00:00:00Z', remarkIds: [],
conflicting: false, wakes: 0,
};
const results = [];
for (const required of [false, true]) {
const detail = (status, run, headSha = 'same-head') => ({
headSha, capabilities: { checksAlwaysReport: false },
mergeability: 'mergeable', viewer: 'agent-user',
author: { login: 'agent-user' },
checks: [{
name: 'validate', status, required, description: null,
url: `https://ci.example/runs/${run}/jobs/validate`,
}],
});
const first = evaluate(watch, detail('success', 'A'), [], 0);
assert.equal(first.changes[0].kind, 'checks-passed');
const sameRead = evaluate(first.next, detail('success', 'A'), [], 0);
assert.deepEqual(sameRead.changes, []);
const missed = evaluate(first.next, detail('success', 'B'), [], 0);
assert.deepEqual(missed.changes, []); // Confirms the observed missing wake.
const pending = evaluate(first.next, detail('pending', 'B'), [], 0);
const control = evaluate(pending.next, detail('success', 'B'), [], 0);
assert.equal(control.changes[0].kind, 'checks-passed');
const pushed = evaluate(first.next, detail('success', 'B', 'new-head'), [], 0);
assert.equal(pushed.changes[0].kind, 'checks-passed');
results.push({
required, initial: first.changes, sameRunReread: sameRead.changes,
newRunWithoutPendingRead: missed.changes,
newRunWithPendingRead: control.changes, newHead: pushed.changes,
});
if (process.argv.includes('--expect-wake')) {
assert.ok(missed.changes.some(x => x.kind === 'checks-passed'),
'A new completed CI run was not reported because pending was not observed');
}
}
console.log(JSON.stringify(results, null, 2));Download the actual release source and run:
curl -fsSL https://raw.githubusercontent.com/pingdotgg/t3code/bf469bb19669671d0df2efda74ed73867a7137c0/apps/server/src/orchestration-v2/pullRequestWatch.ts -o pullRequestWatch.ts
node repro.mjs pullRequestWatch.ts
# Explicit expected-behavior assertion, currently exits 1:
node repro.mjs pullRequestWatch.ts --expect-wakeThe script strips TypeScript types and removes the external DateTime import. The fixtures never enter its no-checks branch. It runs the actual upstream evaluator; it does not reimplement the decision logic. The same missing-wake behavior was separately reproduced against the evaluator extracted from the installed binary.
Results for both an explicitly required gate and the fallback all-checks gate:
initial successful run A => checks-passed
reread of the same successful run A => [] (correct deduplication)
new successful run B, pending not observed => [] (missing completion)
new run B, pending observed before success => checks-passed
successful run after a new head SHA => checks-passed
Run B has the same check name but a different run URL. --expect-wake fails with:
AssertionError: A new completed CI run was not reported because pending was not observed
Expected behavior
An agent explicitly awaiting a newly started CI cycle should receive its completion even when the cycle finishes between polls. Repeated reads of the same completed run should remain deduplicated. At minimum, whether completion is reported should not depend on accidentally observing pending between two otherwise identical run histories.
This is not a request to merge automatically or to treat a wake as proof that merge/deploy policy is satisfied.
Actual behavior
The watch remains active, the PR snapshot updates to passing, and no new agent turn starts. The final agent message continues to say it is awaiting CI even though that CI has finished.
Diagnosis
The installed reactor uses a two-minute sweep interval. evaluatePullRequestWatch deduplicates success using the head SHA, passed, gate names and owed checks. It does not distinguish successive completed CI runs through their URL/ID/attempt.
With the same SHA and gate names, previously saved passed=true, and owedChecks=[], a second successful run does not satisfy the new-pass condition. A pending read resets passed, explaining the contrasting control result. No new gate names appear in this case, so the late-required-gate handling does not help.
A shorter interval may reduce frequency but cannot eliminate this race. Any fix should preserve same-run deduplication while recognizing a new awaited CI cycle; run identity may need to survive the provider/fingerprint/evaluator boundary.
Impact
Major degradation or frequent failure: an agent waiting for an authorized follow-up can remain idle indefinitely until a user supplies another message.
Version or commit
Live incident and installed evaluator: 0.1.8, release commit bf469bb19669671d0df2efda74ed73867a7137c0.
Deterministic public-source reproduction also confirmed against main at dea84724384fcc68ef802e7db8440d9abaf0ef26. This is a function-level check of main, not a full end-to-end run of that build.
Source: release evaluator, release reactor.
Environment
Linux ARM64 server, kernel 7.0.0-1013-oracle; T3 Code 0.1.8; Codex provider, gpt-6.1-sol; GitHub Actions. Local standalone reproduction: Node 24.21.0. No UI-specific behavior was tested.
Logs or stack traces
Sanitized live evidence; all times UTC on 2026-11-11:
06:07:54.000 Initial CI succeeds.
06:08:40.462 Watch enabled.
06:10:11.222 Initial checks-passed wake delivered; agent verifies first run.
06:15:28.454 Review completion wakes agent.
06:16:12.513 Watch sweep starts; new CI does not yet exist.
06:16:12.994 Watch sweep finishes.
Agent marks draft PR ready, keeping head SHA unchanged.
06:16:29.000 New CI run created.
06:16:43.485 Agent turn ends, explicitly waiting for new CI.
06:16:45.779 PR snapshot updates: draft=false, checksState=pending.
Saved watch still has passed=true and owedChecks=[].
06:17:18.000 New successful check completes.
06:17:19.000 New CI run completes successfully.
06:18:12.995 Next watch sweep starts.
06:18:13.428 Watch readGroup performs full detail read.
06:18:14.083 Sweep finishes successfully.
06:18:14.644 PR snapshot updates: checksState=passing.
Watch remains active: passed=true, owedChecks=[].
06:56+ Observer confirms no new notification/run; no active run,
no pending user request; GitHub reports PR CLEAN.
The snapshot synchronization and the watch evaluator use separate paths: a persisted pending snapshot does not mean that the periodic evaluator observed a pending gate. The actual watch sweeps bracket the complete new CI run.
Private PR URLs, thread IDs, account names and home paths are intentionally omitted. The synthetic reproduction above contains no organization-specific data.
Related issues
- [Bug]: GitLab PR watch misses manual-job completion between polls, leaving dependent work stalled #34110: closely related underlying timing problem, but GitLab optional manual jobs changing neutral to success; this report supplies a GitHub successful-run-to-successful-run case with captured watcher sweep timings. That report was closed as not planned after an initially inconclusive live reproduction; a later comment adds stronger evidence.
- [Bug]: PR watch reports "All N checks passed" again on the same head after a pending read or a rerun #32276: reports repeated pass notifications when pending is observed, and proposes pass-once-per-head semantics. This report demonstrates the complementary path: an agent awaiting a new run gets no completion when pending is missed. Please consider these together when deciding consistent rerun semantics.
- [Bug]: PR watch never reports a rerun that fails again, and on GitHub a failed run can be hidden by another event's run of the same check #32277: same-head reruns that fail again between reads; this report is about successful subsequent runs.
- [Bug]: watch_pull_request can miss late required gates that pass between polls #15362: a newly appearing required gate, already fixed; here the set of check names does not change.
- PR watcher misses successful replacement CI when job names change #30378: superseded cancelled checks with changed job names; here the replacement successful check names stay unchanged and no stale failure is needed for the deterministic reproduction.
Workaround
Manually prompt the waiting thread to verify the current run and continue. This supplies a manual wake but does not fix monitoring. No runtime patch, database write or service restart was performed for this investigation.
Activity
juliusmarminge commentedon Nov 11, 2026
I tried to reproduce this on the current build and could not.
Not run end to end: that needs a GitHub PR whose same-head CI rerun starts and finishes inside one 2-minute watcher sweep, and I did not create a test repo or PR for that. On origin/main 362ef9a I added a case to apps/server/src/orchestration-v2/pullRequestWatch.test.ts that calls the evaluator (evaluatePullRequestWatch) directly. A first read with check 'validate' passed in run 1 gives checks-passed. A reread of run 1 gives no change, which is correct. A read where the same check passed in run 2 (same head, new URL), with no pending read in between, gives no change: the missing wake. The same result holds with the check required and not required.
vp test run src/orchestration-v2/pullRequestWatch.test.ts: 1 failed (only the new assertion), 23 passed. Pushed as factory/repro-34212 (1351502).Closing as not planned. If it still happens for you, reply with the exact steps and the version you are on and it will be looked at again.
AndryHTC commentedon Nov 11, 2026
Follow-up after an independent review of the report:
The polling-dependent behavior is confirmed by the evaluator reproduction. The desired resolution should be stated separately from that observation: recognizing a newly completed CI run is our proposal; #32276 proposes pass-once-per-head instead. Whichever policy is chosen, observing an intermediate pending read should not accidentally decide whether a completion is news.
One correction to the standalone script: guard the observed-behavior assertion so
--expect-wakecan also pass after a fix:The fixture evaluates the initial pass before reusing
first.next, so its second read uses a non-emptypassedCheckslist. The empty list in the initial input is not the state used for reproducing the missed wake.The live case was on 0.1.8 (
bf469bb19669671d0df2efda74ed73867a7137c0). Its two recorded watcher sweeps bracket the complete new run. Selected trace fields, UTC on November 11:The watch remained active and no subsequent notification or run existed at observation. This is evidence from an existing live workflow, not a newly executed end-to-end test of current main. The standalone main check is function-level only. Frequency was not measured; the impact is a stalled continuation when a new CI cycle completes between reads. Current main also uses workflow URLs for approval tracking; the URL-insensitive claim concerns successful-run deduplication specifically.
I am studying a focused fix and a reactor/persisted-notification regression. No running installation or production state has been modified.
AndryHTC commentedon Nov 11, 2026
Following up on #34212 (comment) with the completed reactor regression and proposed fix, rather than just the evaluator reproduction.
Affected live version: T3 Code 0.1.8, release commit
bf469bb19669671d0df2efda74ed73867a7137c0, Linux ARM64, GitHub Actions, Codex provider.Exact live sequence:
pull_request.openedandpull_request.ready_for_review.watch_pull_request, let the first CI run pass and receive its completion wake.The existing live incident meets that timing condition: the new run was created at 06:16:29 UTC and completed successfully at 06:17:19 on November 11; the agent ended its waiting turn at 06:16:43.485. Watcher reads bracketed the run at 06:16:12.994 and 06:18:13.428. The later snapshot was passing, the watch remained active, and no follow-up turn existed at observation. The selected server trace fields are in my preceding comment: #34212 (comment).
Additional evidence now available: #34221, current head
7731959f5e8b6e71415dec8b4db108eb7d9c5562, extends the existingruntimeLayer.test.tsreactor lifecycle case. It supplies two successful same-head runs with different job URLs and no intermediate pending detail. On the original implementation the assertion fails because only one notification is persisted; with the proposed fix the second notification is persisted exactly once. It also checks rereads, reactor restart, unwatch/rewatch, required/fallback gates and a new head. This executes the reactor, orchestrator and persistence with a mocked source-control host, not just the pure evaluator.The final candidate passes local
check:changed: 170 tests in 7 files, typecheck of 37 affected/dependent packages, format, lint, knip and GraphQL checks. Review feedback on duplicate check names, owed advisory completion and GitLab fallback baselines was corrected; an independent Opus 5.5 medium code review found no blockers. Known non-blocking GitLab limitations are documented in the PR.This is not a claim that we ran a fresh real-GitHub end-to-end experiment on current main, or installed the patched runtime. That validation remains outstanding. The proposed per-run completion policy also remains subject to maintainer agreement, especially alongside #32276.
Could you reconsider/reopen this report in light of the recorded live case and the reactor-level regression? The proposed fix and its verification are available in #34221.
AndryHTC commentedon Nov 11, 2026
@juliusmarminge would you have a look?