Skip to content

tui: with tabs.scope=global, every new CLI launch rewrites the shared tab strip and silently drops previously open tabs #60859

Closed as not planned

Description

@turian

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

  1. Set ~/.config/opencode/cli.json to:
    { "tabs": { "scope": "global" } }
  2. In terminal A (project P1), launch opencode and open several sessions as tabs (e.g. via the session list, <leader>l).
  3. In terminal B (a different working directory / project P2), launch a fresh opencode and create a new session.
  4. Inspect ~/.local/state/opencode/latest/tui/tabs.json: the global.tabs array now contains only terminal B's session(s).
  5. 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) or opencode -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 reads session_v2 (most recently updated top-level sessions) and relaunches each via tmux 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 for scope: 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

  1. opencodejr commented on Oct 31, 2026

    @opencodejr

    Thanks for the detailed report. I tried to reproduce this on 2.2.4 and couldn't.

    What I tried, with cli.json set to { "tabs": { "scope": "global" } }:

    1. Opened three sessions as tabs in one TUI. latest/tui/tabs.json had 3 entries in global.tabs.
    2. Started a second, fresh TUI while the first was still running and created a session. The file then had 4 entries, and the first TUI's tabs were still there.
    3. Closed that one, then started three throwaway TUIs one after another, each creating a session and exiting. The file ended with all 7 tabs, and the first TUI's strip still showed its own tabs plus the new ones.

    That matches how the code is meant to work: each tab change takes a file lock, re-reads tabs.json from 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:

    • A copy of tabs.json from 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.
    • Whether the first terminal was still running, or had exited, when the other instances were launched.
    • Whether those instances were plain opencode, or used -s, --continue, --server or similar flags.
    • Whether you closed tabs in any instance (<leader>w or the tab menu) or deleted sessions around that time.
    • Any Failed to persist session tabs errors 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).

  2. turian commented on Oct 31, 2026

    @turian
    Author

    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 of sessionIDs computed from the copies, not the live file.

    Trials and results

    # Action Added Removed
    1 plain opencode launch (home screen) from a different directory — none
    2 opencode -s <existing-session-id> from a different directory the opened session none
    3 kill -9 the TUI process owning a tab, then launch another instance elsewhere — none — the killed instance's tab persisted
    4 graceful exit (ctrl+c = app.exit) of a TUI owning a tab — none — the tab persisted
    5 isolated service + --standalone TUIs strip never written at all —

    No 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: --standalone instances 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):

    1. Read-failure fallback: if reading the tab store fails (permissions, truncation, partially-written file), a fallback to initial/empty state followed by a normal merge-write would produce exactly that file silently. Our logs contained no Failed to persist session tabs errors, but a silent fallback presumably wouldn't log.
    2. Stale-snapshot replace on reopen: reopening a closed tab appearing to replace the list from a previously computed client snapshot would drop tabs added in between.

    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.

  3. opencodejr commented on Nov 3, 2026

    @opencodejr

    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 to tabs.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:

    • a copy of tabs.json from just before and just after a launch that drops tabs
    • whether the first terminal was still running at the time
    • the exact launch command and flags
    • any Failed to persist session tabs errors in the logs

    With those we can take another look. Thanks for the detailed report.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions