Repository navigation
(restore): save the open sessions when the app closes - #479
Conversation
devsuitup
left a comment
There was a problem hiding this comment.
Review at 82cf905. Three points block; each was reproduced by the reviewer against the shipped code and I re-read the paths.
Blocking
- Quitting while the index is cold, or before the Restore prompt is answered, overwrites the previous saved set with
[](public/app.jsflushStateForExit,persistWorkingSet({ final: true })). Nothing is open yet at that point, so the final write replaces a good set with an empty one. Fix: skip the final write when the restore has not resolved, or keep the saved entries that are not open yet. Test: quit with the saved set unrestored, then check the stored set. - A session stopped on purpose during the restore comes back at the next start. If the user stops a session while the restore is still running and quits before it ends, the unchanged previous snapshot is what stays, so the stopped session is restored again.
- Changes made during the ten-second grace period of an aborted exit are never saved.
schedulePersistWorkingSetreturns early whileexitingAppis set, and whenexitingAppTimerexpires nothing persists again. Fix: persist once when the grace period ends.
Conflicts with #441 (lazy restore)
- The final serializer must keep the dormant entries (
dormantWorkingSet), as #441'spersistWorkingSetdoes; otherwise a quit drops them. - Entries the planner has not resolved yet need the same protection as in the #441 review.
- A dormant entry dismissed during an aborted exit must not be lost. The branch also needs a rebase on current main.
Non-blocking
- Windows logoff and shutdown bypass the new flush (
unsaved-guard.js), so a pending debounce can still be lost. - The CHANGELOG entry must end with
(#479).
79 tests in the seven related files pass locally on Node 24. Not verified: Node 20/22 with c8.
|
Tested on Linux (Ubuntu, GNOME on Wayland), PR head 82cf905. Setup. An isolated instance, with a throwaway HOME and its own data directory. The fixture is one git project with synthetic transcripts. A stub Working
Blocking: "quitting before the Restore prompt is answered overwrites the saved set with []." Reproduces.
Not tried. Blocking points 2 (a session stopped during the restore comes back) and 3 (no save after the 10 s grace period). |
82cf905 to
00783c2
Compare
|
Thanks, and thanks @jbr-sekoia for the reproduction. Rebased on current main (eaf59ff); the fixes are in 00783c2, on top of the rebased original commit. Blocking
Conflicts with #441. I checked them in a throwaway merge of both branches, where both test suites pass (64):
Non-blocking
Each fix was removed in turn, and each removal fails a test. The related suites pass (60). On the full suite, the only failures are the CLI-state canary, |
jbr-sekoia
left a comment
There was a problem hiding this comment.
Review at 00783c2 (rebased on eaf59ff). The three blocking points and the Linux reproduction are fixed in the code. Two of the new guards can be deleted with the suite still green, and one of them is the line that makes the final write happen at all.
Previous points
- Quitting during a cold index or before the Restore prompt is answered: resolved. The final write goes through
persistWorkingSet, which keepsrestorePlanner.pending()andrestoreAwaitingConsent. A quit beforerestoreWorkingSethas read the saved set writes nothing (savedWorkingSetRead). These are the two cases from the Linux comment. I traced them in code and they are covered bytest/exit-flush.test.js, but I did not repeat the live run. - A session stopped during the restore comes back: resolved.
flushStateForExitno longer returns early whilerestoringWorkingSetis set. The stopped entry isclosed, and it has already leftrestoreInFlight, so it is left out. Adding the old early return back fails "an exit during a restore keeps the sessions not started yet and leaves out one stopped". - No save after the grace period of an aborted exit: resolved for a scheduled save. Removing the persist when the grace period ends fails a test, and so does removing the flag in
schedulePersistWorkingSet. See Blocking 2 for a directpersistWorkingSet()call.
- #441: a throwaway merge of 00783c2 and f4bd11b conflicts only in
test/restore-pending-persist.test.js(the declarations list). AfterdormantWorkingSetis added totest/exit-flush.test.jsandexitingApp/persistSkippedWhileExitingtotest/restore-lazy.test.js, 73 tests pass across the restore, exit-flush and unsaved-guard files. The final write includesdormantWorkingSet. - CHANGELOG: ends with
(#479).
Blocking
- Deleting
savedWorkingSetRead = truefromrestoreWorkingSet(public/app.js) leaves every related test green. The setup intest/exit-flush.test.jssets the flag itself. With that line gone,flushStateForExitnever writes on any quit, so the last change before a quit is lost again, and nothing fails. Fix: one test that calls the shippedrestoreWorkingSet(modeoffis enough), thenflushStateForExit, and checks that the set was written. - The quit answer is not shown to wait for the write (
public/file-panel.js,unsaved-checkhandler). Ifawait Promise.race([window.flushStateForExit(), bound])is replaced by a call that is not awaited,test/dom-file-panel-unsaved-guard.test.jsstill passes. The stub recordsanswers.lengthwhen it is called, not when it resolves. With that mutation, main getsunsaved-check-resultwhile the write is still in flight and starts killing the PTYs and closing the DB. That is the race this PR exists to remove. Fix: make the stub return a promise the test resolves by hand, and assert that no answer is sent before it resolves.
Non-blocking
- The skip flag in
persistWorkingSetis never tested (public/app.js). RemovingpersistSkippedWhileExiting = truefrom its early return leaves the suite green. Two callers depend on it: the persist afterrunRestore(auto mode and the Restore button), anddismissDormantSessiononce #441 is merged. Without the flag, a restore that finishes during an aborted exit is not saved until the next change. appQuittinginmain.jshas no test. Removing the guard, or the assignment, leaves the suite green. The PR body says so.- Removing
onExitFlushregistration inpublic/app.jsleaves the suite green. The main side of the Windows logoff path is tested and the renderer side is not. That path is best effort, as the docs say. - A cancelled Windows logoff turns off the flush for later quits (
unsaved-guard.js).endSessioncallsapproveQuit(), andquitApprovedstays true. If the logoff is cancelled, every later close or quit skipsunsaved-check, soflushStateForExitnever runs and a pending debounce can be lost. This existed before the PR, but the new flush depends on that check.
Tests: 89 pass in the 8 restore and unsaved-guard files at 00783c2 (Node, SWITCHBOARD_TEST_CONCURRENCY=1). Mutations killed: the read guard, the pending entries in the held list, the early return during a restore, the persist at the end of the grace period, the scheduled-save flag, the final bypass, the reload and cancel conditions, the exit-flush send. Not verified: a live run at the new head, Windows logoff, Node 20/22 with c8.
devsuitup
left a comment
There was a problem hiding this comment.
Review at 00783c2, rebased on current main. Thanks: the three earlier blockers are fixed (quitting while the index is cold or before the Restore prompt is answered no longer writes [], a session stopped on purpose no longer comes back, and changes suppressed during the grace period of an aborted exit are saved), Windows shutdown now dispatches a best-effort flush, and the changelog ends with (#479). 89 tests in eight related files pass locally on Node 24. One new point blocks; the reviewer executed it against the shipped handler and guard.
Blocking
- A Quit or Close that joins an unanswered Reload check skips the flush (
public/file-panel.js~184,unsaved-guard.js~27 and ~63). The guard shares one in-flight request. If a Reload check is waiting for the user and a Quit or Close arrives, it joins that request, which still saysreload; the renderer therefore does not run the exit flush, while main proceeds with the exit. In the executed probe: zero writes, the saved set stayed[a]while the open sessions were[a, b]. Fix: when a later request with a different reason joins an in-flight check, carry the stronger reason (exit) to the renderer, or run the flush on the main-side decision to exit and not on the renderer's reading of the reason. Test: start a reload check, send a quit before it is answered, answer, and expect exactly one flush with the final set.
Integration with the other open PRs (to settle before merge)
- #441: the final serializer has to keep the dormant, the unresolved planner and the in-flight restore entries, and must not revive a session stopped on purpose.
- #478: the flush has to run after its final close confirmation, and the shared Reload/Exit intent has to resolve the same way in both.
Non-blocking
- Quitting before the restore ends preserves the pending sessions but clears their active marker, so the next restore selects the last session (
public/app.js~205). - The immediate flush stub in
test/dom-file-panel-unsaved-guard.test.js(~243) does not notice removing theawait; the main-side notification suppression and the shutdown subscriptions are untested. - Remove the new inline rationale comment at
unsaved-guard.js~85.
Not run: Electron, a real shutdown, Node 20/22 with c8, lint, the full suite, fresh CI. CI on this head is not approved yet.
00783c2 to
71ea998
Compare
|
Thanks to both of you. Rebased on main at 805d064, the last commit before e329a33: my token cannot push a branch that contains the workflow change of #498, so the branch is three commits behind main (e329a33, #507, v0.0.90), none of which touches these files. The fixes are in 71ea998, on top of the two rebased commits. Blocking
Non-blocking
Integration with #441 and #478. The dev branch #480 combines the three. There, the Mutations, one at a time; each turned at least one test red: deleting Lint: 0 errors. On the full suite (Node 22), the only failures are the CLI-state canary, |
devsuitup
left a comment
There was a problem hiding this comment.
Review at 00783c2, rebased on current main. Thanks: the three earlier blockers are fixed (quitting while the index is cold or before the Restore prompt is answered no longer writes [], a session stopped on purpose no longer comes back, and changes suppressed during the grace period of an aborted exit are saved), Windows shutdown now dispatches a best-effort flush, and the changelog ends with (#479). 89 tests in eight related files pass locally on Node 24. One new point blocks; the reviewer executed it against the shipped handler and guard.
Blocking
- A Quit or Close that joins an unanswered Reload check skips the flush (
public/file-panel.js~184,unsaved-guard.js~27 and ~63). The guard shares one in-flight request. If a Reload check is waiting for the user and a Quit or Close arrives, it joins that request, which still saysreload; the renderer therefore does not run the exit flush, while main proceeds with the exit. In the executed probe: zero writes, the saved set stayed[a]while the open sessions were[a, b]. Fix: when a later request with a different reason joins an in-flight check, carry the stronger reason (exit) to the renderer, or run the flush on the main-side decision to exit and not on the renderer's reading of the reason. Test: start a reload check, send a quit before it is answered, answer, and expect exactly one flush with the final set.
Integration with the other open PRs (to settle before merge)
- #441: the final serializer has to keep the dormant, the unresolved planner and the in-flight restore entries, and must not revive a session stopped on purpose.
- #478: the flush has to run after its final close confirmation, and the shared Reload/Exit intent has to resolve the same way in both.
Non-blocking
- Quitting before the restore ends preserves the pending sessions but clears their active marker, so the next restore selects the last session (
public/app.js~205). - The immediate flush stub in
test/dom-file-panel-unsaved-guard.test.js(~243) does not notice removing theawait; the main-side notification suppression and the shutdown subscriptions are untested. - Remove the new inline rationale comment at
unsaved-guard.js~85.
Not run: Electron, a real shutdown, Node 20/22 with c8, lint, the full suite, fresh CI. CI on this head is not approved yet.
71ea998 to
8bf1c90
Compare
|
Thanks for the rebase. One thing the rebase did not fix: the |
8bf1c90 to
4a84554
Compare
|
Rebased on current main (a02f96c, v0.0.92); new head 4a84554. The changelog line is now under The rebase met #520 in A note on the review at 71ea998: its text is the review of 00783c2 ("Review at 00783c2…"), and its points were answered in the previous reply. At this head they are covered by:
Full suite locally: only the failures this sandboxed environment has on main. Not run: Electron, a real Windows logoff. |
devsuitup
left a comment
There was a problem hiding this comment.
Re-review at 4a84554 — approve, with non-blocking points. No defect found in the code that blocks; nothing merged is reverted (#518/#520 runRestore untouched, keepAttached kept; notStarted drops none of its entries). Base a02f96c, mergeable; CI green on the exact head (runs 38040124304 / 38040124329); changelog under Unreleased / Fixed.
Previous findings — all closed except one:
| Finding | Status |
|---|---|
Quit during a cold index / before the prompt writes [] |
closed (exit-flush tests 4–6, fail on main) |
| Session stopped during restore comes back | closed (test 7) |
| Changes during the grace period never saved | closed (tests 8, 10) |
| Windows logoff bypasses the flush | closed, best effort (unsaved-guard.js:90-102, guard test 14, exit-flush 12) |
| Cancelled logoff turns the flush off | closed (LOGOFF_CANCEL_MS, guard test 18) |
| Quit joining a reload check skips the flush | closed (guard 16, dom 10), residual race below |
| Active marker cleared on an early quit | closed (exit-flush 11) |
savedWorkingSetRead, await on the flush, skip flag, onExitFlush untested |
closed — mutations deleting each now fail a test |
appQuitting untested |
still open (N1) |
| #441 / #478 integration | not applicable until one of them lands; the second to land handles it |
Red-first: every commit on the branch ships tests with its fix, so CI never shows the new tests red. Replaying the PR's test files on top of main (Node 22) does fail them — exit-flush 12/12, dom-file-panel-unsaved-guard 9 and 10, unsaved-guard 14, 16, 18 — so I am not holding the PR on it; please keep the tests-first commit for new fixes, as #538–#540 do.
Non-blocking
main.js:2442,:3538:appQuittinghas no test (0 hits intest/ande2e/); the renderer half (exitingApp) is covered, so a regression on the main side would only be caught by the 10 s grace.public/app.js:209: the #520 rule (a not-started entry holding the saved active marker keeps it over a shownkeepAttachedentry) is unpinned — marking both active leaves 137/137 related tests green.public/file-panel.js:164-165withunsaved-guard.js:32-37(plausible): if the renderer has answered the reload check but main has not processed it yet, the id is already deleted and theunsaved-check-reasonupgrade is dropped, so the close/quit skips the flush. IPC latency only; reachable from a programmatic quit (updater install).unsaved-guard.js:35,:99: the two rationale comments on empty catches go against the comment rule (main.jsuses barecatch {}).- Pre-existing, plausible: a quit joining a reload check still reloads before quitting, and the reloaded page starts with
exitingApp=false.
Merge order with #540: they conflict (unsaved-guard.js ask/inflight and exports, tests, CHANGELOG, viewer-panel.md), and #540's fixed 3 s bound can destroy a busy page before this flush runs; whichever lands second needs a fresh look.
Not run here: a live Electron quit, a real Windows logoff, a macOS dock close.
|
Non-blocking points addressed at b41237a (tests first: 4e2eefd, run 38068143104 red on all four test jobs on exactly the two new tests; b41237a run 38068446906 green on every job).
Docs: Merge order: this PR before #540/#478; they get rebased on it, keeping this flush after |
devsuitup
left a comment
There was a problem hiding this comment.
Re-review at b41237a — approve (2 minor, 1 nit, non-blocking).
Red-first: 4e2eefd (tests only) fails all four test jobs (run 38068143104) on exactly the two new behaviour tests, both on assertions; b41237a green on every check (runs 38068446906 / 38068446891). Locally (Node 22) unsaved-guard, dom-file-panel-unsaved-guard and exit-flush: 45/45.
| Previous point | Status |
|---|---|
appQuitting untested |
closed, weakly — test/exit-flush.test.js:209 matches main.js text (nit below) |
| #520 active-marker rule unpinned | closed — exit-flush.test.js:198 asserts [a,false],[b,false],[c,true] |
| Reason upgrade lost after the renderer answered | closed, best effort — file-panel.js:153-160 answeredReloads + fire-and-forget flush; covered, including no flush for a reload answered no |
| Rationale comments on empty catches | closed — bare catch {} |
| A quit joining a reload check reloads first | closed — unsaved-guard.js:125-129 checks check.reason !== 'reload', with a test |
Minor
unsaved-guard.js:126-129(plausible): a reload request arriving while a quit/close question is open is now dropped after a yes (it went ahead at 4a84554). Right for a quit or close; but if the yes comes fromconfirmQuit(updater install,main.js:3297) andquitAndInstallthen does not quit, the reload is lost and the page stays in exit mode for 10 s with saving stopped. "a reload joining a quit check leaves it a quit" only checks the channel; assertingwc.reloads === 0after a yes would pin the behaviour.file-panel.js:158(plausible): when the page had already answered the reload, the flush is fire-and-forget while main quits at once, so the save races window destruction — documented as best effort. A narrower pre-existing window remains (main already processed the reload answer, the quit's question reaches a reloading page, 2.5 s timeout, quit without saving).
Nit: test/exit-flush.test.js:209-218 matches the text of main.js; a behaviour-preserving refactor breaks it and a text-preserving regression passes.
Conflicts: with #540 now only test/unsaved-guard.test.js (both append tests); with #478 still heavy (unsaved-guard.js 6 hunks, public/file-panel.js, CHANGELOG, viewer-panel.md, both guard test files).
Mergeable on a02f96c, changelog under Unreleased / Fixed, only one-line pointers. Not run: a live Electron quit/reload, a real Windows logoff, an updater install.
Superseded: each item of this review was verified closed at f39d5ea, with a test that fails when its line is removed (B1 app.js:354, B2 file-panel.js:173, NB1-NB4). See the review comment on this PR.
…d active marker Tests for the last review round of devsuitup#479, which devsuitup#548 did not merge: a quit that joins a reload check the page already answered still writes the app state, and a reload check a quit joined quits without reloading first. Two more pin existing behaviour: main stops reporting session exits once the quit starts killing them, and a not-started entry holding the saved active marker keeps it over a reattached session shown meanwhile.
f54f3fa to
91c13ef
Compare
…sions when the app closes, last review round) (#555)
|
@abate f39d5ea is on main: #555 merged it with a merge commit at 14:13Z. The only conflict was the CHANGELOG with #477, and both lines are kept under Unreleased. Your new head 91c13ef sits on 2bae3ce. Against main, it now carries only the tests for the late save after an answered reload, for the minor point in my review. I'll review that round as its own delta once the fix is pushed. It is not needed for v0.0.93, so it will go into the next release. Showing the test red locally is enough now. No red CI run is needed, so you can push the tests and the fix together. |
… open sessions when the app closes, last review round) Merged onto main after devsuitup#477: the only conflict was the CHANGELOG, where both PRs moved their stale v0.0.90 entry to Unreleased; both lines are kept under Unreleased and neither under v0.0.90.
5920379 to
1dfff09
Compare
|
Review of 5920379 (the late-save round): changes requested. One rebase, plus one minor point.
|
1dfff09 to
7b3fdbb
Compare
|
Follow-up to the 13:48 minor (a quit joining a reload the page already approved). Rebased on main 24ac041 (#555 merged f39d5ea); this PR now carries only these two commits. Was there a gap? Yes. Main processes the renderer's answer to the reload, resolves the joined quit, and calls Fix:
Tests first:
The changelog line is extended, since the check requires an entry. |
|
Review of 7b3fdbb: changes requested.
|
7b3fdbb to
87920e2
Compare
|
Addressed at 9f9c57f, rebased onto main 421f77f. Major (changelog in a released section). The previous edit of the released #479 line under v0.0.93 is reverted; the follow-up has its own entry under Unreleased / Fixed. Minor (race: answer clears Nit (renderer gone). Red first: 87920e2 (tests only), run 38216641758 fails exactly the two new tests on all four test jobs. Fix 9f9c57f: run 38217312151 green on every job. Mutations: dropping the |
devsuitup
left a comment
There was a problem hiding this comment.
Re-review at 9f9c57f. The code is approved; the branch needs a rebase before it can merge.
Merge blocker: CHANGELOG conflict with current main
CHANGELOG.md conflicts with main (now abc9a54). Two changes on main touch the same spot. #580 added its line under ## Unreleased / ### Fixed. v0.0.95 (#581) then renamed that section to ## v0.0.95 — 2026-10-12 and opened a new, empty ## Unreleased. Please rebase onto main and put this PR's line under the new ## Unreleased / ### Fixed, not inside the v0.0.95 section. All other files merge cleanly.
Previous points (review of 7b3fdbb)
| Point | Status | Evidence |
|---|---|---|
| Major: CHANGELOG edit landed in the released v0.0.93 section | Closed | The diff adds one line under ## Unreleased / ### Fixed. The released #479 line is unchanged. The changelog check passes. |
Minor: the answer clears inflight before a joined quit is approved, so a later close or quit asks again |
Closed | unsaved-guard.js:64 sets held. :95 no longer clears inflight for a held check. :74-77 clears it once the wait ends. Covered by "a close or quit arriving while a joined quit waits for the late save joins it instead of asking again" and "a joined quit the page declined leaves nothing for a later close to join". |
Nit: waitReasonDone did not release when the renderer died |
Closed | unsaved-guard.js:44-54 listens to render-process-gone and destroyed, and removes both listeners on release. The test covers both events and checks that no bounded timer is left. |
Tests fail first, then pass (local, Node 22, unsaved-guard.test.js + dom-file-panel-unsaved-guard.test.js)
| Commit | Content | Result |
|---|---|---|
| 3d9653b | tests only | 2 of 42 fail, on assertions |
| a1ccc8e | fix | 42 of 42 pass |
| 87920e2 | tests only | 2 of 44 fail, on assertions |
| 9f9c57f | fix | 45 of 45 pass |
- CI matches: the runs on 87920e2 and 91c13ef fail, and run 38217312151 on 9f9c57f is fully green.
- The related test files at the head give 325 passing, 0 failing and 2 skipped.
Minor (non-blocking)
test/unsaved-guard.test.js, the new tests, againstunsaved-guard.js:35-38. Every main-side test sendsunsaved-check-reason-doneafter the answer. When the question is still open, the renderer sends it before the answer (file-panel.js:163), and that is the common case.- Mutation run: adding
if (pending.has(id)) return;to theunsaved-check-reason-donehandler leaves 32 of 32 tests passing. - Impact: that regression would hold every joined quit for up to 2.5 s after a quick answer.
- Suggested test: send
reasonDone(id)beforeanswer(id, true), then expect the quit on the next tick with no bounded timer still running.
- Mutation run: adding
Checked, nothing found
- The wait timer starts at the join, not at the answer. This is harmless in both cases:
- when the answer already left, the result arrives right after;
- when the question is still open, the renderer reports done at once.
- A reload during the held wait joins the chain. The original
will-prevent-unloadhandler still skips the reload. - No new inline comments, and the docs (
viewer-panel.md,session-restore.md) match the behaviour.
Not run: Electron, a real quit or reload, the full suite.
… late save When the renderer answered the reload before the upgrade reached it, its save runs after main already went on to quit, and the window can close before the write lands. Main should wait for the page to report the save done, bounded, and the page should report it.
…save is done The renderer answers every reason upgrade with unsaved-check-reason-done, after the save when it is writing for an already approved reload, at once otherwise; main waits for it, bounded by the check timeout.
…when the page dies A close or a quit that arrives after the page answered a reload that a quit joined, but before the page reported its late save, must join that quit instead of asking again. A page that crashes or is destroyed while the quit waits must release it at once rather than after the bound.
The answer used to clear the check in flight while the joined quit still waited for the page's late save, so a close or quit in between started a second question. The joined check now stays in flight until that wait ends, and the wait ends at once when the renderer process is gone.
…s the joined quit on that answer The renderer reports the upgrade done before it answers when the question is still open; the joined quit must then go on the answer, with no bounded wait left running.
9f9c57f to
80c87e3
Compare
|
Addressed review of 9f9c57f. Backup of the old head:
Commits rebased: 26d53d8, 551f721, a1c1d94, 767c556 (former 3d9653b, a1ccc8e, 87920e2, 9f9c57f), plus 80c87e3. CI at 80c87e3: Test run 38256934189 and Build & Release run 38256934158, both green (incl. e2e). |
devsuitup
left a comment
There was a problem hiding this comment.
Re-review at 80c87e3. Approve. The rebase is clean, the CHANGELOG line sits in the right section, and the earlier minor point is closed.
What changed since 9f9c57f (git range-diff)
- Commits 1, 3 and 4 are unchanged.
- Commit 2 changes only the CHANGELOG hunk, which is now under the new
## Unreleased. - Commit 5 (80c87e3) is test-only: +15 lines in
test/unsaved-guard.test.js. unsaved-guard.js,public/file-panel.jsandpreload.jsare byte-identical to the approved 9f9c57f.
Previous points
| Point | Status | Evidence |
|---|---|---|
| Merge blocker: CHANGELOG conflict with main | Closed | The #479 line sits under ## Unreleased / ### Fixed, above ## v0.0.95. The v0.0.95 section is unchanged, and the changelog check passes. |
Minor: no main-side test sent unsaved-check-reason-done before the answer |
Closed | New test "a page that reports the upgrade done before it answers lets the joined quit go on the answer" (test/unsaved-guard.test.js:345). Mutation check: adding if (pending.has(id)) return; to the unsaved-check-reason-done handler makes this test fail, while the 9f9c57f test file still passes 32/32 against the same mutant. |
Checks
- Base and merge: the branch is based on current main (61bee93), the merge has no conflicts, and GitHub reports MERGEABLE.
- Local tests (Node 22):
unsaved-guard+dom-file-panel-unsaved-guardpass 46/46. The related set (file panel, restore, New Session dialog, keyboard focus, choice dialog) passes 510/510. - CI on 80c87e3 is green: lint, changelog, e2e, tests on Node 20/22 × ubuntu/windows, and the 4 builds.
- Pending #589 (modal stack, async discard check): a trial 3-way merge has no textual conflicts, and the related tests pass on the merged tree (351/351). Whichever of the two lands second should re-run
dom-file-panel-unsaved-guard.test.js.
Not run: Electron, a real quit or reload, the full suite.
Now: the late-save wait only
Main merged this PR's last round at f39d5ea (#555). This branch is main (421f77f, v0.0.94 released) plus:
unsaved-check-reasonwithunsaved-check-reason-done: after the write for an approved reload, at once otherwise. Main waits for it, bounded by the check timeout, and not at all for a declined or unanswered check.render-process-gone,destroyed).Commits, each fix after its own tests:
Problem
The open working set (
global.openWorkingSet, what restore reopens) is saved on a 500 ms debounce, so:before-quitkills every PTY while the renderer is alive; each kill arrives asprocess-exited→entry.closed→ a scheduled persist that, if the app lingers past 500 ms (e.g. the ActivityWatch flush, up to 1.5 s), saves an empty set.Fix
unsaved-checkhandler awaitsflushStateForExit()(bounded to 2 s) before answering main. It cancels the debounce, writes the set at once, and blocks further working-set writes for 10 s (restored afterwards in case the exit does not happen, e.g. an installer that fails). A reload does neither; an exit during a restore writes nothing, keeping the previous run's set.appQuittingis set beforebefore-quitkills the PTYs, andprocess-exitedis no longer sent after it.Docs:
docs/session-restore.md→ "Closing the app".Tests
test/exit-flush.test.jsloads the shippedapp.jsfunctions: immediate write keeps other global keys; shutdown exits cannot empty the set; persistence resumes after the grace period; an exit mid-restore writes nothing.test/dom-file-panel-unsaved-guard.test.js: confirmed quit flushes before answering, reload does not, cancel does not.test/restore-live-elsewhere.test.jsharness declaresexitingApp.appQuittingguard has no unit test.🤖 Generated with Claude Code