Repository navigation
[Bug]: the Cursor T04 heartbeat-only case still states a no-deadline-expired contract against real elapsed time #5168
Description
Client or integration
Other
Area
Provider adapter
Summary
The Cursor T04 heartbeat-only test case states a correct production contract in a way that only holds while the CI runner keeps up, so it can report a failure while the watchdog is behaving correctly.
tests/providers/cursor/cursor-stream-health.test.ts asserts that liveness-only traffic survives the silence threshold and then fails at the longer heartbeat-only threshold. It demonstrates the first half by keeping a real setInterval writing heartbeat and checkpoint frames every 40ms, which requires that no decoded-frame gap ever exceeds the silence budget for the whole heartbeat-only window. On CI that budget is floored to 5s by isolationBudgetMs, inside a 22s fixture window.
That is an assertion that a deadline did not expire, and it carries a term for elapsed real time. The synthetic HTTP/2 server shares one Bun process with the rest of the suite, so under contention its own frames arrive late, the silence watchdog fires first, and the case fails with no inbound frames while both watchdogs are correct. The production contract is not at fault: CURSOR_STREAM_SILENCE_FAIL_MS is 30s and CURSOR_STREAM_HEARTBEAT_ONLY_FAIL_MS is 90s.
This is the defect #5131 removed from the sibling re-arming case in the same file, left behind on the one case that still needed a real interval. Scaling the budget is not a remedy: #3940 already scaled it once, and a larger floor lengthens the window during which no pause may exceed one budget.
Reproduction
Static source trace at f6b59da1919ba7a73ef6a76d265b119a43eec0d1, plus the hosted CI observations below. No local suite was run.
- Read
tests/providers/cursor/cursor-stream-health.test.ts:silenceMsisisolationBudgetMs(1_000)(5s on CI),heartbeatOnlyMsis twice that, and the fixture writes liveness frames on a real 40ms interval. - Read
armStreamHealthTimerinsrc/adapters/cursor/live-transport.ts: the single timer fires at the minimum oflastInboundFrameAt + silenceMsandlastMeaningfulFrameAt + heartbeatOnlyMs, and the reported message is selected byheartbeatOnly = stalledFor < silenceMs. - Delay decoded frame arrival past the silence budget at any point in that 10s window, which both the unsharded and the sharded macOS lanes do. The silence watchdog wins and the case fails on the message assertion.
The same defect has already produced a real failure in this file. Run 35386429688, job 105822272974, on the unsharded macos control lane reported Cursor stream stalled: no inbound frames for 5s before turnEnded from live-transport.ts:895 against the sibling re-arming case at 16733.88ms; that occurrence was dispositioned by #5131. The heartbeat-only case has not failed yet, and it has come within 1.2s of the same outcome: 11219.78ms in that job and 10029.66ms in job 105880533745 (macos 2/2, run 35436695398), both against a 10s budget.
Version
f6b59da1919ba7a73ef6a76d265b119a43eec0d1 (dev, after #5131 landed as e668aa1f2e).
Operating system
Observed on the hosted macOS lanes (macos control and macos 2/2). The mechanism is platform-independent; the Windows shards carry a larger floor and the same shape.
Provider and model
cursor / composer-2 against a synthetic in-process HTTP/2 Connect fixture. No credentials and no live provider are involved.
Logs or error output
# run 35386429688, job 105822272974 (macos control) - sibling case, dispositioned by #5131
(pass) Cursor inbound stream-health watchdog (T04) > heartbeat-only traffic survives the silence threshold but fails at the heartbeat-only threshold [11219.78ms]
error: Cursor stream stalled: no inbound frames for 5s before turnEnded
at <anonymous> (src/adapters/cursor/live-transport.ts:895:18)
(fail) Cursor inbound stream-health watchdog (T04) > meaningful frames keep resetting both clocks; turnEnded finishes cleanly [16733.88ms]
# run 35436695398, job 105880533745 (macos 2/2)
(pass) Cursor inbound stream-health watchdog (T04) > heartbeat-only traffic survives the silence threshold but fails at the heartbeat-only threshold [10029.66ms]
Screenshots and supporting files
- The watchdog and its two production budgets: src/adapters/cursor/live-transport.ts:96.
- The deadline and the message selector: src/adapters/cursor/live-transport.ts:875.
- The clock seam fix(cursor): state the T04 re-arming contract on an injectable clock #5131 added: src/adapters/cursor/transport.ts:69.
- The case that still needs a real interval: tests/providers/cursor/cursor-stream-health.test.ts:200.
- The CI floor that lengthens the exposure rather than shortening it: tests/helpers/ci-watchdog.ts.
- Prior disposition of the same defect class on the sibling case: fix(cursor): state the T04 re-arming contract on an injectable clock #5131, recorded against [Bug]: four tests exceed their timeouts in the unsharded macOS control lane, first observed once the lane was able to finish #4997.
Redacted configuration
{ "adapter": "cursor", "baseUrl": "http://127.0.0.1:0", "apiKey": "test-token" }Implementation path
- Move the heartbeat-only case onto the existing
streamHealthClockseam and advance virtual time by hand between liveness frames whose arrival the case awaits. Contention can then delay a frame without any deadline passing. - Synchronise on a decoded frame rather than on elapsed time. A
conversationCheckpointUpdateis a progress frame, so it yields one outwardheartbeatmessage; a bare heartbeat frame yields none, and the frame chain is serialized, so awaiting the checkpoint message proves both frames were decoded and the watchdog re-armed. - Keep the firing half of the contract on real timers in the silence case, so a genuine stall still fails the turn for real.
- Leave production budgets, the first-frame timer, the turnEnded grace and the outbound heartbeat interval untouched. No raised timeout, no retry, no platform skip.
Acceptance criteria and verification
- No T04 assertion claims that a deadline did not expire while depending on real elapsed time.
- The case still fails, and still names which watchdog won, if liveness frames wrongly refresh the progress clock, if the deadline stops being the minimum of the two, or if the watchdog never fires.
- Production silence and heartbeat-only budgets are unchanged in
src/adapters/cursor/live-transport.ts. - Execution proof comes from exact-head hosted CI only. No local suite, focused test, typecheck or build is run for this work.
Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
lidge-jun commentedon Sep 19, 2026
Closed by #5169, squash-merged into
devasef7e59f930.Two corrections belong in this record, because the brief that opened this lane was wrong on both counts and the fix is better for having been argued down.
HEARTBEAT_MS = 5_000inlive-transport.tsis the outbound client heartbeat interval, not a watchdog threshold. The production T04 budgets areCURSOR_STREAM_SILENCE_FAIL_MSat 30s andCURSOR_STREAM_HEARTBEAT_ONLY_FAIL_MSat 90s, and the 5s that appears in CI output is a budget the test itself shrinks, floored byisolationBudgetMs. Nothing in #5169 touches a production budget.The specific occurrence named at dispatch —
Cursor stream stalled: no inbound frames for 5s before turnEndedat 16733.88ms in job 105822272974 — had already been dispositioned by #5131, which landed ase668aa1f2eand put the re-arming case on an injected clock. The defect class was real; that instance was closed.What remained open was the sibling case in the same file. The heartbeat-only case kept a real 40ms interval and required no decoded-frame gap beyond the 5s silence floor across a 10s window, which is the same "a deadline did not expire" shape #5131 removed from its neighbour. It had not failed, and it had come within 1.2s twice: 11219.78ms in that same job and 10029.66ms on macos 2/2 of run 35436695398, both against a 10s budget. Waiting for it to go red would have taught reviewers that a red Cursor lane is something you rerun.
Adversarial review with five parallel reviewers found a defect CI would have passed, and it is the more important half of this change. The
min(silence, progress)rule was red-capable nowhere: a watchdog reading only the progress deadline would relax production silence detection from 30s to 90s while every real-timer case stayed green, because a later deadline still produces the same message inside the case timeout. There is now a virtual-clock case requiring the turn to fail at exactly S with nothing left armed. A second finding restored real-timer coverage of the progress branch firing through the production default clock, whichstructure/providers/cursor.mdasserted was still present; the document now states which half lives where. A third bounded the manual clock's drain loop, because an expired-deadline re-arm mutation would have spunadvanceTosynchronously and wedged the lane rather than reddening one case.The division the file now follows is that every "a deadline did not expire" claim lives under the injected clock, and every "it fires" claim stays on real timers with budgets chosen so ordinary load delays the case rather than flipping its outcome — the real-timer progress case uses a 60s silence budget against a 600ms progress budget, so a pause long enough to be misreported as silence has already blown the case's own 15s limit.
No local suite was run in this lane. The change touches only
tests/andstructure/, so the changes filter correctly skipped the Windows shard matrix on the pull-request event; thedevpush matrix covers Windows at the merged commit.