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
Pinned sessions can be lost when multiple TUI instances write session.json #44736
Each TUI reads the global session.json pin list once and keeps a private in-memory copy. Pinning, unpinning, or handling session.deleted rewrites the entire file without locking, rereading, merging, or serializing writes. A stale TUI can therefore erase pins added by another TUI.
Reproduction
Start TUI A and TUI B with the same pin list.
In B, pin a session. B writes the updated list.
In stale A, pin/unpin another session or delete a session.
Restart OpenCode.
The pin added in B is gone because A replaced the complete file with its stale snapshot.
Expected
Concurrent TUI instances should not erase unrelated pin updates.
Cause
createSession() loads the file once and each save() passes the full in-memory array to a fire-and-forget writeJsonAtomic() call:
Atomic rename prevents partial files but does not prevent stale writers. There is also no file watcher or close-time merge. Closing a TUI alone does not save; the last completed save wins.
I verified that the relevant persistence code is identical in v1.18.18, v1.18.19, v1.18.20, v1.18.21, and current dev.
Environment
macOS
Multiple OpenCode TUIs in WebStorm and system terminals
Confirmed the analysis: createSession() in packages/tui/src/context/local.tsx reads session.json once into sessionStore.pinned, and save() hands the whole in-memory array to a fire-and-forget writeJsonAtomic(). Nothing rereads, locks, or serializes, so a stale instance replaces the file wholesale.
I reproduced the loss directly against the current save() semantics — two writers starting from an empty snapshot, one pinning a and one pinning b, leaves the file as ["b"].
Planned fix, following the pattern context/kv.tsx already uses for kv.json:
express each change as an intent (sessionID + pin/unpin) instead of a snapshot, and apply it to the file's current contents inside Flock.withLock
chain writes off the initial read so ordering is guaranteed and the pending flag goes away
keep the optimistic in-memory update so the UI still responds immediately, then reconcile the store with whatever the merged file ended up containing
Worth noting kv.tsx locks but still writes its own snapshot, so kv.json has the same stale-writer shape. I'm leaving that alone here to keep the change focused — happy to follow up if you want it covered too.
PR shortly, with regression tests for the concurrent case.
Activity
rareboe commentedon Aug 26, 2026
I'd like to take this one.
Confirmed the analysis:
createSession()inpackages/tui/src/context/local.tsxreadssession.jsononce intosessionStore.pinned, andsave()hands the whole in-memory array to a fire-and-forgetwriteJsonAtomic(). Nothing rereads, locks, or serializes, so a stale instance replaces the file wholesale.I reproduced the loss directly against the current
save()semantics — two writers starting from an empty snapshot, one pinningaand one pinningb, leaves the file as["b"].Planned fix, following the pattern
context/kv.tsxalready uses forkv.json:sessionID+ pin/unpin) instead of a snapshot, and apply it to the file's current contents insideFlock.withLockpendingflag goes awayWorth noting
kv.tsxlocks but still writes its own snapshot, sokv.jsonhas the same stale-writer shape. I'm leaving that alone here to keep the change focused — happy to follow up if you want it covered too.PR shortly, with regression tests for the concurrent case.