Skip to content

feat: session snapshot history + programa snapshot restore - #228

Merged
arzafran merged 3 commits into
mainfrom
feat/snapshot-restore
Jul 31, 2026
Merged

feat: session snapshot history + programa snapshot restore#228
arzafran merged 3 commits into
mainfrom
feat/snapshot-restore

Conversation

@GFier

@GFier GFier commented Jul 31, 2026

Copy link
Copy Markdown
Member

Closes #227

What this does

After a reboot or crash, programa can silently lose the whole window/workspace layout: one file holds it, and the next launch overwrites that file with no backup. This adds a rolling history of the session file, records whether the previous run quit cleanly, and adds programa snapshot list/restore to bring a lost layout back as new windows. It also adds programa worktree open --all to reopen every worktree of a repo in one call.

Summary

  • SessionPersistenceStore.rotateIntoHistory(): on every launch, before load and before any save can clobber, copy session-<bundleId>.json into session-history/<yyyyMMdd-HHmmss>-<bundleId>.json. Keep the 10 newest, skip byte-identical duplicates, best-effort throughout (never blocks launch).
  • AppSessionSnapshot.cleanShutdown: Bool? (optional, so old files still decode). Normal saves write false; the applicationShouldTerminate / applicationWillTerminate / willPowerOffNotification saves write true.
  • New v2 methods snapshot.list / snapshot.restore in Sources/TerminalController+Snapshot.swift, mirroring the worktree.* handler shape. Restore creates new windows via the same createMainWindow(sessionWindowSnapshot:) path startup restore uses; it never closes or mutates existing windows. Wire keys are snake_case, matching the v2 convention.
  • CLI: programa snapshot list [--json], programa snapshot restore [<id>|latest], and worktree open --all (mutually exclusive with a positional target and with --focus; reuses the existing idempotent worktree.list + worktree.open RPCs, no app-side changes).
  • Docs: docs/plans/snapshot-restore.md, parity-table rows in docs/v2-api-migration.md, CHANGELOG entries.

Test plan

  • CI unit tests: rotation copy/dedupe/prune-to-10, legacy decode without cleanShutdown, flag round-trip, corrupt-data decode (8 new tests in programaTests/SessionPersistenceTests.swift)
  • CI socket tests: snapshot.list shape + snapshot.restore unknown-id rejection, worktree open --all flag validation (2 new files in tests_v2/)
  • programa and programa-cli schemes build clean locally

GFier and others added 3 commits July 31, 2026 11:05
Review of this PR surfaced two defects in the new history path.

rotateIntoHistory deleted an existing archive before copying its
replacement. Archive filenames carry the live file's modification date at
second resolution, so two launches can collide on one name, and a copy
that failed after the delete (full disk) left neither the old archive nor
the new one -- the exact loss this file exists to prevent. It now stages
the copy beside the destination and swaps it into place.

snapshot.restore reconstructed every window in the archive, while startup
restore clamps to maxWindowsPerSnapshot. A corrupt or hand-edited history
file could freeze the app on the one command you reach for when
recovering. Both paths now go through SessionPersistenceStore.windowsToRestore.

Tests cover the clamp and the same-filename rotation (replace, don't
duplicate; no staging files left behind). The failure-injection half of
the swap -- copy failing mid-rotation -- has no seam to test through and
is not covered.
@arzafran
arzafran merged commit f4545aa into main Jul 31, 2026
10 checks passed
@arzafran
arzafran deleted the feat/snapshot-restore branch July 31, 2026 13:55
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.

Session layout is unrecoverable after a cold boot

2 participants