Skip to content

tui: "Restart" after /update exits to the shell instead of restarting OpenCode #56103

Open

Description

@Addin

Description

After installing an update from the TUI update dialog (the /update command or the update notification), the dialog reads "Update successful! A restart is required. Any active sessions will be resumed automatically." and offers a Restart button. Pressing Restart does not restart OpenCode: the TUI closes and drops back to the shell prompt of the terminal where OpenCode was running. Nothing is relaunched, the session is not resumed, and no message is shown — opencode has to be started manually to get the new version. This happens every time an update is installed this way.

Expected: Restart relaunches the updated OpenCode in the same terminal and resumes the session, as the dialog states. If exiting on purpose is intended (see #47364), the dialog should not promise a restart with automatic session resume and should tell the user to run opencode again.

Plugins

None.

OpenCode version

2.0.31 (observed while updating 2.0.30 → 2.0.31; curl install at ~/.opencode/bin/opencode, channel latest)

Steps to reproduce

  1. Run OpenCode 2.0.30 in a terminal (Ghostty + bash).
  2. When 2.0.31 is published, open the update dialog (notification or /update) and press Update.
  3. The dialog shows "Update successful! A restart is required. Any active sessions will be resumed automatically." with the Skip / Restart buttons.
  4. Press Restart.

The TUI exits to the shell. No new process is spawned; you must run opencode yourself.

Screenshot and/or share link

No response

Operating System

Pop!_OS 24.04 LTS (kernel 7.1.5, x86_64)

Terminal

Ghostty

Additional context

Log evidence from the machine (opencode.log):

  • message="updated OpenCode" from=2.0.30 to=2.0.31 method=curl at 14:09:36 UTC — after this, the TUI session ends and no process is started by the Restart action.
  • message="cli starting" version=2.0.31 args=[] at 14:10:37 UTC — the manual relaunch a minute later, which then replaces the version-mismatched background service normally.

Code path: restart() in packages/tui/src/context/update-notification.tsx only calls the TUI exit; the dialog copy is in packages/tui/src/component/dialog-update.tsx. PR #47364 ("Exit normally after an update; the user reopens OpenCode") removed the self-relaunch wrapper, so the button and copy no longer match the behavior.

Activity

  1. opencode-agent commented on Oct 16, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report and the log lines. I couldn't fully reproduce this in our sandbox. Our test harness runs OpenCode in an isolated environment, so /update stopped at "Could not detect the installation method" and never got to the Restart button.

    The code agrees with what you describe, though. In 2.0.30 and on the current dev branch, the Restart action in packages/tui/src/context/update-notification.tsx only exits the TUI and does not start a new process. Meanwhile the dialog in packages/tui/src/component/dialog-update.tsx still says a restart is required and that sessions will be resumed automatically. That matches #47364, which removed the self-relaunch on purpose without changing the dialog text.

    This needs a maintainer to check it with a real curl install. Until it's resolved, running opencode again after pressing Restart starts the new version, as you found.

    Related (not duplicates):

  2. jkemp814 commented on Oct 21, 2026

    @jkemp814

    Adding a side-effect that makes this more than a UX annoyance: the unclean exit appears to damage plugin SQLite databases.

    Observed on 2 of 3 machines in my fleet (both Silverblue hosts, opencode-mem plugin active) after using this Restart button on 2.0.32 → 2.1.2:

    • ~/.opencode-mem/data/*.db left with 0-byte -wal files and stale -tshm files
    • PRAGMA integrity_check failures: btree corruption, out-of-order rowids, invalid page numbers in user-prompts.db, user-profiles.db, and project shard DBs
    • opencode-mem then failed to initialize with I/O error: short read on WAL frame ... expected 4096 bytes, got 0 and Corrupt database: Payload too small for indicated header size

    The third machine (HP-Z840) was unaffected, and pre-existing long-running server processes on all three had logged unrelated plugin load warnings around the same window, so this is correlation rather than proof. But the failure mode fits: the Restart path exits without letting SQLite checkpoint/close cleanly, and a hard kill mid-write is exactly how you get truncated WALs.

    Salvage was possible (sqlite Connection.backup + row-by-row recovery for the worst files; backups kept), but ideally the exit path should either actually restart as the dialog promises, or at minimum close/checkpoint plugin DBs before quitting.

    Happy to provide the full recovery log or the corrupt DB files if useful for debugging.

  3. jkemp814 commented on Oct 26, 2026

    @jkemp814

    Confirming this still repros on 2.1.4 (curl install, ~/.opencode/bin/opencode, channel latest) and adding a second occurrence for the DB damage I reported above.

    Restart is still exit-only. After the update dialog's Restart press, the TUI closes, the shell prompt returns, nothing relaunches, and no session is resumed. Reproduced twice on two different machines (HP-Z840 and Dell-7740, both Fedora Silverblue). Same failure signature as the original report, so #47364's removal of the self-relaunch is still not reflected in the copy or the button.

    On the wording: "reload" and "restart" are not the same operation. If relaunching is intentionally gone, the dialog should say "Shut down — run opencode again to start the new version" and not "A restart is required... any active sessions will be resumed automatically". Right now the UI promises the operation it no longer performs.

    DB damage recurrence (2026-10-25, opencode-mem, turso/libsql):

    • Signature: <db>-wal truncated to 0 bytes next to a stale <db>-tshm (86,016 bytes). Every later open fails with I/O error: short read on WAL frame at offset N: expected 4096 bytes, got 0. Auto-capture retries 3x, then user-profile learning and idle processing abort on the same error.
    • Timeline on HP-Z840: user-profiles.db last successful write 2026-10-20 22:59, first failure 2026-10-21 00:00:43. Second event left ai-sessions.db, metadata.db and skills.db all stamped 2026-10-22 06:06:14.
    • New detail vs. my earlier comment: this time the main .db files passed PRAGMA integrity_check — the damage was sidecar-only. The btree/rowid failures I reported earlier were likely a worse instance of the same truncation rather than a separate defect.
    • One detection wrinkle: an empty -wal beside a -tshm is also the normal shape of an idle DB (turso recreates both on demand), and neither sqlite3 nor python's sqlite3 reproduces turso's own shm validation. So this can only be repaired, not cleanly detected: verify the main DB, delete the sidecars, let turso rebuild. That heals in place, no restart needed, no data loss.
    • Plugin DBs live in the shared server (opencode serve --service), not in the TUI, so a hard stop takes every plugin database down at the same moment.

    Ask: whatever the exit path does, plugins need a chance to close or checkpoint SQLite handles before the process dies — a SIGTERM with a grace period, or an awaited plugin shutdown hook. That converts this from data damage into a clean shutdown regardless of whether Restart ever relaunches.

    Happy to run diagnostics against my curl install — the exit path is reproducible on demand here, which is what the earlier reply said was missing for triage.

  4. jkemp814 commented on Oct 26, 2026

    @jkemp814

    Follow-up with the host-side half, since the earlier reply said triage needed a real curl install. Measured on 2.1.4 (~/.opencode/bin/opencode, channel latest), Fedora Silverblue.

    The restart path, as executed: service restart = St.stop() then St.ensure(). St.stop() runs the PTY handoff, then:

    • SIGTERM to the opencode serve --service pid;
    • liveness poll (kill(pid, 0)) every stopPollInterval: 50 ms for stopPollAttempts: 100 — 5 s total;
    • unconditional SIGKILL, then another liveness wait.

    So the dialog's "restart" is: kill the shared server with a 5 s tolerance, then try to start a new one. On this install the start half doesn't happen (TUI exits to the shell), which is the behaviour already reported above.

    Why this damages plugin state. Plugins live in the shared server, not the TUI — the same process that gets SIGTERM/SIGKILL. The plugin API declares Hooks.dispose?: () => Promise<void>, but I could not find any call site that invokes a server plugin's dispose on shutdown; every .dispose?.() hit in the bundle is TUI/renderable-side. What does exist is the runtime's own process.on("SIGTERM") → interrupt the main fiber → teardown → process.exit(), which can cut an async plugin cleanup short. A plugin's only reliable chance is therefore its own SIGTERM handler, racing the 5 s budget.

    Concrete consequence (turso/libsql databases, opencode-mem):

    state metadata.db WAL user-prompts.db WAL
    before shutdown 74,192 45,352
    after shutdown, no process alive 0 0

    That is the healthy direction — with a pre-close PRAGMA wal_checkpoint(TRUNCATE) in the plugin, the WAL is flushed on the way out and nothing is left half-written. Without it, the same databases have twice been left with a 0-byte -wal beside a stale -tshm, after which every open fails with I/O error: short read on WAL frame at offset N: expected 4096 bytes, got 0 and the whole memory subsystem (tools, capture, web server) is gated off.

    Plugin-side fix, if useful as a reference: tickernelz/opencode-mem#440 — checkpoint before database.close(), bounded so it cannot outlast a shutdown window.

    Host-side ask (this is the part a plugin can't fix): whichever path tears the server down should either call plugin dispose and await it, or SIGTERM with a grace period longer than the cleanup needs, rather than a fixed 5 s followed by SIGKILL. And separately: if relaunching really is gone per #47364, the button and copy should say "Shut down — run opencode again", because "Restart" with "sessions will be resumed automatically" is currently a promise the code doesn't keep.

    Happy to test a diagnostic build — the exit path is reproducible on demand here.

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