Skip to content

fix: persist local panels in disconnected remote workspaces - #268

Merged
arzafran merged 2 commits into
mainfrom
fix/remote-disconnect-loses-sessions
Aug 6, 2026
Merged

fix: persist local panels in disconnected remote workspaces#268
arzafran merged 2 commits into
mainfrom
fix/remote-disconnect-loses-sessions

Conversation

@arzafran

@arzafran arzafran commented Aug 6, 2026

Copy link
Copy Markdown
Member

What this does

Disconnect a remote workspace from the sidebar, keep using it with local shells, and every terminal in it silently vanished from session persistence -- gone at the next restart, with no warning. Three live local terminals were lost this way in a real incident on release 0.4.218: the workspace was absent from both the autosave and the clean-quit snapshot, and there was nothing in the diagnostics log to explain why.

The root cause: remoteConfiguration intentionally survives a user-initiated disconnect (the sidebar's disconnect action passes clearConfiguration: false on purpose, so Reconnect keeps working), but the session-snapshot filter excluded any workspace with remoteConfiguration != nil -- stale config presence, not whether the workspace is actually still live remote right now.

Summary

  • Added Workspace.isLiveRemoteWorkspace (isRemoteWorkspace && remoteConnectionState != .disconnected) and switched TabManager.sessionSnapshot to it, so a disconnected-but-configured workspace persists its local panels like any other workspace.
    • Considered clearing remoteConfiguration on disconnect instead (simplest fix): rejected -- reconnectRemoteConnection() and the socket workspace.remote_reconnect path both guard let remoteConfiguration before reconnecting, so clearing it on disconnect would permanently break Reconnect.
    • Considered filtering at the panel level (a connected remote workspace can genuinely contain local panels too, e.g. a split created without an auto-connect startup command) as the most correct long-term shape, but it's a materially larger change to the layout/panel snapshot plumbing. Scoped this fix to the reported incident (a fully disconnected workspace) and called the mixed-panel-while-connected gap out below rather than guessing at a bigger restructure.
  • Every workspace still skipped for being genuinely live-remote now logs session.snapshot skipped workspace=<id> reason=remote panels=<n> to the always-on diagnostics log (~/Library/Logs/Programa/diagnostics.log), so this class of gap is never silent again.
  • TerminalController+Telemetry's TTY-report/port-kick routing had the same stale-flag bug (a disconnected-but-configured workspace's local panels stopped getting local port-scanning/dev-server detection, since telemetry kept routing to an inert remote path) -- switched those 4 branches to isLiveRemoteWorkspace too.
  • Left other isRemoteWorkspace call sites (sidebar Reconnect/Disconnect menu state, browser proxy website-data-store routing, git-metadata-polling exclusion) untouched -- those gate "has a remote destination configured" facts, not the live-session distinction this bug is about.

Known residual gap (disclosed, not fixed here): a workspace that is still actively connected but also contains local panels (possible when a split is created without an auto-connect startup command) still has those local panels excluded from persistence, same as before. It's no longer silent -- the new diagnostics line fires for it too -- but a full fix needs panel-level filtering in the layout snapshot, which is a larger, separate change.

Test Plan

  • xcodebuild -project GhosttyTabs.xcodeproj -scheme programa -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/programa-remotefix build -> BUILD SUCCEEDED
  • Two-commit red/green: commit 1 (test only) fails with 3 assertion failures against unmodified source; commit 2 (fix) passes
  • -only-testing:programaTests/WorkspaceRemoteConnectionTests -> 41/41 passed
  • -only-testing:programaTests/TabManagerSessionSnapshotTests -> 3/3 passed (existing exclusion test updated to simulate an actually-connected workspace, since it previously relied on the buggy "config present = excluded" behavior)
  • -only-testing:programaTests/SessionPersistenceTests -> 62/62 passed
  • -only-testing:programaTests/TerminalControllerSocketSecurityTests -> 11/11 passed

A workspace the user disconnects from its remote host but keeps using with
local shells was silently dropped from every session snapshot for the rest
of its life -- three live terminals lost on relaunch with no warning
(release 0.4.218 incident).

remoteConfiguration intentionally survives a user-initiated disconnect (the
default and the sidebar's disconnect action both pass clearConfiguration:
false) so reconnectRemoteConnection() has something to reconnect to. That
made isRemoteWorkspace (config presence) a bad proxy for "this workspace's
panels currently live on a remote host" -- a disconnected-but-configured
workspace is a local workspace again for every practical purpose. Added
isLiveRemoteWorkspace (config present AND connection state != disconnected)
and switched session persistence, and the matching telemetry-routing
branches in TerminalController+Telemetry, to it.

Also logs a session.snapshot diagnostics line for every workspace still
skipped (a genuinely live remote workspace), so the gap can never be
silent again.
@arzafran

arzafran commented Aug 6, 2026

Copy link
Copy Markdown
Member Author

Residual edge worth tracking separately (not blocking): isLiveRemoteWorkspace treats .error as live, so a workspace whose remote connection errored and was never explicitly disconnected stays excluded from snapshots — the same data-loss shape as this bug, with a narrower trigger. It now at least logs via the new session.snapshot line instead of being silent. Worth deciding whether .error should decay to .disconnected after a timeout, or be treated as not-live for persistence purposes.

@arzafran
arzafran merged commit 33a7383 into main Aug 6, 2026
12 of 13 checks passed
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.

1 participant