Skip to main content
CHANGELOG

What shipped

Released versions only, generated at build time from the product Keep a Changelog file. Unreleased work is not listed here: if it is not in a version heading, it has not shipped.

Latest released v3.23.4 · · 61 releases

Currently downloadable v3.25.2-preview.1 · preview channel

Those are two different questions. This page lists versions that have been released, from the product changelog. The download page serves whatever the live preview channel points at, which moves ahead of the last released version with every build. Download the current build from /download. Channel and exact version always come from the live release manifest.

Releases 46–50 of 61

Page 10 of 13

v2.10.2

First-launch input race fix + helper-orphan cleanup

Two prod-only bugs surfaced during fresh-PC dogfood of v2.10.1. Neither reproduced under dev (npm start) because the vite dev-server load delay hides the underlying daemon-bootstrap timing.

Fixed

  • First-launch keystroke loss on fresh installs. v2.10.1's DaemonRespawnController introduced a race between renderer mount and the LOCAL→DAEMON IPC handler swap. On cold-start PCs the daemon spawn stretches into hundreds of ms (Defender realtime scan + ASAR cold cache

    • ConPTY cold start), wide enough for the renderer to mount and reach handler-swap mid-startup. Any pty.write that carried a LOCAL-prefix id (pty-N) into the DAEMON handler was silently dropped because sessionPipes.get('pty-N') is undefined — manifesting as "the first keystroke does not register" or "only the first keystroke registers" on the very first session. Fix splits renderer navigation out of createWindow() into a standalone loadMainRenderer() export and defers the call until after bootstrap() returns and markDaemonReady() has unblocked daemon.whenReady(). Every pty.create from the renderer now hits a stable handler topology and produces a correctly-prefixed id. The macOS app.on('activate') re-open path keeps the immediate-load default because the daemon is already healthy by then.
  • Helper-orphan zombies on quit. before-quit has five awaits (renderer save, sleep, daemon shutdown race up to 8s, disconnect, cleanup) before app.quit(). Any hang (stuck pipeServer.stop(), detached webview blocking will-quit, ConPTY/OSC 7 finalization stall) leaves Electron's renderer / GPU / utility helpers as orphans. On Windows the dev npm start Ctrl+C path also leaks helpers because SIGINT only reaches npm.exe, not the electron tree. Reproduced locally as 20-helper orphan buildup spanning days. Add a 1.5s setTimeout after app.quit() that calls app.exit(0) if the graceful path has not finalized; unref() keeps the timer non-blocking so a normal sub-second quit isn't held open. The graceful path is unchanged — this only fires on hang.

Internal

  • .team/ added to .gitignore so worktree coordination metadata cannot leak into future commits — matches the existing .claude/ / .gstack/ exclusions (Codex /codex review P2 finding).

v2.10.1

Notification system expansion + CI hardening

Five-surface notification system (StatusBar bell, pane border ring, Windows taskbar flash, in-app toast, sidebar dot) with per-workspace mute, four new user settings, and a pure-function policy refactor of the notification dispatcher. Two CI hardening fixes also land.

Added

  • NotificationBell on StatusBar. The existing ● {unreadCount} element is now a clickable accessible button (native <button>, dynamic aria-label, focus-visible outline, 24x24 minimum click area, 999+ clipping). Click opens the notification panel.
  • Pane NotificationRing. Per-pane state machine: flash 500ms → glow steady → cleared on focus or read. Honors prefers-reduced-motion (instant transitions) and forced-colors: active (Windows high-contrast 2px border).
  • Auto-markRead on pane focus. Clicking a pane marks its notifications read and clears the ring entry — but only if at least one notification was actually marked, so plain focus clicks don't wipe a fresh flash.
  • Relative time format in NotificationPanel. Replaces hh:mm with just now / Xm ago / Xh ago / Xd ago / local date. Future-skew safe.
  • Taskbar flashFrame on Windows. Window unfocused + new notification arrives → taskbar flashes for attention. Auto-clears on window focus. BrowserWindow.isDestroyed() guard prevents Electron throw.
  • Per-workspace mute. Each workspace can be muted from SettingsPanel. Muted workspaces still record notifications in the panel; bell badge excludes them; toast/sound/ring/flashFrame are suppressed.
  • Four new settings toggles. Pane ring on/off, pane flash on/off, taskbar flash on/off, notification sound choice (default/none).
  • markAllRead() global + jumpToUnread() selector. Global mark-all button in NotificationPanel (separate from the existing per-workspace one). jumpToUnread navigates to the most recent unread workspace without marking read.
  • NotificationPanel a11y. role="dialog", initial focus on first unread, Esc closes, Tab cycles, screen-reader announces "{type}, {title}, {timeAgo}, {read|unread}" per row.

Fixed

  • Notification ring lifecycle. Ring entries are now cleared on every user-action read path (Pane.handleClick, markAllRead, setActiveWorkspace, removeWorkspace) so panes can no longer get stuck in 'glow' after the user already handled the notification.
  • Listener refactor. useNotificationListener is now a thin IPC dispatcher that delegates decisions to useNotificationPolicy (pure function, testable in isolation). Replaces the module-scope mutable lastSoundTime map with createThrottler(ms) closures (per-NotificationType for sound, global 500ms for flashFrame burst protection).
  • runSnapshotOnce test 7-day time bomb. The test used a hardcoded lastActivity: '2026-05-15T00:00:00Z' for a suspended session, which SUSPENDED_TTL_HOURS = 168 pruned exactly 7 days later. Test now uses a dynamic Date.now() - 1h so the fixture never expires.
  • ProcessMonitor CI flake. watch() left the first probe to the first setInterval tick; under CI CPU contention two tasklist execs could exceed the test's 5s timeout. watch() now triggers an immediate first probe (production benefit: dead-PID detection is no longer up-to-5s delayed). Test timeout bumped to 20s with documented latency reasoning.

Internal

  • 12 IRON-RULE regression tests lock down previously untested but correct behavior in the notification stack (cap eviction, throttle, target resolution, active-surface skip, toggle plumbing).
  • Test suite total: 1665 tests, 136 files. Five consecutive stable full-suite runs verified post-fix.

v2.10.0

tmux prefix expansion + 16 new locales

This release rounds out the tmux-style prefix layer with pass-through and three new bindings, fixes a long-standing dead-event handler on the workspace rename shortcut, and ships UI translations for 16 additional locales.

Added

  • tmux pass-through. Pressing the prefix combo twice (Ctrl+B Ctrl+B by default) now forwards a literal Ctrl+B byte to the active terminal, so a nested tmux/screen session running inside a wmux pane receives its own prefix instead of being swallowed by wmux.
  • Three new prefix bindings. , opens inline rename for the active workspace, & closes the workspace (disposing every PTY in its tree first), ? redisplays the keyboard cheat sheet even after it has been permanently dismissed.
  • 16 new UI locales: Arabic, Bosnian, Danish, German, Spanish, French, Hindi, Indonesian, Italian, Malay, Norwegian Bokmål, Polish, Brazilian Portuguese, Russian, Thai, Turkish, Ukrainian, Vietnamese, and Traditional Chinese. Switch from Settings → Appearance → Language.

Fixed

  • Workspace rename actually works now. Both the new Ctrl+B , prefix action and the existing Ctrl+Shift+R shortcut previously dispatched a custom event that no component was listening for, so neither path opened the inline rename input. The sidebar's WorkspaceItem now subscribes to the event on the active workspace.
  • ? prefix re-opens the cheat sheet after permanent dismissal. The AppLayout mount gate previously prevented the cheat sheet from rendering at all once the user clicked "Don't show again", so the one-shot force-show flag had nothing to react to. The gate now honors the override.
  • ? prefix works after a previous cheat sheet auto-expired. The 30-second countdown now clears the force-show flag when it hits zero, so the next ? press flips the selector and re-triggers the show effect.

Changed

  • createPrefixActions factory. The prefix-mode action registry in useKeyboard.ts is now an exported factory taking {store, electronAPI, doc}, so unit tests can drive every action with mocks instead of needing a DOM harness. 32 new unit tests cover every action plus the ctrlByteForKeyCode pass-through helper.

v2.9.1

Scrollback restore hotfix

v2.8.x 이후 silently broken 이었던 scrollback restore 를 살리는 hotfix release. tray Quit → restart 시 모든 pane 이 fresh empty terminal 로 뜨던 증상의 진짜 root cause 3개를 모두 잡았다 (다층 race). 사용자 dogfood 로 end-to-end 검증 완료.

업그레이드 영향:

  • 모든 변경은 v2.9.x backwards-compatible. 새 wire contract / disk schema 없음.
  • 새 설정 한 개: Settings → Terminal → "시작 시 복원" (Restore on launch, default ON). 끄면 매 launch fresh 시작.
  • 누적된 session.json ↔ daemon dump mismatch 가 있어 복원 안 보이는 사용자를 위해 scripts/scrollback-reset.mjs 한방 cleanup util 제공 (백업 후 정리, 비파괴).
  • 로그 파일이 자동으로 14일 retention 으로 정리됨 (이전엔 무제한 누적, 일부 사용자에서 ~700MB 까지 부풀었던 사례).

Added

  • Scrollback restore 토글 (uiSlice.scrollbackRestoreEnabled, default true) — Settings → Terminal 에서 끌 수 있음. OFF 시 startup 에 clearAllPtyState() 로 모든 pane fresh 시작. daemon 은 ringBuffer dump 계속 (renderer 가 안 읽어서 orphan .buf 는 다음 launch cleanOrphanedBuffers 가 청소). en/ko/ja/zh i18n.
  • Log auto-prune (main/util/logSink.ts, daemon/util/logSink.ts) — 14일 이상 된 daily log 파일 startup 시 자동 삭제. 이전엔 retention 정책 없어 무제한 누적.
  • scripts/scrollback-reset.mjs — 비파괴 cleanup util. ~/.wmux/buffers/, sessions.json*, %APPDATA%/wmux/session.json* 를 ~/.wmux/backup-<timestamp>/ 로 이동 (삭제 아님). 사용자가 session.json ↔ daemon dump mismatch 누적된 상태를 한 번에 청소할 수 있음.
  • scripts/scrollback-restore-test.mjs — bundled daemon subprocess + RPC probe 기반 dynamic test. recovery + flush bytes contract regression 가드.

Fixed

  • L1 — workspaceSlice.loadSession ptyId wipe 제거. 매 startup 마다 모든 surface.ptyId 를 "" 로 force-clear 하던 코드가 reconcile 의 reconnect 경로 진입 자체를 막고 있었다. saved ptyId 는 이제 보존된다. 대신 AppLayout 이 paneGate ('pending' | 'ready') render gate 로 PaneContainer mount 를 reconcile 완료 이후로 미뤄서 옛 propagation race 를 원천 봉쇄한다. 추가로 clearAllPtyState cross-slice atomic clear action 이 reconcile 실패/timeout 시 explicit fallback.
  • L2 — BEFORE_QUIT_TIMEOUT_MS 4s → 8s (cherry-picked from fix/daemon-shutdown-phase-instrumentation #45). 50-pane daemon 에서 4초로는 buffer dump 가 못 끝나 다음 launch 가 recovery 할 게 없던 상태. 동시에 daemon-side logSink (daemon/util/logSink.ts) + [shutdown.phase] per-phase 지표 + [recovery] session X bytes=N 가시화 도구 도입 — 이게 없었으면 다음 layer 진단 자체가 불가능했다.
  • L3 — pty.reconnect race-free 재구성. AppLayout.reconcilePtys 는 이제 sync liveness check 만 (dead ptyId clear, live 는 그대로). 실제 reconnect 호출은 useTerminal mount 안에서 모든 listener 등록 후 발생. 이전 구조는 daemon SessionPipe replay (10KB+) 가 win.webContents.send(PTY_DATA, …) 로 forward 됐을 때 renderer ipcRenderer.on(PTY_DATA) listener 가 아직 없어 Electron IPC 가 silently drop 하던 게 진짜 사용자 가시 root cause 였다.
  • pty.reconnect failure 처리 — {success: false} 응답을 더 이상 swallow 하지 않는다 (useTerminal 가 clearSurfacePtyIdByPty 호출 → Terminal self-create fallback). 이전엔 dead session 이 stale ptyId 로 input-mute 영구 유지될 수 있었음 — 정확히 Fix 0 이 없애려던 클래스.
  • daemonMode flag race — isDaemonModeActive 를 startup IIFE 안에서 paneGate 가 ready 로 바뀌기 전 에 명시 set. 이전엔 별도 effect 가 set 해서 Terminal 이 daemonModeAtMount=false 로 mount 되고 reconnect 자체를 안 부르던 케이스 가능.
  • Startup IIFE outer try/finally — session.load() rejection 이 .then 안의 try 를 우회해서 paneGate 가 영구 pending 으로 갇히던 edge 봉쇄.
  • useRpcBridge startup-window 가드 — external RPC (MCP, A2A) 가 startup 중에 stale ptyId 로 write 들어오는 걸 {error: 'wmux is still starting', retryable: true} 로 차단.
  • main/util/logSink.ts stdout tee — 이전엔 stderr 만 tee 해서 console.log 결과가 disk 에 안 남았다 (console.warn/error 만 capture). renderer 진단 라인이 main log file 에 같이 누적되도록 console-message level<2 return 필터도 제거.

Out of scope (다음 PR 후보)

  • Fix B (cap-aware suspended-session promote) — 50-pane 이상에서 MAX_RECOVER_SESSIONS=40 초과 session 은 여전히 복원 못 함. design doc docs/internal/scrollback-restore-design.md §5 에 spec. TODOS.md 에 항목 등록. 50-pane thundering herd (codex P1#3) 와 함께 처리.
  • Substrate Phase 2+ Fix C — 2-storage 통합. weeks 단위 작업. 별도 트랙.
  • AppLayout.gate integration test — vitest config 가 현재 environment: 'node' 라 jsdom + RTL setup 필요. follow-up.

외부 협의 / Reviews

  • Codex outside-voice — plan 단계에서 13 holes 지적 → plan v2 resolution map 에 모두 매핑. 최종 pre-merge review 에서 추가 P1 3 + P2 3 — P1 + red test 는 fix, P1#3 (thundering herd) 와 P2#6 (session-end timeout) 은 known limitation 으로 명시 + 다음 PR 로 deferred.

PR: #46 (path-D inventory, docs), #45 (daemon instrumentation + before-quit timeout 8s), #47 (Fix 0 — three-layer race fix + toggle + log prune).

v2.9.0

Substrate 3.0 — Phase 0 + M0

wmux의 substrate identity 를 v3.0 으로 끌고 가기 위한 첫 번째 ship unit. v2.8.x 에서 이미 50% 가 출하돼 있던 substrate 표면 (PaneMetadata, EventBus, bootId, asOfSeq, system.capabilities, MCP host, mcp.claimWorkspace) 위에 (a) 그 표면의 contract 를 명문화한 Phase 0 문서, (b) main process 측 metadata authority 인 MetadataStore 와 그 wire 통합 (M0-af), (c) v2.8.x dogfood 중 노출된 스크롤백 손상 + reconcile race + logSink durable write 안정성 픽스를 한꺼번에 ship. 메인 PR 은 #34 (Substrate 3.0 — Phase 0 + M0, v2.9.0 ship unit) 이고 후속 마이그레이션 도구는 #35 (chopped-dump recovery tool) 로 따라간다. 외부 RFC 협의는 #15 (@alphabeen) 에서 진행됐고 그 OCC + mergeMode 디자인이 코드로 착지.

업그레이드 영향:

  • 와이어 contract 는 v2.x 와 backwards-compatible 이다 (expectedVersion, mergeMode, pane.metadata.changed 의 version 모두 additive optional).
  • 디스크에 새로 등장하는 폴더: userData/wmux/scrollback/corrupted/ 와 scrollback/*.txt.bak[.1..3] 회전 슬롯. 둘 다 자동 관리.
  • v2.8.x 사용자가 첫 부팅 때 일부 패널 스크롤백이 비어 보일 수 있다 — 이미 디스크에 chopped 형태로 저장돼 있던 dump 가 v2.9.0 detector 에 의해 격리되기 때문. 데이터는 격리 폴더에 보존되며 scripts/recover-scrollback.mjs 로 사람이 읽을 수 있는 텍스트로 복원 가능. 자세한 가이드는 docs/upgrade-v2.9.0.md 참조.

Added

  • Substrate 3.0 contract documentation — docs/PROTOCOL.md (substrate wire contract: layered status, namespacing, optimistic concurrency, mergeMode, cursor opaqueness, snapshot reconciliation, permission enforcement sketch, Named Pipe token security model), docs/api/{inventory,versioning,stability}.md (모든 RPC/MCP/event 의 stability tier + semver + 자동 업데이트 호환 정책), docs/internal/{m0-design,paneSlice-callsite-inventory}.md (M0 race specs + paneSlice 변경 blast-radius).
  • MetadataStore 모듈 (M0-a) — main process 의 PaneMetadata authority. get / set / clear / snapshot / hydrate / serialize / migrate / onPaneDeleted, per-pane monotonic version, expectedVersion 기반 OCC, 세 가지 mergeMode (merge / replace / replaceShared). 31 unit test 가 CRUD + version + mergeMode 트랜잭션 + OCC + 검증 + snapshot + persistence + EventBus emission 을 cover, codex full-stack review 가 catch 한 3건 (replaceShared 의 custom 보호, 누적 size cap, updatedAt 추가 후 cap 적용) regression test 포함.
  • pane.resolveActiveLeaf IPC 채널 (M0-b) — caller 가 paneId 를 생략하면 main 이 renderer 에 active leaf id 를 query (read-only, paneSlice 쓰기 0) 한 뒤 MetadataStore 에 commit. codex P1 review 가 잡은 split-store read-after-write 구멍 닫힘.
  • MetadataStore.snapshot() ↔ pane.list 통합 (M0-c) — pane.list envelope 가 store snapshot 으로 anchored, asOfSeq 가 snapshot lineage 를 반영. renderer 가 더 이상 metadata 를 자체 합성하지 않음.
  • SessionManager.saveMetadataSync 와이어 (M0-e) — MetadataStore 의 persist callback 이 metadata.json 에 atomic write, launch 시 store 가 그 파일에서 hydrate. codex P2 review 가 잡은 strict field validation 포함.
  • Wire format 추가 (M0-f) — pane.setMetadata 가 optional expectedVersion + mergeMode, reply / event / list 가 optional version 필드. v2.x subscriber 영향 없음 (모두 additive).
  • Optional version 필드 on pane.metadata.changed events.
  • PR template with CHANGELOG + stability-tier sections.
  • atomicWriteText / atomicReadText (sync + async) — core.ts 의 JSON 변종과 짝이 되는 텍스트 변종. rotation chain + quarantine 파이프라인 공유. JSON 변종이 parseable payload 를 전제하기 때문에 raw-bytes contract 가 필요한 스크롤백을 위해 sibling 으로 분리.
  • Cols-collapse corruption detector (src/main/scrollback/corruption.ts) — chopped dump 의 on-disk 시그니처 (median 비공백 행 길이 ≤ 3자, CRLF 바이트 비율 ≥ 0.3) 휴리스틱 검출기. 단일 패스 스캔, allocation 최소. 15 unit test 가 production v2.8.4 fixture (median=1, max=60 까지 outlier 살아남은 chopped 파일) 와 false-positive 저항 (정상 출력, sparse 세션, narrow pane, ANSI-rich 로그, 단일 긴 줄) cover.
  • scrollbackDump util 모듈 (src/renderer/utils/scrollbackDump.ts) — renderer 의 dump serializer 를 AppLayout.tsx 에서 분리. eligibility 가드 (cols < 12 / rows ≤ 0 / terminal.element.offsetWidth === 0 / detached) 가 unit-testable. 13 test 가 각 가드 branch + happy path 를 pin.
  • scripts/recover-scrollback.mjs (#35) — read-only 마이그레이션 CLI. v2.8.x → v2.9.0 첫 부팅에서 corrupted/ 로 격리된 chopped dump 를 reverse-reflow 로 사람이 읽을 수 있는 텍스트로 복원. node:util parseArgs 기반, dry-run / verbose / 입출력 dir 오버라이드 지원. 19 unit test (detector parity + 순수 transform + processFile e2e + CLI plumbing). 출력은 별도 폴더로만 쓰고 격리 원본은 절대 수정하지 않음.
  • docs/upgrade-v2.9.0.md (#35) — v2.8.x → v2.9.0 사용자 마이그레이션 가이드. corrupted/ 폴더의 의미, 첫 부팅 시 무엇을 보게 되는지, 복원 스크립트 사용법, 복원 한계, 롤백 절차, FAQ.

Changed

  • README opening 이 LSP-for-terminals substrate 프레이밍 으로 시작 (AI agent 가치 제안과 tmux 대체 키워드는 보존).
  • pane.{set,get,clear}Metadata 핸들러 (M0-b) 가 MetadataStore 로 라우팅. paneSlice 는 더 이상 RPC metadata path 에 의해 mutate 되지 않음.
  • paneSlice 가 mirror-only (M0-d) — 컴파일-타임 write protection 추가. M0-b 가 이미 모든 write path 를 우회시켜 M0-d 는 거의 no-op.
  • pane.list envelope (M0-c) 가 MetadataStore.snapshot() 으로 anchored. snapshot lineage 를 asOfSeq 가 반영.
  • SessionManager (M0-e) 가 metadata.json 을 MetadataStore persist callback 으로 atomic write, launch 시 store 를 그 파일에서 hydrate.
  • SCROLLBACK_DUMP IPC 핸들러 가 직접 writeFileSync 대신 atomicWriteTextSync 사용. rotation chain (.bak / .bak.1 / .bak.2 / .bak.3) 활성화. pre-write corruption 시그니처 검출 시 payload 거부 (defense in depth — renderer 가드 회귀 대비).
  • SCROLLBACK_LOAD IPC 핸들러 가 atomicReadTextSync + validate hook 으로 load. chopped 시그니처 매칭 시 primary 를 corrupted/{ts}.bak 으로 격리 후 .bak 체인 fallback 으로 시도. 구조화 CORRUPT_FILE 로그를 stderr 로 emit. 손상 파일이 fresh xterm 에 복원돼서 다음 5초 dump 가 chopped 상태를 다시 디스크에 쓰는 자기증식 루프를 끊음.
  • vitest.config.ts 가 scripts/__tests__/**/*.test.mjs 도 include — 운영 도구 (마이그레이션 스크립트 등) 가 같은 test runner 아래에서 회귀 보호됨.

Fixed

  • replaceShared mergeMode 가 caller 의 custom patch 를 덮어쓰던 결함 (codex full-stack review P2) — patch.custom 을 silently ignore 해 tool-namespace clobber 방지. substrate 의 namespace boundary guarantee.
  • MetadataStore size cap (PANE_METADATA_MAX_BYTES) 이 updatedAt 추가 전에 검증되던 결함 (codex P2) — 최종 저장 shape (updatedAt 포함) 에 대해 검증. boundary 안전.
  • MetadataStore custom entry cap 이 patch 에만 적용되던 결함 (codex P2) — 누적 merge 가 cap 을 우회하지 못하도록 post-merge shape 에 대해 검증.
  • Split-store read-after-write hole (M0-b codex P1) — paneId 없이 write 한 뒤 paneId 있는 read 가 stale 을 반환할 수 있던 구멍. 3 개의 metadata 핸들러 모두 pane.resolveActiveLeaf 로 통일.
  • workspaceId ?? '' 가 기억된 scope 를 덮어쓰던 결함 (M0-b codex P2) — coercion 제거; MetadataStore 의 기존 fallback 이 정상 동작.
  • 스크롤백 손상 자기증식 루프 (P0 layered defense) — hidden / zero-width 컨테이너에 대한 fit() 이 cols 를 ~2 로 collapse 시키면, renderer 의 5초 autosave 가 그 reflowed 버퍼를 캡처해 column-of-chars 로 디스크에 dump. 다음 부팅에 fresh xterm 에 복원되고 또 다시 5초 후에 dump 되며 영구적 손상 루프. 픽스는 네 층: (a) dump-time eligibility 가드 (cols < 12 / rows ≤ 0 / offsetWidth === 0 / detached element), (b) font/theme-change fit() 의 visibility 가드 (마지막 unguarded fit 사이트 닫힘), (c) IPC SCROLLBACK_DUMP 의 시그니처 거부, (d) IPC SCROLLBACK_LOAD 의 시그니처 검출 + 격리 + .bak 회전 체인 fallback. 시각 증상은 "재부팅하면 일부 패널 스크롤백이 비어 보임". 자세한 forensic 은 PR #34 참조.
  • 부팅 직후 일부 패널이 input-mute 였던 결함 (reconcile race) — daemon.whenReady() 와 daemon.onConnected 가 첫 연결에 같은 reconcile 을 동시에 trigger, 두 walk 가 같은 session 에 대해 race 하면서 한쪽이 ptyId 를 clear. 사용자 증상: 부팅 후 워크스페이스 전환을 한 번 해야 일부 패널이 살아남. 픽스: reconcileInFlightRef 가 중복 trigger 를 drop, workspace snapshot 을 walk 마다 다시 읽어 동시 spawn 이 frozen view 에 가려지지 않음.
  • pty:resize 가 recovery PTY mute race 를 유발하던 결함 — daemon 이 아직 session 을 publish 하기 전에 renderer 가 보낸 pty:resize 가 "session not found" 로 실패하고 recovery PTY 가 muted 상태로 남던 결함. 50 × 20ms retry budget + 진단 로그 추가.
  • IPC session + scrollback 핸들러가 daemon-connect handler-swap cycle 의 unregister 윈도우에 떨어지던 결함 — cold boot 시 scrollback:load 가 "No handler registered" 로 거부되고 다음 5초 autosave 가 빈 버퍼를 디스크에 덮어쓰던 결함. session + scrollback 핸들러를 swap cycle 밖으로 이동.
  • logSink 의 EPIPE 무한 루프 — stdout 이 닫힌 상태에서 console.error 가 logSink 를 호출하고 logSink 가 다시 console.error 를 호출하던 reentrancy 루프. reentrancy 가드 + orig() try/catch 추가. appendFileSync 사용으로 로그가 디스크에 durable.

Migration Notes

  • 자동 마이그레이션. 사용자 액션 불필요한 부분: substrate wire 변경 (모두 additive optional), MetadataStore 통합 (paneSlice consumer 영향 없음), atomic write + .bak rotation (v2.7.x 부터 이미 다른 파일에 적용된 패턴).
  • v2.8.x 의 chopped 스크롤백: 첫 부팅에서 자동 격리된다. 데이터를 v2.9.0 이 버린 게 아니라 v2.8.x 시점에 이미 chopped 형태로 저장돼 있던 것을 v2.9.0 이 검출만 한 것. 사람이 읽을 수 있는 텍스트로의 회수는 node scripts/recover-scrollback.mjs --verbose 로 가능 (자세한 가이드는 docs/upgrade-v2.9.0.md).
  • corrupted/ 폴더: 30 일 / 폴더당 10 파일까지 자동 정리. 수동 삭제도 안전.
  • pane.metadata.changed event subscriber: optional version 필드가 추가됐다. 무시해도 v2.x 와 동일 동작.