Skip to content

fix(client): keep session moves made during an in-flight read - #57184

Closed
WilliamAGH wants to merge 1 commit into
anomalyco:devfrom
WilliamAGH:session-move-race
Closed

WilliamAGH wants to merge 1 commit into
anomalyco:devfrom
WilliamAGH:session-move-race

Conversation

@WilliamAGH

@WilliamAGH WilliamAGH commented Oct 19, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #57139

Type of change

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

What does this PR do?

session.sync wrote whatever the server returned, even if a session.moved event came in while the request was still out. So a stale read could undo a move, and the TUI would go back to the old (sometimes deleted) directory and show the location prompt again.

Now each running sync collects the moves that arrive while it's waiting: for its own session, or for any session in a family read, since the children aren't known until the list comes back. When the read finishes, those moves are applied on top of the response before it goes into the store. If the read fails and a move arrived while it was running, it retries once, because the failure was probably about the old directory. A second failure throws like before, so it can't loop.

I also changed the session.moved handler to reconcile the location instead of merging it, so a workspaceID from the old location doesn't survive the move.

How did you verify your code works?

I added tests to packages/client/test/solid-data.test.ts that hold the read open, emit session.moved, then let the read finish:

  • single and family reads, with and without the session cached
  • a single and a family read racing two moves: the last move wins, with no leftover workspaceID or subpath
  • a read that fails after a move: retried once, and a second failure still rejects

All 9 fail on dev and pass with this change. bun test test/solid-data.test.ts (39 pass) and bun typecheck are clean in packages/client. I also ran the packages/tui suite on dev and on this branch. The failures were timeouts on both, from a heavily loaded machine, and the one move-related TUI test failed the same way on both.

Screenshots / recordings

No UI change.

Checklist

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

Added by the opencode bot (AI-generated).

Related: #56736

Agent-Session: a435ec61-6096-4d48-82ea-d70df6783e62
@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@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.

This fixes #57139. On the base, the bot's reproduction fails all 3 cases (a read already in flight undoes the move for a single session and for a family read, and workspaceID survives a move). With this change all 3 pass. The packages/client tests (238 pass) and typecheck are clean.

Public API: unchanged. The changes are internal to the createData store, and no exported signatures or events change.

The approach looks sound: moves are collected only while a read is running, even for sessions that aren't cached yet, and applied over the response in the same synchronous batch. A second failure still rejects, and reconcile correctly drops old location keys.

A few things worth addressing:

  • The retry also fires on moves of unrelated sessions during a family read (see the inline comment).
  • The new tests add about 210 lines for 9 cases. Most of that is copied setup, plus fetch stubs that branch on children, moved, cached and request counts, and the concurrent-snapshot test needs a stub that throws on a third request. That makes it hard to see what each case checks. Please cut them down to the core cases: a single read and a family read held open across a move, the workspaceID and subpath cleanup, and one retry case, using a small shared helper for the data/bus/held-fetch setup.
  • #56886 changes the same session.sync function to stop stale reads restoring deleted sessions. The two will conflict, and the 'events that arrive during a pending read' tracking may be better shared than added twice for different events.

Related: #56736

return
case "session.moved": {
for (const read of sessionReads) {
if (read.sessionID !== event.data.sessionID && !read.children) continue

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.

For a family read, read.revision goes up for a session.moved on any session, related or not, so a real failure gets retried whenever some unrelated session moves during the read. Collecting every move makes sense, since children aren't known until the list returns. But the retry check could look only at moves for read.sessionID, or at moves for sessions this read actually asked about, rather than any movement at all.

@WilliamAGH WilliamAGH closed this by deleting the head repository Oct 20, 2026
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: a session move gets reverted by a session fetch that was already in flight

1 participant