You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
test(remote): investigate intermittent live transcript bootstrap failure #847
While verifying #846, quality-gate failed in the unchanged WebSocketSessionFeed.integration.test.ts case TranscriptStore folds live jsonl + semantic + backfill into a feed-ready snapshot. At line294, the expected fixture entry u-live was absent (entries was empty).
The same file passed all13 tests locally. The failed CI job was rerun on the identical commit, without test/source/timeout changes, and its system-test stage passed. No OpenCode CLI helper is invoked by the failing fake-manager/Claude path; the new CLI tests passed on both attempts.
The real WebSocket feed/store integration test should reliably observe its synthetic live entry before backfill, without relying on scheduling luck.
Investigation
Root cause is not established. Inspect socket-open versus subscription/bootstrap readiness and initial-history/live-event ordering. Do not simply increase waitFor timeouts or weaken the assertion. If the race is in production rather than the harness, add a deterministic behavioral regression before fixing it.
This tracks the independent verification failure; it is not included in the OpenCode export fix scope.
CI run 34650675583 passed typecheck/core tests, then failed the unchanged remote reconnect after bounded output overflow case at WebSocketSessionFeed.integration.test.ts:386: the initial backfill expected 10 entries but observed zero, before the overflow is triggered. All other 120 system tests passed; the full local run had passed this case. This is a similar bootstrap symptom, not proof of the same root cause. The usage-limit change only changes admission of API errors while waiting for a semantic turn; this failing case does not emit API errors. No assertion or timeout was changed for #891.
The subsequent #891 run 34651024742 passed system tests and the full coverage run with this integration test unchanged. The intervening commit only scoped legacy API-error display IDs by run; this bootstrap test emits no API errors. No retry, timeout, or assertion change was added.
One pattern that may help the investigation, though it isn't a diagnosis: both failures land at about 1s, which is vi.waitFor's default timeout, and both assertions saw an empty result, not a partial one. That fits the snapshot never being populated in time, whether because of a slow runner or the readiness/ordering race this issue describes. I haven't changed any timeout. It's now three failures across two cases, including one on main.
Another sighting, different case in the same file: PR #872 (head c1c9e21), quality-gate → Test system failed in WebSocketSessionFeed.integration.test.ts > remote reconnect after bounded output overflow > backfills a previously loaded session after overflow and retains access to every missed page with AssertionError: expected [] to have a length of 10 but got +0 (same "entries empty" shape as the original report).
Why this is not the PR's change: #872 adds a terminal-kind gate to SessionFeedSource.emit (getSessionKind(id) ?? getSpawnKind(id)), but this test's fake manager returns 'claude' from getSessionKind for every id, so the gate never drops a frame and the getSpawnKind fallback is never evaluated. The file passed 13/13 three consecutive times locally on Node 24 at the same commit.
Likely root cause for this whole class: the failing assertion is the test's FIRST wait — await vi.waitFor(() => expect(store.getSnapshot('s1').entries).toHaveLength(10)) right after manager.emit('started', …) — which uses vitest's DEFAULT 1000 ms vi.waitFor timeout (the first failure timed out at 1015 ms), while every later wait in the same test passes { timeout: 6000 }. The initial backfill is a full WebSocket round trip (started → client get-history → server resolveTranscriptFile + file read → reply → TranscriptStore fold); on a loaded macOS runner that can exceed 1 s. The original report's case (TranscriptStore folds live jsonl + semantic + backfill) shows the same "entries empty" shape.
Not changing the test in #872 (standing rule: don't raise timeouts to silence flakes; the fix belongs to this issue with its own decision). #872's remote change is a terminal-kind gate in SessionFeedSource.emit; this test's fake manager returns 'claude' for every id, so the gate is a pass-through here. Failed job rerun on the identical commit.
Another occurrence on an unrelated PR, during the coverage re-run of the quality gate (core, system and renderer tiers had all passed on the same commit):
WebSocketSessionFeed against a live RemoteServer > TranscriptStore folds live jsonl + semantic + backfill into a feed-ready snapshot
AssertionError: expected [] to deeply equal [ 'u-live' ], 477 of 478 files passing
The PR touches the built-in MCP domain list, the control executor's caller kinds, the operator tool projection and a renderer command; the remote feed path and its fake manager are unreachable from that change. Re-running the failed job rather than changing any timeout.
Another occurrence, PR #909 commit 31321855 (two renderer modals and a pure helper; remote feed unreachable): system tier, https://github.com/Juliusolsson05/agent-code/actions/runs/34669154994/job/103487110726. Different case in the same file this time: remote reconnect after bounded output overflow > backfills a previously loaded session after overflow and retains access to every missed page, AssertionError: expected [] to have a length of 10 but got +0. 18 of 19 system files passed. Re-running the failed job; no timeouts changed.
The failure is in the phone client's TranscriptStore, not in the test. Every session-list frame was treated as authoritative: any session absent from the list lost its state and its listener set. The server computes the handshake list at connection time, before a session that starts a moment later, and the client feed reports open on the socket event before the handshake frames are parsed. Whether that stale list is processed before or after a view subscribes depends on TCP chunking of the upgrade response:
same chunk: the stream drains on process.nextTick, before the promise continuation that subscribes; nothing evicted; the started patch backfills (passing order);
later chunk: the list is parsed after the view subscribed and read its first snapshot; state and listeners deleted; the started patch finds no state; later live entries dropped as "unviewed". The view stays mounted and empty. That is both signatures seen here (expected [] to have a length of 10, expected [] to deeply equal ['u-live']).
CI load widens the gap between the upgrade response and the handshake frames, which is why it clustered there and rarely reproduced locally. On the phone the same race hits any reconnect and any tap on a just-started session.
#911 makes lists evict only unviewed state, never touch listeners, retries backfill over views rather than states, and drops a forgotten session's state when its last view leaves (retention guarantee kept). A new unit suite replays the order deterministically; all four cases fail on the pre-fix store. No timeout or assertion was changed in the integration test.
Activity
Juliusolsson05 commentedon Sep 11, 2026
Two more occurrences today, on commits that don't touch
src/remote-clientorsrc/main/remote:main@48ea6912(merge of feat(codex): add Remove Cybersecurity Block command #849): quality-gate "Enforce coverage baseline" failed a different case in the same file, "remote reconnect after bounded output overflow > backfills a previously loaded session after overflow and retains access to every missed page", withexpected [] to have a length of 10 but got +0at 1017ms. The same case passed earlier in that job's "Test system" step. https://github.com/Juliusolsson05/agent-code/actions/runs/345394967736ee33cc0(Dispatch renderer files only): quality-gate "Test system" failed this case again, "TranscriptStore folds live jsonl + semantic + backfill…", withexpected [] to deeply equal [ 'u-live' ]at 1012ms. The PR's previous head0ef2b17epassed the whole file. https://github.com/Juliusolsson05/agent-code/actions/runs/34545514427One pattern that may help the investigation, though it isn't a diagnosis: both failures land at about 1s, which is
vi.waitFor's default timeout, and both assertions saw an empty result, not a partial one. That fits the snapshot never being populated in time, whether because of a slow runner or the readiness/ordering race this issue describes. I haven't changed any timeout. It's now three failures across two cases, including one onmain.Juliusolsson05 commentedon Sep 11, 2026
Another sighting, different case in the same file: PR #872 (head c1c9e21),
quality-gate→ Test system failed inWebSocketSessionFeed.integration.test.ts > remote reconnect after bounded output overflow > backfills a previously loaded session after overflow and retains access to every missed pagewithAssertionError: expected [] to have a length of 10 but got +0(same "entries empty" shape as the original report).Why this is not the PR's change: #872 adds a terminal-kind gate to
SessionFeedSource.emit(getSessionKind(id) ?? getSpawnKind(id)), but this test's fake manager returns'claude'fromgetSessionKindfor every id, so the gate never drops a frame and thegetSpawnKindfallback is never evaluated. The file passed 13/13 three consecutive times locally on Node 24 at the same commit.Run: https://github.com/Juliusolsson05/agent-code/actions/runs/34624833635 — failed job rerun on the identical commit with no test/source/timeout changes.
Juliusolsson05 commentedon Sep 11, 2026
Second sighting on PR #872 (merged-with-main head 6cd99ec, run https://github.com/Juliusolsson05/agent-code/actions/runs/34628010752): same case,
backfills a previously loaded session after overflow and retains access to every missed page,expected [] to have a length of 10 but got +0.Likely root cause for this whole class: the failing assertion is the test's FIRST wait —
await vi.waitFor(() => expect(store.getSnapshot('s1').entries).toHaveLength(10))right aftermanager.emit('started', …)— which uses vitest's DEFAULT 1000 msvi.waitFortimeout (the first failure timed out at 1015 ms), while every later wait in the same test passes{ timeout: 6000 }. The initial backfill is a full WebSocket round trip (started → client get-history → server resolveTranscriptFile + file read → reply → TranscriptStore fold); on a loaded macOS runner that can exceed 1 s. The original report's case (TranscriptStore folds live jsonl + semantic + backfill) shows the same "entries empty" shape.Not changing the test in #872 (standing rule: don't raise timeouts to silence flakes; the fix belongs to this issue with its own decision). #872's remote change is a terminal-kind gate in
SessionFeedSource.emit; this test's fake manager returns'claude'for every id, so the gate is a pass-through here. Failed job rerun on the identical commit.Juliusolsson05 commentedon Sep 12, 2026
Another occurrence on an unrelated PR, during the coverage re-run of the quality gate (core, system and renderer tiers had all passed on the same commit):
e697f30bWebSocketSessionFeed against a live RemoteServer > TranscriptStore folds live jsonl + semantic + backfill into a feed-ready snapshotAssertionError: expected [] to deeply equal [ 'u-live' ], 477 of 478 files passingThe PR touches the built-in MCP domain list, the control executor's caller kinds, the operator tool projection and a renderer command; the remote feed path and its fake manager are unreachable from that change. Re-running the failed job rather than changing any timeout.
Juliusolsson05 commentedon Sep 12, 2026
Second consecutive occurrence on the same commit (
e697f30b, PR #907), this time in the system tier of the re-run job: https://github.com/Juliusolsson05/agent-code/actions/runs/34662138975/job/103468427654 — same assertion (expected [] to deeply equal [ 'u-live' ]). The same tier passed on this exact commit in the previous attempt, so the failure is not deterministic for the branch.Juliusolsson05 commentedon Sep 12, 2026
Another occurrence, PR #909 commit
31321855(two renderer modals and a pure helper; remote feed unreachable): system tier, https://github.com/Juliusolsson05/agent-code/actions/runs/34669154994/job/103487110726. Different case in the same file this time:remote reconnect after bounded output overflow > backfills a previously loaded session after overflow and retains access to every missed page,AssertionError: expected [] to have a length of 10 but got +0. 18 of 19 system files passed. Re-running the failed job; no timeouts changed.docs(remote): record the session-list eviction pull request in the plan
Juliusolsson05 commentedon Sep 12, 2026
Root cause established; fix in #911.
The failure is in the phone client's
TranscriptStore, not in the test. Everysession-listframe was treated as authoritative: any session absent from the list lost its state and its listener set. The server computes the handshake list at connection time, before a session that starts a moment later, and the client feed reportsopenon the socket event before the handshake frames are parsed. Whether that stale list is processed before or after a view subscribes depends on TCP chunking of the upgrade response:process.nextTick, before the promise continuation that subscribes; nothing evicted; thestartedpatch backfills (passing order);startedpatch finds no state; later live entries dropped as "unviewed". The view stays mounted and empty. That is both signatures seen here (expected [] to have a length of 10,expected [] to deeply equal ['u-live']).CI load widens the gap between the upgrade response and the handshake frames, which is why it clustered there and rarely reproduced locally. On the phone the same race hits any reconnect and any tap on a just-started session.
#911 makes lists evict only unviewed state, never touch listeners, retries backfill over views rather than states, and drops a forgotten session's state when its last view leaves (retention guarantee kept). A new unit suite replays the order deterministically; all four cases fail on the pre-fix store. No timeout or assertion was changed in the integration test.