Skip to content

fix(tui): stop stale instances from erasing pinned sessions - #45117

Open
rareboe wants to merge 1 commit into
anomalyco:devfrom
rareboe:fix/44736-pin-race
Open

fix(tui): stop stale instances from erasing pinned sessions#45117
rareboe wants to merge 1 commit into
anomalyco:devfrom
rareboe:fix/44736-pin-race

Conversation

@rareboe

@rareboe rareboe commented Aug 26, 2026

Copy link
Copy Markdown

Issue for this PR

Closes #44736

Type of change

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

What does this PR do?

createSession() in packages/tui/src/context/local.tsx read the global session.json pin list once at startup and kept a private in-memory copy. togglePin, prune and the session.deleted handler all rewrote the whole file from that copy through a fire-and-forget writeJsonAtomic() — no reread, no lock, no ordering. An instance holding a stale snapshot therefore replaced pins another instance had added. writeJsonAtomic prevents torn files, not stale writers.

Reproduced against the old semantics: two writers starting from an empty list, one pinning a and one pinning b, leave the file as ["b"].

The fix changes what gets persisted. Instead of writing a snapshot, each change is now an intent — a session ID plus pin/unpin — applied to the file's current contents inside Flock.withLock, the same lock context/kv.tsx already uses for kv.json. Writes chain off the initial read, so they can never run before the list has loaded and the pending flag is no longer needed. The in-memory store is still updated immediately so the UI doesn't wait on the write, then reconciled with whatever the merged file actually ended up containing.

parsePinned, setPinned and persistPinned are exported so the behaviour is testable directly, matching how parseModel / recentModels are already handled in this module.

Worth flagging: kv.tsx takes the lock but still writes its own snapshot, so kv.json has the same stale-writer shape. I left it alone to keep this focused — happy to follow up if you want it covered.

How did you verify your code works?

Added tests to packages/tui/test/context/local.test.ts. They use a temp directory for both the file and the lock root, so nothing touches real state:

  • three concurrent persistPinned calls all survive — this is the regression; under the old snapshot write the same shape collapses to a single pin
  • a pin written by another instance survives a later write from an instance that never saw it
  • unpinning removes only the target
  • parsePinned drops non-string entries and bad shapes; setPinned doesn't duplicate

I also verified the old behaviour actually fails, by reimplementing the previous snapshot write in a scratch test:

OLD RESULT: ["b"]     # pin "a" lost
$ bun test                     # packages/tui
 198 pass, 1 skip, 0 fail

$ bun typecheck
(clean)

Screenshots / recordings

No visible UI change — the pin list renders the same, it just stops losing entries.

Checklist

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

Every TUI instance read the global session.json pin list once at startup and
kept a private copy. Pinning, unpinning and session.deleted all rewrote the
whole file from that copy with an unserialized, fire-and-forget write, so an
instance holding a stale snapshot replaced pins added by another instance.

Two writers starting from an empty list, one pinning "a" and one pinning "b",
end up with just ["b"].

Apply each change as an intent against the file's current contents inside
Flock.withLock, the same lock context/kv.tsx already uses, and chain writes off
the initial read so they cannot run before it completes. The in-memory update
still happens immediately so the UI does not wait on the write, and the store is
reconciled with the merged result once it lands.
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.

Pinned sessions can be lost when multiple TUI instances write session.json

1 participant