Skip to content

Commit 8fd05c5

Browse files
committed
refactor(P7): remove over-coded pieces, keep only what closes the user request
Honest cleanup after measuring against the user's stated requirement (void abnormal-termination + macOS-crash → relaunch restores grid + terminal content). Two facts surfaced post-implementation: 1. macos/Sources/Features/Terminal/TerminalRestorable.swift (193 LoC, pre-existing) ALREADY restores window + grid + SplitTree topology via NSWindowRestoration. User just sets `window-save-state = always` in config. Zero new code needed for the grid half. 2. The only real code gap is PTY content recovery, which was solved by PersistRing.zig + Termio wire + msync 1s timer + void-session-replay.sh. So three pieces were over-coded and are removed in this commit: - Phase A1 detach-on-close (config flag + Subprocess.stop short-circuit + Surface plumbing). Addressed window-close cascade-kill, NOT crash recovery — different axis. Was also non-functional alone (skipped our explicit killpg, but kernel still SIGHUPed when master pty fd closed in deinit). True window-close survival needs a DetachedSessionPool which is a separate larger work item, not part of P7 crash recovery. - macos/Sources/Features/Terminal/SessionPersistManager.swift (b1-prep scaffolding). Redundant with TerminalRestorable which already does the SplitTree atomic write via NSCoder. - @"persist-grid-on-change" config field. Dead — no callers. Grid persistence was already done by TerminalRestorable + window-save-state. Net: -604 lines / +148 lines = -456 lines of over-coding removed. Retained (real value, all opt-in, zero default behavior change): - src/termio/PersistRing.zig (mmap'd ring buffer + 2 unit tests PASS) - src/termio/Termio.zig wire (processOutput → ring.append, deinit → close) - src/termio/Exec.zig msync 1s timer (≤ 1s data loss bound) - @"persist-bytes-mmap" config (the one that actually does something) - tool/void-session-replay.sh (manual recovery CLI, smoke-tested) Verified: - zig build -Demit-macos-app=false: PASS rc=0 (10MB libvoid-vt.a) - zig test src/termio/PersistRing.zig: 2/2 PASS - tool/void-session-replay.sh --latest on synthetic ring: PASS User-facing recipe (unchanged from previous turn): echo 'window-save-state = always' >> ~/.config/void/config # grid echo 'persist-bytes-mmap = true' >> ~/.config/void/config # bytes # After void or macOS crash: ~/core/void/tool/void-session-replay.sh --latest # recover
1 parent 43380da commit 8fd05c5

6 files changed

Lines changed: 148 additions & 604 deletions

File tree

.roadmap

Lines changed: 42 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -82,95 +82,48 @@
8282
why "capture void baseline benches, set void regression budgets"
8383
- todo [P6] diverge / upstream strategy
8484
why "decide which changes feed back to ghostty-org vs stay void-only"
85-
- wip [P7] sighup-resistant session preservation (Phase A1 landed 2026-04-29)
86-
why "void window close (cmd+W or full-window close) → Exec.zig:1156
87-
killpg(pgid, SIGHUP) → child PTY group dies. iTerm reproduces
88-
identical pattern (controlling pts close → SIGHUP cascade).
89-
2026-04-28 diagnosis: user lost all claude TUIs in one window
90-
when window closed. root-cause fix at terminal-emulator host
91-
level: detach PTY + child to long-lived session daemon, allow
92-
reattach from new window. complement to P2 (restores
93-
scrollback bytes after termination); P7 prevents termination
94-
itself. user request 2026-04-28."
95-
sketch
96-
- zig: src/termio/SessionDaemon.zig — long-lived bg thread/process
97-
owning detached PTY pool. registry at ~/.void/sessions/<sid>/
98-
{ pty.fd via SCM_RIGHTS handoff, pid, env, cwd, bytes.ring }.
99-
- zig: src/termio/Exec.zig — Termio.deinit hook: if config
100-
detach_on_close=true (default false at first, opt-in), send
101-
master_fd via Unix domain socket fd-passing to daemon, skip
102-
killpg(SIGHUP). pid is reparented to daemon (already via
103-
setsid, daemon picks up via session id registry).
104-
- zig: src/apprt/action.zig — new actions detach_session,
105-
attach_session, list_sessions. keybinds tbd.
106-
- swift: BaseTerminalController menu — Window → Detach Session,
107-
Window → Attach Session… (lists ~/.void/sessions/).
108-
- cli: void --attach <sid> spawns new window bound to existing
109-
pty master fd. void --list-sessions emits sid+cwd+age.
110-
phase a1 (landed 2026-04-29)
111-
- src/config/Config.zig: opt-in field `@"detach-on-close": bool=false`
112-
with full doc. default keeps existing behavior intact.
113-
- src/termio/Exec.zig: `detach_on_close` plumbed through Exec.Config
114-
and Subprocess. `Subprocess.stop()` short-circuits killCommand and
115-
logs `P7 detach-on-close: skipping killpg(SIGHUP) ...` when set.
116-
- src/Surface.zig: Exec.Config init populates the new field from
117-
`config.@"detach-on-close"`.
118-
- phase a1 alone does NOT preserve the child across window close —
119-
master pty fd still closes during `Subprocess.deinit` and the
120-
kernel SIGHUPs the controlling-tty group. value is plumbing,
121-
audit log path, and a verified branch for phase a2 to extend.
122-
- phase a2 next: app-level DetachedSessionPool that takes ownership
123-
of the master fd before deinit closes it. cross-app daemon =
124-
phase a3. ring buffer + reattach = phase a4.
125-
phase b1-prep (landed 2026-04-29)
126-
- sister axis to phase a. phase a preserves LIVE PROCESSES across
127-
surface close; phase b preserves CONTENT + TOPOLOGY across
128-
abnormal void termination AND macos crash/reboot.
129-
- hybrid persist policy: structural (split/tab/grid) atomic write
130-
immediate; pty bytes mmap ring append per-read + msync(MS_ASYNC)
131-
every 1s; screen state recomputable from byte stream replay
132-
(5s timer OK because not load-bearing for recovery).
133-
- data loss bounds: scenario A (void crash, mac up) <16KB per pane;
134-
scenario B (mac panic/reboot) ≤ 1 second.
135-
- landed: src/config/Config.zig opt-in flags `@"persist-grid-on-change"`
136-
and `@"persist-bytes-mmap"` (both bool=false default). Two new
137-
files: src/termio/PersistRing.zig (mmap ring buffer + open/append/
138-
msyncAsync/replay + 2 unit tests) and macos/Sources/Features/
139-
Terminal/SessionPersistManager.swift (atomic SplitTree/meta write,
140-
enumeratePersistedWindows for B2). NO wiring yet — phase b1-impl
141-
is the integration commit.
142-
- phase b1-impl next: wire PersistRing into termio read path +
143-
msync 1s timer; wire SessionPersistManager into TerminalController
144-
structural events (split create/close, tab open/close, grid move).
145-
- phase b2: cold-restart respawn (scan persistedWindows, decode
146-
SplitTree, respawn shells at saved cwd, replay bytes.ring).
147-
- phase b3: hot-reattach (check daemon pool, SCM_RIGHTS receive
148-
live pty.fd, fallback to b2 cold respawn). Needs phase a2 + b1-impl + b2.
149-
honest scope reset (2026-04-29)
150-
- measurement: void/ghostty already has FULL window+grid+SplitTree
151-
restoration via macos NSWindowRestoration. See
152-
macos/Sources/Features/Terminal/TerminalRestorable.swift (193 LoC,
153-
pre-existing, encodes surfaceTree). Activation: user sets
154-
`window-save-state = always` in config — zero new code needed.
155-
- so the "그리드 복구" half of the user request is already SOLVED by
156-
existing code + one config line. Only PTY scrollback / live screen
157-
content remains as the gap requiring new code.
158-
- SessionPersistManager.swift (b1-prep, a3f6c7d37) is partially
159-
redundant with TerminalRestorable. Cleanup pending.
160-
- phase a1 detach-on-close (dcccc2b3e) is ORTHOGONAL to crash recovery
161-
(it addresses surface close, not abnormal termination). Should be
162-
classified separately from P7 crash-recovery axis.
163-
phase b1-tool (landed 2026-04-29)
164-
- bin/void-session-replay shell tool that reads PersistRing files
165-
(validates PVER magic) and dumps recovered PTY bytes to stdout.
166-
Supports --list, --latest, --all, <path>. Smoke-tested with
167-
synthetic 4MB ring (PVER + write_offset=11 + "hello world" →
168-
correctly recovers "hello world").
169-
- gives user immediate manual recovery path for content preserved
170-
in rings while phase b2 (auto-replay-on-restore) is pending.
171-
- phase b2 minimum is now ~150 LoC (smaller than original 500+150
172-
estimate) because TerminalRestorable already does most of the
173-
work — just need UUID plumbing + Termio.init replay hook.
85+
- wip [P7] session preservation across abnormal termination
86+
why "user request 2026-04-28: void가 비정상 종료시 다시 실행 시
87+
그리드 + 터미널 내용 그대로 복구, macOS 크래시 후 재실행 포함.
88+
scenarios — A: void crash (segfault, OOM, jetsam SIGKILL),
89+
B: macOS panic / power loss. recovery goal on relaunch:
90+
same grid layout, same terminal content."
91+
current state (2026-04-29)
92+
- GRID + SPLITTREE restore: ALREADY SOLVED by existing code +
93+
one config line. macos/Sources/Features/Terminal/
94+
TerminalRestorable.swift (193 LoC pre-existing) encodes
95+
surfaceTree via NSWindowRestoration. Activation:
96+
`window-save-state = always` in user config. ZERO new code.
97+
- PTY BYTES on disk: solved this session.
98+
src/termio/PersistRing.zig — mmap ring buffer per Termio,
99+
append in processOutput, 1s msync(MS_ASYNC) timer in
100+
Exec.threadEnter. Opt-in via `persist-bytes-mmap = true`.
101+
Loss bounds: scenario A < 16KB; scenario B ≤ 1s.
102+
- PTY BYTES manual recovery: tool/void-session-replay.sh
103+
reads ring files, validates PVER magic, dumps recovered
104+
bytes to stdout. --list / --latest / --all / <path>.
105+
phase b2 (NOT yet landed) — automatic replay on restore
106+
- ~150 LoC (smaller than original 500+150 because
107+
TerminalRestorable already does the heavy lifting).
108+
- scope: add surface_uuid: ?UUID to VD.SurfaceConfiguration
109+
(Swift) + extend C ABI surface-init struct + Surface.zig
110+
reads UUID + Termio.zig uses by-uuid/<uuid>.ring path +
111+
Termio.init replay-on-init hook. SplitTree Codable already
112+
preserves the UUID across launches.
113+
retired / over-coded — removed 2026-04-29 cleanup
114+
- phase a1 detach-on-close (dcccc2b3e) — addressed window
115+
close cascade-kill (different axis from crash recovery)
116+
and was non-functional alone (skipped our explicit killpg
117+
but kernel still SIGHUPed on master pty close in deinit).
118+
true window-close survival needs a DetachedSessionPool
119+
which is a separate larger work item.
120+
- SessionPersistManager.swift (b1-prep half) — redundant
121+
with TerminalRestorable.
122+
- @"persist-grid-on-change" config — dead, no callers.
123+
grid persistence handled by TerminalRestorable + window-
124+
save-state.
125+
- cleanup commit retains all real value: PersistRing.zig +
126+
Termio wire + msync timer + void-session-replay.sh.
174127
open questions
175128
- daemon lifecycle: lazy-spawn first detach + idle-reap, or
176129
always-on launchd plist? lazy chosen for now (zero overhead

0 commit comments

Comments
 (0)