Skip to content

feat: auto-save and full session restore on macOS system shutdown - #75

Merged
project820 merged 9 commits into
mainfrom
feat/shutdown-autosave-session-restore
Jul 24, 2026
Merged

feat: auto-save and full session restore on macOS system shutdown#75
project820 merged 9 commits into
mainfrom
feat/shutdown-autosave-session-restore

Conversation

@project820

Copy link
Copy Markdown
Owner

Summary

Closes #73. Related backlog: #74.

On macOS system shutdown / restart / logout, Notepad AI no longer cancels the OS action with a dirty-document dialog. Instead it:

  1. Latches an explicit CloseIntent: 'shutdown' via powerMonitor + createQuitApprovalController.beginSystemShutdown().
  2. Hybrid-persists without any dialog: dirty path documents are saved; untitled content is kept in the session snapshot; empty windows are excluded.
  3. Commits cleanExit: false plus a one-shot restoreReason: 'shutdown' marker atomically.
  4. On next launch, auto-applies the shutdown snapshot with no recovery banner. Crash restore still shows the existing banner (no regression).
  5. File-save failures fall back to the dirty session snapshot (no data loss).
  6. Shutdown supersedes a prior approved quit fence; if that shutdown pass is denied, the controller falls back to the already-approved quit so no residual latch/fence remains.

No new UI / i18n strings / settings toggle. Session schema stays v2 (optional fields only).

Commits

Commit branch matrix

Intent Dialog Path dirty Untitled dirty Empty window Session marker Next launch
User close / ⌘Q existing dialogs save/discard/cancel as today same n/a cleanExit on approved quit crash banner only if unclean
System shutdown none auto-save snapshot only excluded cleanExit:false + restoreReason:'shutdown' (one-shot) auto-apply, no banner
Shutdown after approved quit supersede + re-run hybrid hybrid excluded supersede fence as shutdown
Shutdown denied after superseded quit n/a n/a n/a n/a fall back to approved quit (app quits) clean

Smoke isolation / order

  • Legacy 7 scenarios keep historical order and share one userData (cold per-scenario userData exposed a pre-existing save-close delay flake on both main and this branch; not a product regression).
  • New pairs shutdown-restore and file-failure-restore each use pair-local userData.
  • Shutdown first worker is driven through real beginSystemShutdown() (NOTEPAD_AI_CLOSE_SMOKE_TRIGGER=shutdown); file-save fault via NOTEPAD_AI_SMOKE_FAIL_FILE_SAVE=1.

Verification (all green on HEAD c8117c1)

  • npm run preflight:tessdata
  • npm run typecheck
  • npm run test — 149 files / 2422 tests
  • npm run build
  • npm run test:security-e2e
  • npm run test:converter-e2e
  • npm run test:html-export-direct
  • npm run test:roundtrip-smoke
  • npm run test:close-dialog-smoke — 7 legacy + shutdown-restore + file-failure-restore (3 consecutive greens at residual fix + reconfirm after knip fix)
  • npm run knip
  • npm audit --omit=dev --audit-level=high — high findings are pre-existing on origin/main; this PR does not change package.json / package-lock.json

Architect re-review: CLEAR / CLEAR / CLEAR · APPROVE (prior HIGH supersede residue resolved).
Red-team adversarial cases a–e (empty-window exclusion, crash banner, one-shot marker, ⌘Q regression, file-failure dirty fallback): all passed.

PII 4-surface scan

Surface Result
Commit author/committer project820 noreply only
Diff 0 personal/local-absolute-path hits
Commit messages 0 hits
PR body 0 hits

Real-device manual QA (user-owned — leave blank until exercised)

Do not merge this PR until the table below is filled by a human on a real Mac and an explicit merge approval names this PR number.

Path Dirty path doc auto-saved? Untitled restored dirty? Empty window excluded? No crash banner on shutdown restore? Crash banner still works on force-kill? Notes
System Shutdown
System Restart
User Logout

Merge policy

Do not merge without an explicit user approval that names this PR number. No --auto / self-merge / admin merge.

macOS shutdown/restart/logout no longer gets cancelled by the save dialog:
- powerMonitor 'shutdown' latches an explicit shutdown intent; before-quit
  joins the same quit-approval controller (user-initiated quit unchanged)
- shutdown branch saves dirty path files without any dialog, keeps untitled
  content in the session snapshot (empty windows excluded), and commits
  cleanExit:false plus a one-shot restoreReason:'shutdown' marker atomically
- next launch auto-applies the shutdown snapshot without the recovery
  banner; crash restore keeps the existing banner flow (no regression)
- failed file saves fall back to the session snapshot (no data loss); a
  shutdown commit after an approved quit supersedes the quit fence instead
  of silently no-oping; a denied shutdown releases the latch so later
  Cmd+Q behaves exactly as before
- close-dialog smoke: legacy 7 scenarios isolated per-scenario userData in
  the same order, plus shutdown-restore and file-failure-restore pairs
  driven through the real beginSystemShutdown() path
When an approved quit is superseded by a denied system-shutdown pass, fall
back to the already-approved quit instead of retaining a dead latch/fence.

Smoke runner: share one userData across the legacy seven scenarios (cold
per-scenario isolation exposed a pre-existing save-close flake), bound
shutdown first-phase waits, force-exit failed Electron workers, and focus
CodeMirror before synthetic edits so shutdown pairs stay deterministic.
Keep the shutdown persist snapshot shape local to api-types so knip stays
clean; only the Api surface needs to re-export consumer-facing types.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c8117c19e4

ℹ️ 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".

Comment thread src/main/app-windows.ts

@project820 project820 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🤖 Psycho-842 [BOT] review

⚖️ Verdict: NEEDS REVISION — Codex empty-target power-off denial is real on HEAD; OS power-off can stick after preventDefault().

Summary

macOS power-off path still looks solid for the dirty-path / untitled / empty-window matrix covered by unit + close-dialog-smoke, and tip CI is green. Settlement re-measure cannot promote this PR: an unresolved Codex P1 on empty close targets is confirmed in CloseCoordinator.request + approveAllForQuit('shutdown') — empty target lists fall out of the decision loop as approved: false while the powerMonitor handler has already called preventDefault(), so the app can block OS power-off/restart/logout when there are no windows or only unready blanks.

Findings (by severity)

  • 🔴 P1 Highsrc/main/close-coordinator.ts (request, empty targets) + src/main/app-windows.ts (approveAllForQuit('shutdown')) + src/main/main.ts (powerMonitor.on('shutdown')preventDefault) — Codex thread confirmed. With zero close targets (no open windows, or every window filtered as unready blank), pending = [] skips the decision loop and returns { approved: false }. Because power-off already called preventDefault(), a denied approval leaves Notepad AI alive and can block the OS action. Fix: treat an empty shutdown transaction as approved and commit the empty/filtered session (then allow quit); add a unit/smoke case for zero-target shutdown.
  • 🟡 P3 Lowsrc/main/app-windows.ts (createWindowloadFile) — Short-lived empty third window in close-dialog smoke can be destroyed mid-loadFile, surfacing UnhandledPromiseRejectionWarning: ERR_FAILED. Smoke still passes; guard the load promise.
  • 💡 SUGGESTION.github/workflows/ci.ymltest:close-dialog-smoke (legacy 7 + shutdown-restore / file-failure-restore) is still outside the verify job.
  • 💡 SUGGESTION — Real-device matrix in the PR body (power-off / restart / logout) is still empty; latched smoke is not a live powerMonitor event.

Response to bot findings

  • Codex P1 — Allow shutdown when there are no close targets (discussion): Accepted as P1 (unresolved). Measured on HEAD c8117c1:
    1. CloseCoordinator.request: pending = [...targets]; while (pending.length > 0 && …) never enters when targets is [], then return { approved: false, intent }.
    2. approveAllForQuit('shutdown') builds targets as records.filter(not unreadyBlank); empty registry or all-unready-blank → empty targets → denied.
    3. main.ts power-off listener always preventDefault() before beginSystemShutdown().
    4. Minimal empty-path reproduction: empty targets → approved: false. No test currently asserts zero-target shutdown approval.
    • Not dismissed. Blocks Stage 2 SAFE/CAUTION until fixed or explicitly waived by user.

Findings delta (since Stage 1)

  • Stage 1 on same HEAD had no unresolved P0–P2 (Codex thread landed in the same minute window and was not yet folded in).
  • Settlement: Codex P1 new / still open → max-severity P1NEEDS REVISION. No [FINAL] declaration.
Detailed analysis (CI, diff stats, verification log)

Meta (re-measured settlement)

  • Repo: project820/notepad-ai · PR #75
  • HEAD: c8117c19e471522e82785919f4913a8b14bddc70 (unchanged since Stage 1)
  • Diff: +1289 / −153 · 24 files · mergeable=MERGEABLE · mergeStateStatus=CLEAN
  • Unresolved review threads: 1 (Codex P1 empty close targets)
  • No [FINAL @…] on this HEAD prior to this update

Hotspots re-opened

  • src/main/close-coordinator.ts — empty-target control flow
  • src/main/app-windows.tsapproveAllForQuit / unready-blank filter / decideShutdown
  • src/main/main.tspowerMonitor + preventDefault + beginSystemShutdown
  • src/main/lifecycle-flags.ts — latch / supersede / fallback (unchanged concern)

Caller tracing (empty-path)

  • macOS powerMonitor 'shutdown' → preventDefaultbeginSystemShutdown → controller → approveAllForQuit('shutdown')CloseCoordinator.request(intent, targets, …)
  • Empty targets never reaches commitCloseTransaction / commitShutdownSession; denial bubbles to refused quit approval while the OS action is already deferred.

Adversarial pass (settlement — 4 angles)

  1. Async/ordering — In-flight quit + supersede/fallback still looks correct when targets exist (prior Stage 1 evidence). Empty-target path is the new hole.
  2. Boundaries/failure — File-save fail → dirty snapshot still OK when targets exist. Empty targets: fail-closed incorrectly for OS power-off (should approve + quit).
  3. Contract — Unready blanks are excluded from persist and only force-approved after result.approved; if approval never comes, they never get the free pass either.
  4. Claimed invariants — "power-off does not cancel the OS action" is broken for the no-window / all-blank cases.

CI (HEAD tip)

  • verify on c8117c1: SUCCESS (completed 30+ min stable at settlement)
  • Local Stage 1 evidence still cited: focused lifecycle tests 106 pass; test:close-dialog-smoke pass including restore pairs (does not cover zero-window path)

Unverified

  • Live macOS power-off / restart / logout matrix (PR table still empty)
  • Full npm run test re-run in this settlement session (tip CI already green)

Policy note

  • This COMMENT is not an approval. User final sign-off still required after the P1 is fixed.

🛠️ NEEDS REVISION

🦀 Reviewed by Hermes agent BOT 🦞

CloseCoordinator treated empty target lists as denied. On macOS the
powerMonitor handler has already preventDefault()'d, so a zero-window
or all-unready-blank shutdown left the app alive and blocked the OS.

Empty shutdown now commits the empty session (cleanExit:false + marker)
and returns approved; empty quit/close still deny. Unit coverage for
coordinator + approveAllForQuit zero-window / all-blank paths.
@project820

Copy link
Copy Markdown
Owner Author

Response to Codex P1 / Psycho NEEDS REVISION

Status: fixed on 5624c3a (pushed). Review thread resolved.

P1 — empty close targets block power-off

Accepted and fixed. Root cause matched the review: empty targets skipped the decision loop and returned approved: false while powerMonitor had already called preventDefault().

Fix: CloseCoordinator.request short-circuits empty shutdown targets: commit the empty session (markShutdownRestoreQueued([])cleanExit:false + one-shot marker) then return { approved: true, intent: 'shutdown' }. Empty quit/close remain denied. Existing approveAllForQuit('shutdown') still force-approves filtered unready blanks after a successful empty commit.

Tests:

  • close-coordinator.test.ts — empty shutdown approves + commits; empty quit/close deny; empty commit failure denies
  • app-windows-discard.test.ts — zero windows; all unready blanks (also force-approves those window ids)

Other notes from Psycho

  • P3 loadFile ERR_FAILED on short-lived empty third smoke window: known noise; smoke still green; not changed in this commit.
  • CI wiring of close-dialog-smoke / real-device matrix: unchanged (out of this fix scope).

Verification (5624c3a)

  • npm run typecheck green
  • npx vitest run — 149 files / 2427 tests
  • npm run build + npm run test:close-dialog-smoke full green

No merge until the real-device QA table is filled and an explicit PR #75 merge approval is given.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5624c3a6df

ℹ️ 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".

Comment thread src/main/app-windows.ts
Comment thread src/main/main.ts
Restored or file-backed windows that are still loading (ready=false) cannot
complete the renderer lease handshake, so decideShutdown used to cancel after
powerMonitor already preventDefault()'d and strand OS power-off.

Unready restorable windows now mint a main-owned lease and commit the durable
main-side snapshot (restoreSnapshot / lastSnapshot / path-only disk fallback)
without querying the renderer. Empty blanks stay excluded as before.
@project820

Copy link
Copy Markdown
Owner Author

Follow-up: Codex P1s on 5624c3a → fixed on 8fb0bcb

Responding immediately to the two new Codex threads.

1) Handle restorable windows before renderer readiness — fixed

Unready restored/file-backed windows now commit durable main-side snapshots with a main-owned lease (no renderer handshake). Empty blanks still excluded.

2) Reattribute commit to project820 — not a tip issue

Branch tip identity is already project820 noreply for author and committer on every commit in this PR range. Thread resolved as not applicable to HEAD.

Verification (8fb0bcb)

  • typecheck green
  • vitest 149 files / 2429 tests
  • build + full close-dialog-smoke green

Still no merge without real-device QA table + explicit PR #75 approval.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8fb0bcb1b2

ℹ️ 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".

Comment thread src/main/app-windows.ts Outdated
Comment thread src/main/session-schema.ts Outdated
Ready crash-recovery windows can still show a blank renderer while
restoreSnapshot holds the only recovered draft. Prefer that durable
snapshot over an empty live shutdown persist so markShutdownRestoreQueued
does not drop it.

Path-only file-backed snapshots (doc empty) are now restorable so an
unready loading window reopens after power-off instead of being filtered
out of the shutdown session.
@project820

Copy link
Copy Markdown
Owner Author

Follow-up: Codex P1/P2 on 8fb0bcb → fixed on 367c8e8

P1 — Preserve unanswered recovery snapshots during shutdown

Fixed. Prefer durable restoreSnapshot over empty ready-renderer shutdown persist.

P2 — Keep path-only shutdown fallbacks restorable

Fixed. isRestorableSessionWindow accepts non-empty path so loading file-backed windows reopen after power-off.

Verification (367c8e8)

  • typecheck green
  • vitest 149 / 2431
  • build + full close-dialog-smoke green
  • project820 author/committer

Polling continues until review quiet; auto-merge only after 10 minutes with no new review comments and green CI (per owner instruction for this PR).

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 367c8e83e0

ℹ️ 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".

Comment thread src/main/app-windows.ts Outdated
Comment thread src/renderer/main.ts Outdated
Comment thread src/renderer/session-snapshot.ts
- Prefer unanswered restoreSnapshot whenever the live renderer doc is empty
  (including path-backed recovery drafts)
- Bound renderer shutdown save to 3.5s so slow disk cannot miss main's
  deadline; always return a post-save snapshot (dirty cleared on success)
- Path-only shutdown restores reopen via openFileInCurrent
@project820

Copy link
Copy Markdown
Owner Author

Follow-up: Codex round on 367c8e8 → fixed on 2a799a6

  1. P1 path-backed unanswered recovery — prefer restoreSnapshot whenever live doc empty
  2. P1 slow file save deadline — 3.5s bounded save + always return snapshot
  3. P2 path-only restore apply — openFileInCurrent for path-only shutdown restores

Verification: typecheck, vitest 2433, full close-dialog-smoke green. CI re-running.

Resuming 2-minute poll / 10-minute quiet auto-merge watch.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2a799a6126

ℹ️ 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".

Comment thread src/main/main.ts
Comment thread src/main/app-windows.ts Outdated
Comment thread src/main/app-windows.ts
Comment thread src/renderer/session-snapshot.ts
- Prefer pending recovery when it has doc or chat and live has neither
- session:clear clears in-memory restoreSnapshot/restoreReason after decline
- Clean path-backed shutdown restores reopen from disk (dirty still applies snapshot)
@project820

Copy link
Copy Markdown
Owner Author

Follow-up: Codex round on 2a799a6 → fixed on ce5de20

  1. Identity phantom commit — not on branch tip (rebutted)
  2. Chat-only recovery preference — fixed
  3. Clear restoreSnapshot on decline — fixed
  4. Clean path reopen from disk — fixed

Verification: typecheck, vitest 2436, close-dialog-smoke green. Resuming 2m poll / 10m quiet auto-merge.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ce5de2067a

ℹ️ 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".

Comment thread src/renderer/session-snapshot.ts
Comment thread src/main/app-windows.ts
Comment thread src/renderer/session-snapshot.ts Outdated
Comment thread src/main/main.ts
…#73)

- Clean path-backed shutdown restore reopens disk file and still applies
  chat/view metadata from the snapshot
- Path-only ordinary crash restores reopen the file (no blank untitled)
- Live session:write clears pending restoreSnapshot after apply
@project820

Copy link
Copy Markdown
Owner Author

Follow-up: Codex round on ce5de20 → fixed on 8185337

  1. Preserve chat while reopening clean files — fixed
  2. Acknowledge recovery after apply — fixed (session:write clears restoreSnapshot)
  3. Path-only ordinary crash reopen — fixed
  4. Phantom Codex identity commit — not on branch (rebutted)

Verification: typecheck, vitest 2439, close-dialog-smoke green.

Resuming 2m poll / 10m quiet auto-merge.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8185337f13

ℹ️ 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".

Comment thread src/main/main.ts
@project820

Copy link
Copy Markdown
Owner Author

Identity-only Codex thread on tip 8185337

Cited commit 4abc5d7… is not on this branch. All PR commits remain project820 noreply. Thread resolved as not applicable.

Resuming 2m poll / 10m quiet auto-merge.

@project820
project820 merged commit ca15e89 into main Jul 24, 2026
1 check 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.

feat: macOS 시스템 종료 시 자동저장 및 전체 세션 복구

1 participant