Skip to content

Session restore stress: recover from corrupt snapshot via -previous backup - #5914

Merged
lawrencecchen merged 9 commits into
mainfrom
task-agent-session-restore-stress
Jun 12, 2026
Merged

lawrencecchen merged 9 commits into
mainfrom
task-agent-session-restore-stress

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Stress exercise: spawn many tracked agent sessions (claude, codex, opencode), kill cmux in different ways (clean quit, repeat quit, SIGKILL, snapshot corruption), reopen, and verify sessions restore or stay recoverable.
  • Bug found and fixed: a corrupt primary session snapshot at startup made cmux silently start fresh AND delete session-<bundle>-previous.json (the restore-session backup), so the user's workspaces and agent sessions were unrecoverable. SessionPersistenceStore now distinguishes a missing snapshot (clean state) from an unusable one (unreadable data, decode failure, schema drift, anomalous empty window list): when the primary is unusable the backup is preserved and startup restore falls back to it, so workspaces and tracked agent sessions come back automatically.
  • New end-to-end stress harness tests/test_session_restore_stress_kill_cycles.py: six Claude/Codex/OpenCode sessions across six workspaces, then clean relaunch, second relaunch, relaunch after SIGKILL (autosave path), and relaunch with a corrupted primary snapshot (backup recovery path). Fake agents stay running so every snapshot records the agent as live.
  • New unit regression tests in SessionPersistenceTests (two-commit structure, commit 1 red / commit 2 green): corrupt primary preserves the backup, startup load recovers from the backup, missing primary still clears the stale backup and does not resurrect it.
  • Repaired silently-rotted claude coverage in tests/test_session_relaunch_resumes_agent_sessions.py (not run in CI): claude hook records are only restorable when their transcript exists (hookRecordIsRestorable), and claude resume resolves the binary through the cmux claude wrapper, not the captured executable. Both harnesses now write transcriptPath, point CMUX_CUSTOM_CLAUDE_PATH at the fake binary, and match resume markers as order-agnostic tokens.

Testing

  • Baseline (pre-fix build): corrupt-snapshot phase failed as predicted: workspaces=1 after relaunch and the -previous backup deleted.
  • Post-fix tagged build: tests/test_session_restore_stress_kill_cycles.py PASS (all phases: clean relaunch x2, SIGKILL relaunch, corrupt-snapshot recovery with backup preserved and cmux restore-session reopening the backed-up session in a new window, all 6 sessions resumed including claude through the wrapper).
  • tests/test_session_relaunch_resumes_agent_sessions.py PASS post-repair (was failing on claude against unmodified main).
  • cmuxTests/SessionPersistenceTests on AWS M4 Pro (macOS 15.7.4): 135 passed, all 4 new tests passed; red/green proven empirically (the two corrupt-recovery tests fail at the test-only commit f15bf28, pass at head). 4 pre-existing testHermesAgentHookSurfaceResume* failures on that box are environment-dependent (subrouter bootstrap state) and untouched by this change; the CI tests job passes.

Issues

  • Task: stress-test agent session lifecycle (spawn/kill/reopen/update) so sessions restore properly or stay recoverable; requested directly by Lawrence, no tracker issue.

Note

Medium Risk
Changes core session persistence and startup restore paths where corrupt snapshots previously caused silent data loss; behavior is well-covered by new unit and stress tests but affects all users on launch.

Overview
Fixes session restore when the primary session-<bundle>.json is corrupt or unreadable: startup no longer treats that like a clean slate and stops wiping the session-<bundle>-previous.json backup used by cmux restore-session.

SessionPersistenceStore adds SnapshotLoadOutcome (loaded / missing / unusable) via loadOutcome. syncManualRestoreSnapshotCache now copies a good primary to the backup, clears the backup only when the primary is truly missing, and leaves the backup alone when the primary exists but is unusable. loadStartupSnapshot is used at launch (replacing load() in AppDelegate) and falls back to the -previous file when the primary is unusable.

Adds SessionPersistenceTests for corrupt-primary backup preservation, startup fallback, and missing-primary behavior. Adds end-to-end tests/test_session_restore_stress_kill_cycles.py (relaunch, SIGKILL, corrupt primary). Updates test_session_relaunch_resumes_agent_sessions.py so Claude resume checks use transcriptPath, CMUX_CUSTOM_CLAUDE_PATH, and order-agnostic scrollback markers.

Reviewed by Cursor Bugbot for commit 09904ad. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Bug Fixes

    • Session restoration now intelligently falls back to backup sessions when primary sessions are corrupted or missing
    • Enhanced startup logic to better detect and handle unusable or invalid session files
    • Improved session recovery mechanisms during app crashes and forced restarts
  • Tests

    • Added stress testing to validate session persistence and recovery across repeated app restart cycles and forced termination scenarios

…re backup

A corrupt primary session snapshot at startup currently makes
syncManualRestoreSnapshotCache delete session-<bundle>-previous.json (the
restore-session backup) and the app silently starts fresh. These tests pin
the desired behavior: keep the backup and recover startup restore from it.

Includes tests/test_session_restore_stress_kill_cycles.py, an end-to-end
stress harness covering repeated clean relaunches, SIGKILL relaunch, and
corrupt-snapshot recovery for tracked Claude/Codex/OpenCode sessions.

SessionPersistenceStore.syncManualRestoreSnapshotCache and the new
loadStartupSnapshot gain injectable bundle/app-support parameters
(behavior unchanged in this commit) so the regression tests can run
against temp paths.
…apshot is corrupt

SessionPersistenceStore.load() treated a corrupt primary snapshot the same
as a missing one: startup silently began a fresh session and
syncManualRestoreSnapshotCache deleted session-<bundle>-previous.json, the
only remaining copy of the user's workspaces, so restore-session could not
recover anything either.

loadOutcome now distinguishes a missing snapshot (clean state) from an
unusable one (unreadable data, decode failure, schema drift, anomalous
empty window list). When the primary is unusable, the backup is preserved
and startup restore falls back to it, so workspaces and tracked agent
sessions come back automatically.

Also captures launch environment in the stress harness hook entries so
fake-claude resume wins the PATH lookup (claude resume intentionally
routes through the wrapper shim / PATH instead of the captured executable).
@vercel

vercel Bot commented Jun 11, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 12, 2026 3:22am
cmux-staging Building Building Preview, Comment Jun 12, 2026 3:22am

@coderabbitai

coderabbitai Bot commented Jun 11, 2026 •

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2d78dd8a-4b09-403f-9989-f8079e1c91d4

📥 Commits

Reviewing files that changed from the base of the PR and between aa06b13 and 09904ad.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (1)
  • Sources/AppDelegate.swift

📝 Walkthrough

Walkthrough

Centralizes snapshot-file validation into SnapshotLoadOutcome, updates cache sync and startup-loading to prefer a valid primary and fall back to a previous backup only when appropriate, switches AppDelegate to the new startup loader, and adds unit and integration tests for corruption, missing-primary, and multi-phase persistence.

Changes

Session restoration with backup fallback and recovery

Layer / File(s) Summary
Snapshot load outcome classification
Sources/SessionPersistence.swift
New SnapshotLoadOutcome enum and loadOutcome(fileURL:) helper classify snapshot states (missing, unusable, successfully loaded) and centralize file existence, decoding, version, and emptiness checks.
Manual cache sync and startup snapshot loading
Sources/SessionPersistence.swift
syncManualRestoreSnapshotCache(bundleIdentifier:appSupportDirectory:) copies valid primary snapshots to backup, deletes backup when primary is missing, and preserves backup when primary is unusable. loadStartupSnapshot(bundleIdentifier:appSupportDirectory:) loads the primary snapshot when valid, returns nil when missing, or falls back to reopening the previous session when primary is unusable.
AppDelegate startup snapshot integration
Sources/AppDelegate.swift
Updates prepareStartupSessionSnapshotIfNeeded() to call loadStartupSnapshot() instead of load() for startup restoration.
Unit tests for backup cache and fallback behavior
cmuxTests/SessionPersistenceTests.swift
SnapshotBackupFixture plus four tests validate backup preservation when primary is corrupt, backup removal when primary is missing, fallback recovery from corrupted primary, and nil return on clean start when primary is missing.
Relaunch test harness updates for Claude transcript-aware restore
tests/test_session_relaunch_resumes_agent_sessions.py
Hook-state writing accepts an optional transcript_path and writes transcriptPath for Claude; test injects a Claude transcript file, sets CMUX_CUSTOM_CLAUDE_PATH, and checks resume via order-agnostic token matching helper.
Integration stress test for session persistence
tests/test_session_restore_stress_kill_cycles.py
End-to-end Python stress test seeds hook-state, orchestrates clean/quits and SIGKILL cycles, corrupts primary to force backup recovery, and verifies resume markers across phases, reporting aggregated PASS/FAIL.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I nibble bytes where snapshots keep,
When primaries falter, backups peep,
On startup's hop I check with care,
Primary first — then trusty spare,
Sessions leap home, snug in my lair.

🚥 Pre-merge checks | ✅ 20 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (20 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately and concisely describes the main change: recovering from corrupt session snapshots using a backup file, which directly addresses the core bug fix in this changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed AppDelegate change is a synchronous @MainActor call to nonisolated SessionPersistenceStore helpers; SessionPersistence.swift adds no @MainActor types, async, or shared mutable Sendable references.
Cmux Swift Blocking Runtime ✅ Passed Reviewed AppDelegate.prepareStartupSessionSnapshotIfNeeded() and SessionPersistenceStore changes: no DispatchSemaphore/DispatchGroup.wait/Task.sleep/polling/DispatchQueue.main.sync/NSLock or other...
Cmux Expensive Synchronous Load ✅ Passed PR #5914’s Swift changes (AppDelegate.swift/SessionPersistence.swift) replace load() with loadStartupSnapshot(), and contain no RestorableAgentSessionIndex.load/sysctl heavy index loader added or m...
Cmux Cache Substitution Correctness ✅ Passed PASS: SessionPersistenceStore reads snapshot files fresh via loadOutcome (missing/unusable/loaded); loadStartupSnapshot returns nil on missing and falls back only on unusable; syncManualRestoreSnap...
Cmux No Hacky Sleeps ✅ Passed runtime-no-hacky-sleeps.md allows deterministic test-only sleeps; the added waits in this PR are in the Python E2E test harnesses (time.sleep/polling), not TS/JS/shell production runtime scripts.
Cmux Algorithmic Complexity ✅ Passed New code in Sources/SessionPersistence.swift (loadOutcome/syncManualRestoreSnapshotCache/loadStartupSnapshot) does single file read+JSON decode and switch logic—no nested full-collection scans, sor...
Cmux Swift Concurrency ✅ Passed Scanned PR-touched Swift blocks (AppDelegate.prepareStartupSessionSnapshotIfNeeded and new SessionPersistenceStore methods); they contain no DispatchQueue/Combine/completion-handler legacy patterns...
Cmux Swift @Concurrent ✅ Passed In Sources/AppDelegate.swift and Sources/SessionPersistence.swift, the changed startup snapshot logic is synchronous (no async/nonisolated async/@Concurrent), so no swift-concurrent-annotation rule...
Cmux Swift File And Package Boundaries ✅ Passed SessionPersistence.swift updates are limited to SessionPersistenceStore snapshot load/sync helpers (Foundation/crypto only; no UI/network/protocol code), and AppDelegate just calls loadStartupSnaps...
Cmux Swift Logging ✅ Passed Scanned Sources/SessionPersistence.swift & AppDelegate.swift: no print/debugPrint/dump/NSLog in SessionPersistence, no file-scoped Logger; the new primaryUnusable log is inside #if DEBUG via cmuxDe...
Cmux User-Facing Error Privacy ✅ Passed Production Swift changes only alter snapshot load/restore logic; PR diff contains no user-facing errors/alerts/command output exposing upstream/vendor/provider names, secrets, tokens, env vars, or...
Cmux Full Internationalization ✅ Passed PR only changes session persistence/restore logic in Swift plus test/stress scripts; no production user-facing text, localized String(localized:), Resources/*.xcstrings, or web/i18n (next-intl) upd...
Cmux Swiftui State Layout ✅ Passed PR only modifies AppDelegate.swift, SessionPersistence.swift, and SessionPersistenceTests.swift; no SwiftUI views or prohibited state/layout patterns (ObservableObject/@Published/GeometryReader) ap...
Cmux Architecture Rethink ✅ Passed Pass: Swift diff only refactors snapshot load/backup via SessionPersistenceStore.loadOutcome/loadStartupSnapshot and updates AppDelegate to call loadStartupSnapshot; no sleeps/locks/polling/observe...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Checked Swift changes: AppDelegate.prepareStartupSessionSnapshotIfNeeded only swaps in loadStartupSnapshot (no NSWindow/NSPanel/closeShortcut nearby), and SessionPersistence.swift adds no window/au...
Cmux Source Artifacts ✅ Passed PR #5914 changes only 4 source files (2 Swift, 1 test Swift, 1 python); none are logs/screenshots/recordings/temp/cache/build/DerivedData artifact directories per source-control-artifacts rules.
Description check ✅ Passed The PR description comprehensively covers all required template sections: clear summary of changes and rationale, detailed testing methodology with empirical results, appropriate checklist items, and contextual notes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task-agent-session-restore-stress

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 11, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a data-loss bug where a corrupt primary session snapshot caused cmux to silently start fresh and delete the -previous backup, leaving workspaces and tracked agent sessions unrecoverable. SessionPersistenceStore now classifies a load as missing (no file → clean state) or unusable (file present but unreadable/corrupt/schema-drifted/empty), preserves the backup when the primary is unusable, and falls back to the backup at startup through the new loadStartupSnapshot() entry point.

  • Core fix (SessionPersistence.swift): adds SnapshotLoadOutcome enum and loadOutcome(fileURL:), updates syncManualRestoreSnapshotCache to keep the backup on .unusable instead of deleting it, and adds loadStartupSnapshot() which falls back to the backup in the unusable case with a debug-build diagnostic log.
  • Call-site change (AppDelegate.swift): startup snapshot load switches from load() to loadStartupSnapshot() — a one-line change that is a strict superset in behavior.
  • Unit tests (SessionPersistenceTests.swift): four new regression tests cover corrupt-primary backup preservation, missing-primary backup cleanup, startup backup fallback, and missing-primary nil return, all using isolated temp-dir fixtures.
  • End-to-end harness (test_session_restore_stress_kill_cycles.py): five-phase stress test — seed six sessions, clean relaunch ×2, SIGKILL relaunch, corrupt-primary recovery — verified against both a pre-fix (failing) and post-fix (passing) build.

Confidence Score: 5/5

Safe to merge — the fix narrowly changes how a corrupt-primary startup is handled, the happy path is unchanged, and the new behavior is covered by both unit and end-to-end tests.

The change is well-scoped: the three state transitions (loaded/missing/unusable) are exhaustively tested in unit tests with isolated fixtures, the call-site change in AppDelegate is a single-line swap, and the logic for each case was verified empirically against a pre-fix failing build and a post-fix passing build. No production logging rule, actor isolation, or blocking-runtime concern was introduced or worsened beyond the pre-existing synchronous startup disk reads.

No files require special attention; Sources/SessionPersistence.swift carries the most new logic but all three outcome branches are covered by dedicated unit tests.

Important Files Changed

Filename Overview
.github/swift-file-length-budget.tsv Budget entries updated to reflect actual line counts after new tests and persistence code were added; straightforward bookkeeping change.
Sources/AppDelegate.swift Single call-site change: switches startup snapshot load from SessionPersistenceStore.load() to loadStartupSnapshot(), enabling backup fallback on corrupt primary; no other logic changed.
Sources/SessionPersistence.swift Introduces SnapshotLoadOutcome enum and loadOutcome(fileURL:) to distinguish missing vs unusable primaries; syncManualRestoreSnapshotCache now preserves the backup when primary is unusable instead of deleting it; new loadStartupSnapshot falls back to the backup in the unusable case; debug-only diagnostic log added for the fallback path.
cmuxTests/SessionPersistenceTests.swift Four new unit tests covering corrupt-primary backup preservation, missing-primary backup cleanup, startup fallback to backup, and missing-primary nil return; uses an isolated temp-dir fixture pattern consistent with the existing suite.
tests/test_session_relaunch_resumes_agent_sessions.py Repaired Claude coverage: writes a transcriptPath so hookRecordIsRestorable passes, routes resume through CMUX_CUSTOM_CLAUDE_PATH, and switches claude assertion to order-agnostic token matching since the cmux wrapper interleaves its own args around --resume.
tests/test_session_restore_stress_kill_cycles.py New end-to-end stress harness: seeds six Claude/Codex/OpenCode sessions, then exercises clean relaunch ×2, SIGKILL relaunch, and corrupt-primary relaunch across all five phases; fake agents keep running so snapshots record wasAgentRunning=true.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[prepareStartupSessionSnapshotIfNeeded] --> B[syncManualRestoreSnapshotCache]
    B --> C{loadOutcome primary}
    C -->|.loaded| D[save primary → backup]
    C -->|.missing| E[delete backup]
    C -->|.unusable| F[keep backup unchanged]
    A --> G{shouldAttemptRestore?}
    G -->|no| Z[return — no restore]
    G -->|yes| H[loadStartupSnapshot]
    H --> I{loadOutcome primary}
    I -->|.loaded| J[return primary snapshot]
    I -->|.missing| K[return nil — fresh start]
    I -->|.unusable| L[loadReopenSessionSnapshot backup]
    L --> M{backup valid?}
    M -->|yes| N[return backup snapshot]
    M -->|no/missing| O[return nil — fresh start]
    N --> P[DEBUG: log primaryUnusable + backupRecovered]
Loading

Reviews (7): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment on lines +1881 to +1888
static func loadOutcome(fileURL: URL) -> SnapshotLoadOutcome {
guard FileManager.default.fileExists(atPath: fileURL.path) else { return .missing }
guard let data = try? Data(contentsOf: fileURL) else { return .unusable }
let decoder = JSONDecoder()
guard let snapshot = try? decoder.decode(AppSessionSnapshot.self, from: data) else { return .unusable }
guard snapshot.version == SessionSnapshotSchema.currentVersion else { return .unusable }
guard !snapshot.windows.isEmpty else { return .unusable }
return .loaded(snapshot)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 loadOutcome is internal but not exercised directly by tests

loadOutcome(fileURL:) and SnapshotLoadOutcome are both internal, which means any @testable import cmux consumer can call them directly and couple to the intermediate outcome values. The four new unit tests only call the higher-level helpers (syncManualRestoreSnapshotCache, loadStartupSnapshot, load, save), so there is no test reason for loadOutcome to be wider than private. Making it private static func would keep the public contract at the level of the store's API rather than its parsing step.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declining: loadOutcome is deliberately covered through the public behaviors that depend on it (syncManualRestoreSnapshotCache, loadStartupSnapshot, load) in SessionPersistenceTests; testing the enum directly would pin implementation rather than behavior.

Comment on lines +1964 to 1983
static func loadStartupSnapshot(
bundleIdentifier: String? = Bundle.main.bundleIdentifier,
appSupportDirectory: URL? = nil
) -> AppSessionSnapshot? {
guard let primaryURL = defaultSnapshotFileURL(
bundleIdentifier: bundleIdentifier,
appSupportDirectory: appSupportDirectory
) else { return nil }
switch loadOutcome(fileURL: primaryURL) {
case .loaded(let snapshot):
return snapshot
case .missing:
return nil
case .unusable:
return loadReopenSessionSnapshot(
bundleIdentifier: bundleIdentifier,
appSupportDirectory: appSupportDirectory
)
}
_ = save(snapshot, fileURL: fileURL)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No os.log diagnostic when falling back from corrupt primary to backup

When loadStartupSnapshot hits the .unusable branch and silently returns a backup snapshot, the event is invisible in Console.app. If the primary is corrupt for a systemic reason (bad autosave write, filesystem issue), there is no telemetry to detect frequency or correlate with user-facing confusion. An os_log warning at the .unusable branch in both syncManualRestoreSnapshotCache and loadStartupSnapshot naming the file URL and outcome would make field incidents diagnosable without exposing user content.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in aa06b13: session.restore.primaryUnusable path=... backupRecovered=0|1 debug-log line on the fallback path.

Comment on lines +145 to +148
try:
os.kill(pid, signal.SIGKILL)
except ProcessLookupError:
pass

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 time.sleep(1.5) after socket readiness hides internal-initialization races

_wait_for_socket proves the socket is reachable but not that the app has finished internal startup (session snapshot applied, workspaces restored, surfaces ready). The 1.5 s wall-clock delay is a best-guess window that can fail on a slow CI host or pass on a fast host that has not actually initialized. The same pattern appears in _quit (time.sleep(0.8)) and _force_kill (time.sleep(0.8)). A real signal from the app (startup-complete ping response, workspace-count API reply, or an explicit ready handshake) would make phase transitions deterministic.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declining the post-socket sleep change: it matches the established harness pattern in tests/test_session_relaunch_resumes_agent_sessions.py, and hook stores are read synchronously at snapshot-save time, so there is no ingestion race. The suite has passed repeatedly end to end.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_session_restore_stress_kill_cycles.py`:
- Around line 336-344: The test currently uses a fixed sleep after writing hook
state files (around client.select_workspace(0) then time.sleep(0.4)) which can
allow an incomplete initial snapshot; replace that fixed sleep with a poll that
verifies the app has ingested all six seeded sessions before calling _quit().
After calling _write_hook_state(hook_state_files[launcher], entries) and
client.select_workspace(0), repeatedly query the authoritative condition (for
example via the client API that reports tracked/ingested sessions or a session
list/count exposed by the test harness) until it shows six sessions are tracked,
with a short sleep between attempts and a sensible overall timeout that fails
the test if readiness is not reached; only then proceed to _quit(bundle_id,
socket_path) (preserve existing failure handling via failures and _report).
- Around line 390-395: Add an explicit invocation of the CLI entrypoint that
exercises the same recovery code path: call “cmux restore-session” (which routes
to session.restore_previous) after confirming previous_snapshot.exists() and
before calling _quit(bundle_id, socket_path), then re-check the resumed/marker
artifacts the same way the startup path does; specifically, in
tests/test_session_restore_stress_kill_cycles.py add a step that runs the cmux
restore-session command for the bundle_id/socket_path and assert the resumed
markers (the same checks used for startup-restore) to ensure the CLI path is
tested in addition to on-startup restore.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 50695f88-a7b0-42dd-9d74-e8dadca2f45f

📥 Commits

Reviewing files that changed from the base of the PR and between ccde75d and 5543b2d.

📒 Files selected for processing (4)
  • Sources/AppDelegate.swift
  • Sources/SessionPersistence.swift
  • cmuxTests/SessionPersistenceTests.swift
  • tests/test_session_restore_stress_kill_cycles.py

Comment thread tests/test_session_restore_stress_kill_cycles.py
Comment thread tests/test_session_restore_stress_kill_cycles.py Outdated
…g and wrapper resolution

Claude hook records are only restorable when their transcript exists on disk
(hookRecordIsRestorable), and claude resume routes through the cmux claude
wrapper, which resolves the real binary instead of the captured executable.
The relaunch harness silently lost its claude assertion when those behaviors
landed (it is not run in CI): fake claude sessions were dropped at index load,
and when they did resume the real claude binary ran instead of the fake.

Both harnesses now write a transcriptPath for claude sessions, point
CMUX_CUSTOM_CLAUDE_PATH at the fake binary, and match resume markers as
order-agnostic tokens on one line (the wrapper inserts its own arguments
around --resume).
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…-session in corrupt phase

Adds a session.restore.primaryUnusable debug-log line when startup falls
back from an unusable primary snapshot, and extends the corrupt-snapshot
stress phase to run the bundled cmux restore-session verb, asserting it
reopens the backed-up session in a new window (window count, since the
restored workspaces share ids with the startup fallback restore).
…store-stress

# Conflicts:
#	.github/swift-file-length-budget.tsv
…store-stress

# Conflicts:
#	.github/swift-file-length-budget.tsv
…store-stress

# Conflicts:
#	.github/swift-file-length-budget.tsv
…store-stress

# Conflicts:
#	.github/swift-file-length-budget.tsv
@lawrencecchen
lawrencecchen merged commit e2d6c44 into main Jun 12, 2026
12 checks passed
hhsw2015 pushed a commit to hhsw2015/cmux that referenced this pull request Jun 12, 2026
…ackup (manaflow-ai#5914)

* Add failing coverage: corrupt session snapshot must not destroy restore backup

A corrupt primary session snapshot at startup currently makes
syncManualRestoreSnapshotCache delete session-<bundle>-previous.json (the
restore-session backup) and the app silently starts fresh. These tests pin
the desired behavior: keep the backup and recover startup restore from it.

Includes tests/test_session_restore_stress_kill_cycles.py, an end-to-end
stress harness covering repeated clean relaunches, SIGKILL relaunch, and
corrupt-snapshot recovery for tracked Claude/Codex/OpenCode sessions.

SessionPersistenceStore.syncManualRestoreSnapshotCache and the new
loadStartupSnapshot gain injectable bundle/app-support parameters
(behavior unchanged in this commit) so the regression tests can run
against temp paths.

* Recover session restore from the -previous backup when the primary snapshot is corrupt

SessionPersistenceStore.load() treated a corrupt primary snapshot the same
as a missing one: startup silently began a fresh session and
syncManualRestoreSnapshotCache deleted session-<bundle>-previous.json, the
only remaining copy of the user's workspaces, so restore-session could not
recover anything either.

loadOutcome now distinguishes a missing snapshot (clean state) from an
unusable one (unreadable data, decode failure, schema drift, anomalous
empty window list). When the primary is unusable, the backup is preserved
and startup restore falls back to it, so workspaces and tracked agent
sessions come back automatically.

Also captures launch environment in the stress harness hook entries so
fake-claude resume wins the PATH lookup (claude resume intentionally
routes through the wrapper shim / PATH instead of the captured executable).

* Repair claude coverage in relaunch/stress harnesses: transcript gating and wrapper resolution

Claude hook records are only restorable when their transcript exists on disk
(hookRecordIsRestorable), and claude resume routes through the cmux claude
wrapper, which resolves the real binary instead of the captured executable.
The relaunch harness silently lost its claude assertion when those behaviors
landed (it is not run in CI): fake claude sessions were dropped at index load,
and when they did resume the real claude binary ran instead of the fake.

Both harnesses now write a transcriptPath for claude sessions, point
CMUX_CUSTOM_CLAUDE_PATH at the fake binary, and match resume markers as
order-agnostic tokens on one line (the wrapper inserts its own arguments
around --resume).

* Compact snapshot backup tests behind a shared fixture; refresh swift file length budget

* Address review: log corrupt-primary backup fallback, exercise restore-session in corrupt phase

Adds a session.restore.primaryUnusable debug-log line when startup falls
back from an unusable primary snapshot, and extends the corrupt-snapshot
stress phase to run the bundled cmux restore-session verb, asserting it
reopens the backed-up session in a new window (window count, since the
restored workspaces share ids with the startup fallback restore).

This branch was successfully deployed

1 active deployment
Preview – cmux — 09904ad1 Deployed Jun 12, 2026 by vercel[bot]
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