Repository navigation
Description
Summary
With tabs.scope: "global" in cli.json, the persistent session tab strip is stored in a single shared file (~/.local/state/opencode/latest/tui/tabs.json, key global.tabs). Every newly launched TUI instance rewrites this file with only its own tabs. Any tabs opened by previous instances are silently dropped from the strip. Session data itself is untouched (this is tab-strip persistence, not data loss), but with several terminals in play it looks exactly like "all my sessions disappeared."
Environment
- opencode version: 2.2.4 (channel: latest)
- OS: Darwin 25.5.0, arm64 (macOS)
- Terminal: iTerm.app (
TERM=xterm-256color,COLORTERM=truecolor), plus tmux 3.x - Shell: /bin/zsh
- Install/channel: latest release channel, shared background service (default)
- Active plugins: none
Reproduction
- Set
~/.config/opencode/cli.jsonto:{ "tabs": { "scope": "global" } } - In terminal A (project P1), launch
opencodeand open several sessions as tabs (e.g. via the session list,<leader>l). - In terminal B (a different working directory / project P2), launch a fresh
opencodeand create a new session. - Inspect
~/.local/state/opencode/latest/tui/tabs.json: theglobal.tabsarray now contains only terminal B's session(s). - Return to terminal A: the tab strip no longer shows terminal A's previously open tabs.
Expected Behavior
Launching a new client instance should merge its tabs into the shared global strip (or otherwise preserve tabs opened by other/prior instances). A client launch must never remove tabs it did not create. Alternatively, if single-writer semantics are intended, the global scope should be per-instance with server-side merge so concurrent instances can't clobber each other.
Actual Behavior
Last launch wins. tabs.json is rewritten wholesale on every TUI launch:
{"global":{"tabs":[{"sessionID":"ses_…","title":"…"}]}}Observed in real use: an operator had ~20 sessions open as tabs across TUI instances in one terminal, then opened 5 quick throwaway instances in other directories for one-off tasks. Each launch rewrote tabs.json; afterwards the original terminal's strip showed only the new instances' tabs. The tab file's mtime updated on each launch, confirming the rewrite. No warning, no merge, no recovery path in the UI.
Additional Context
- Impact is mitigated by the fact that sessions are durable: all sessions (1,000+ in this instance) remained intact and resumable via
<leader>l(session list) oropencode -s <sessionID>from the session's directory. Zero sessions were archived or deleted — verified directly in the local database. This is purely a tab-strip persistence/UX bug, but the failure mode reads as data loss to the user. - Workaround:
"tabs": { "scope": "cwd" }keeps strips per-directory so launches elsewhere don't clobber the working set. Also feasible: a small script that readssession_v2(most recently updated top-level sessions) and relaunches each viatmux new-window … 'opencode -s <id>'. - Suggested fixes (any one would do): (a) merge-on-write instead of overwrite when writing
global.tabs; (b) per-instance tab files plus a server-side merge forscope: global; (c) prompt or toast when a launch is about to drop previously open global tabs. - Frequency: reproduced reliably in practice across multiple sequential launches; not yet tested in a clean-environment minimal repro, but the file rewrite mechanism was confirmed via before/after contents and mtime.
Activity
opencodejr commentedon Oct 31, 2026
Thanks for the detailed report. I tried to reproduce this on 2.2.4 and couldn't.
What I tried, with
cli.jsonset to{ "tabs": { "scope": "global" } }:latest/tui/tabs.jsonhad 3 entries inglobal.tabs.That matches how the code is meant to work: each tab change takes a file lock, re-reads
tabs.jsonfrom disk and adds to it rather than replacing it. Tabs are only removed when you close one or the session is deleted.My setup differs from yours in a few ways: Linux rather than macOS/iTerm/tmux, all instances started from the same directory, and a dedicated server rather than the shared background service. Any of these would help narrow it down:
tabs.jsonfrom just before and just after a launch that drops tabs (session IDs are fine to share). Please also say whether the "after" file still parses as valid JSON.opencode, or used-s,--continue,--serveror similar flags.<leader>wor the tab menu) or deleted sessions around that time.Failed to persist session tabserrors in the logs.Related, but not the same problem: #52080 and #47998 (global tabs shared across different servers), and #48635 (request for tab history and recovery).
turian commentedon Oct 31, 2026
Update: we attempted to reproduce this on the originally reported platform and could not reproduce the drop either. We're withdrawing the "every new launch rewrites the strip" characterization from the original report — it was stronger than our evidence supports.
Environment
OpenCode 2.2.4, macOS arm64, iTerm2 + tmux 3.x, default shared background service,
cli.json→{"tabs": {"scope": "global"}}.Method
Byte-for-byte snapshots of
tabs.json(content + inode/size/mtime + SHA-256 + JSON-validity check) taken immediately before and after each action against a live strip holding 16–19 open tabs; the diff is the set difference ofsessionIDs computed from the copies, not the live file.Trials and results
opencodelaunch (home screen) from a different directoryopencode -s <existing-session-id>from a different directorykill -9the TUI process owning a tab, then launch another instance elsewherectrl+c=app.exit) of a TUI owning a tab--standaloneTUIsNo condition we can construct — including unclean termination — drops previously open tabs. Merge-on-write appears solid in all tested topologies. One side observation from trial 5:
--standaloneinstances never write the strip; persistence appears to require the shared-service topology.For completeness: the sessions behind our originally "missing" tabs were never lost (verified directly in the database: all present, zero archived, zero deleted) — the incident impact was confined to the tab strip view.
What we can and cannot say about the original incident
We no longer have a before-copy, and by the time we investigated, the fleet of TUIs involved had already exited (exit times and causes unlogged), so we cannot reconstruct what the strip held beforehand or prove a drop occurred. It is possible the original report over-interpreted "sessions not in the strip" as data loss combined with an unrelated strip change.
Two code paths we'd still flag as capable of producing the observed end state (a file containing only the most recent writer's tabs, with no error surfaced):
Failed to persist session tabserrors, but a silent fallback presumably wouldn't log.If there is a debug flag, log level, or instrumented build that logs each strip write (writer PID/version, mutation type, read outcome, before/after hashes), we're willing to run it and report back. Otherwise happy to close as not-reproducible.
opencodejr commentedon Nov 3, 2026
I'm closing this for now because we couldn't reproduce it and haven't heard back. On 2.2.4 with
tabs.scope: "global", a fresh TUI launch (with the first one running or closed, and several quick launches in a row) added its tab totabs.json. All earlier tabs stayed in the file and in the first TUI's tab strip.If it happens again, please reopen or comment here with:
tabs.jsonfrom just before and just after a launch that drops tabsFailed to persist session tabserrors in the logsWith those we can take another look. Thanks for the detailed report.