Skip to content

fix(tui): retry transient session and history reads - #59543

Open
x0c wants to merge 3 commits into
anomalyco:devfrom
x0c:dev
Open

x0c wants to merge 3 commits into
anomalyco:devfrom
x0c:dev

Conversation

@x0c

@x0c x0c commented Oct 27, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #59542

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

A failed initial metadata read sends a resumed session home; a failed history read leaves it empty. Both reads now use the shared retry helper for transient errors, with at most five attempts. Leaving the session or reconnecting cancels obsolete retries. Permanent errors fail immediately; exhausted initial history reads show an error. Older-page boundary completion keeps its existing best-effort handling.

How did you verify your code works?

  • Regressions for initial history and older-page failures fail before, pass after. Cancellation and exact retry limits use fake timers; app tests cover rendering, permanent errors and reconnecting.
  • TUI suite: 1572 passed, 2 existing skips; util suite: 139 passed. bun run check and affected packages' typecheck:tsc pass.
  • Real CLI in Terminal/tmux: transient reads recover without reopening the session; a failed older-page read preserves the loaded history without a false error.

Visual

<svg xmlns="http://www.w3.org/2000/svg" width="240" height="72" viewBox="0 0 240 72">
<style>.pulse{animation:p 3s infinite}.b{animation-delay:.8s}.c{animation-delay:1.6s}@keyframes p{0%,60%,100%{opacity:.4}25%{opacity:1}}@media(prefers-reduced-motion:reduce){.pulse{animation:none}}</style>
<path d="M42 28H198" fill="none" stroke="currentColor" opacity=".25"/>
<g fill="currentColor" font-family="sans-serif" font-size="11" text-anchor="middle">
<g class="pulse"><circle cx="40" cy="28" r="7"/><text x="40" y="55">Read</text></g>
<g class="pulse b"><circle cx="120" cy="28" r="7"/><text x="120" y="55">Retry</text></g>
<g class="pulse c"><circle cx="200" cy="28" r="7"/><text x="200" y="55">History</text></g>
</g></svg>

Screenshots / recordings

Terminal captures from a synthetic session. One initial history read returns HTTP 503; later reads succeed.

Before:

Before: history remains empty

After:

After: history loads automatically

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Related: #36582

Public API: adds optional signal to retry() options and exports isTransientError from @opencode/util/retry (backwards compatible)

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks right. On the base, the PR's resume-retry tests fail: after transient 503s the history never renders and no error is shown. With the change they pass, and the nearby TUI and util tests and tsc type checks pass too. I checked this at the app-test level only, not against a live server returning 503s.

Public API: @opencode/util/retry gains an optional signal in RetryOptions and now exports isTransientError. Both are additive and backwards compatible; no server, SDK, config or CLI surface changes.

Two things worth addressing (inline): history retries now also wrap the boundary-completion step, and the new app-level tests use real-time waits.

Related: #36582

Comment thread packages/tui/src/routes/session/rows.ts Outdated
if (controller.signal.aborted) return
rebuild()
// Restoration waits for complete boundary groups so saved group IDs resolve.
await completeGroupBoundary({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

completeGroupBoundary failures used to be swallowed after the first page had already rendered. Now they're inside retrySessionSync, so a failed older-page loadMore re-runs message.sync + rebuild() from scratch. After five attempts it also shows "Failed to load session history" even though the history is on screen. Could the retry wrap only message.sync(id), with boundary completion running afterwards as before (failures ignored)?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d80f0fa. Only message.sync is retried; completeGroupBoundary runs afterward with its previous best-effort error handling, and onSynced still runs. Added an app regression for a failed older-page read; it fails before the fix and passes after.


// The shared retry tests control time and verify each delay. This case exercises the complete
// app's real 7.5-second retry budget and the user-visible error after all five reads fail.
test("exhausted history reads report an error instead of silently leaving an empty transcript", async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test really waits through the full ~7.5s backoff, so it times out under plain bun test (5s default) and only passes with the package script's --timeout 30000. The retry limit and delays are already covered with fake timers in retry.test.ts / session-sync.test.ts, so I'd drop this one. The Bun.sleep(600) in the "leaving" tests below is also timing-dependent: asserting that the abort cleared the pending retry (as the util test does) would be more reliable.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d80f0fa. Removed the 7.5s exhaustion app test and both 600ms cancellation sleeps. Fake-timer helper tests now cover five attempts, all four delays, pending-timer cleanup and an in-flight cancellation. Plain bun test passes for both files; ten randomized repetitions pass too.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both earlier points are addressed. Only message.sync is retried now, and loading older pages keeps its best-effort handling. The timing-dependent app tests were replaced by fake-timer tests of the retry helper.

I re-ran the issue's reproduction: a fault proxy returns one 503 on the resumed TUI's first history read. On the base the transcript stays empty; with this change the read is retried and the history renders (screenshots below). The resume-retry and session-sync tests, the nearby TUI data and dialog tests, and tsc for packages/tui pass.

Public API unchanged since the last review: an optional signal in retry() options and an exported isTransientError in @opencode/util/retry, both backwards compatible. No new concerns.

Before

After

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TUI fails to recover session history after a transient read error

1 participant