v2.8.4
[2.8.4] — 2026-05-12 — Agent Notification Pipeline Restoration
사용자가 보고한 "Claude 가 작업을 끝내도 사이드바 dot, unread 배지, OS 토스트 — 3가지 신호 전부 안 뜬다" 결함을 root-cause 수준에서 복구. main 의 감지 레이어 (PTYBridge, AgentDetector, ActivityMonitor) 가 emit 하는 신호를 renderer UI 까지 연결하는 wiring 이 4 군데 끊겨 있었고, wmux production 인 daemon mode 에서는 PTYBridge 가 아예 우회되어 본 fix 가 0 효과 라는 더 큰 결함도 포함. 메인은 PR #30 (4 commits, +1579/-141, 29 files) 이고, 같은 릴리즈에 두 개의 다른 PR — #28 (@dev-minggyu, workspace drag reorder 복구 — 외부 기여 첫 컨트리뷰션) 과 #29 (multiview sticky group + MiniSidebar feature parity) — 도 함께 ship 됐다.
Fixed
- Workspace 드래그 정렬이 동작하지 않던 결함 (#28, @dev-minggyu — 외부 기여 첫 컨트리뷰션) — 좌측 사이드바의 전역 파일-드롭 핸들러가 내부 워크스페이스 드래그 이벤트까지 OS 파일 드롭처럼 처리하면서
move드래그가 충돌해 정렬이 막혀 있었다. 신규src/shared/dragDrop.ts헬퍼가DataTransfer가 실제 OS 파일 드래그인지 판별, 전역 드롭 핸들러와 오버레이가 파일 드래그에만 반응하도록 제한. 내부text/plain드래그 회귀 테스트 21 라인 추가. - Multiview sticky group + MiniSidebar feature parity (#29) — 사용자가 보고한 multiview 3개 결함을 묶어 수정. (a) Ctrl-click 순서 무시되고 grid 가 항상 workspace 배열 순서로 렌더되던 결함 →
AppLayout이multiviewIds자체를 iterate 해서 Ctrl-click 순서 보존. (b) 그룹 밖 workspace 를 plain-click 하면 그룹이 통째로 사라지던 결함 →setActiveWorkspace가multiviewIdsclear 안 함 +activeWorkspaceId ∈ multiviewIds일 때만 grid 렌더 (그룹 외부 클릭 시엔 단일 view, 멤버 재클릭 시 grid 복구). (c) 접힌 사이드바 (MiniSidebar) 가 multiview indicator / drag-reorder / W1·W2 라벨 / unread 배지 / agent dot 전부 없던 결함 → 펼친 사이드바와 동일 기능 부여,AGENT_STATUS_ICON을Sidebar/agentStatusIcon.ts로 추출해 두 사이드바 lockstep. Codex review 가 잡은 reseed 결함 (stale 그룹에서 새 multiview 시작 시 Ctrl-click 무반응) 도 함께 수정. +5 multiview 회귀 테스트. - AgentDetector status event 가 아무에게도 listen 되지 않던 결함 —
src/main/pty/PTYBridge.ts:207가agentDetector.onCritical만 구독하고onEvent는 dead code. Claude/Codex/Aider 의 "esc to interrupt" / "shift+tab to cycle" / "Applied edit to" 같은 정확한 prompt 패턴은 감지되어 emit 되었지만 호출되는 콜백이 0 개라 사이드바 dot 이 영영 켜지지 않았다. PTYBridge 가onEvent도 구독하도록 추가,IPC.METADATA_UPDATE로agentStatus/agentNamebroadcast +sendNotification호출. IPC.NOTIFICATIONpayload shape 가 sender 마다 달라서 외부 RPC 알림이 깨지던 결함 —PTYBridge는(channel, ptyId, notification)3-arg,notify.rpc.ts는(channel, { title, body, type })1-arg. preloadnotification.onNew는 3-arg signature 라 RPC path 의 첫 인자가 ptyId 자리로 들어가 payload 가 silent 하게 깨졌다. 새sendNotificationutility (src/main/notification/sendNotification.ts) 가 단일(window, ptyId|null, payload)contract 로 통일.IPC.METADATA_UPDATE가 두 sender 사이에 shape 불일치였던 결함 —metadata.handler는(ptyId, data)2-arg,meta.rpc는(payload)1-arg 로 같은 채널에 송신. 한 path 가 정상 동작하는 동안 다른 path 가 silent 하게 깨졌다.MetadataUpdatePayload(src/shared/types.ts) 를 단일 discriminated payload 로 정의,broadcastMetadataUpdateutility 로 모든 sender 통일. meta.rpc 의{kind: 'status'|'progress'}discriminator 폐기, workspace-level field 로 직접 매핑.- WorkspaceMetadata.agentStatus 가 자동으로 'idle' 로 복귀하지 않던 결함 —
'waiting'/'complete'/'running'이 한 번 set 되면 lifecycle reset 없음. 사용자 입력 후 agent 가 다시 실행되어도 dot 은'waiting', PTY 가 죽어도 dot 은'running'으로 남는 거짓말 발생. ActivityMonitor 의 새onActive콜백이 burst 진입 시점에'running'설정,PTYBridge.onExit가'idle'broadcast,cleanupInstance도 dispose path 에서 동일하게 broadcast (idempotent). renderer 의AppLayout가 session restore 직후 모든 workspace 의 stale agentStatus 를 sanitize. - Daemon mode 에서 알림 wiring 이 통째로 빠져 있던 결함 (production blocker) — wmux 의 production normal 은 daemon mode. PTY output 은
DaemonPTYBridge를 통과하고PTYBridge는 우회된다.DaemonPTYBridge가 이미'agent'/'critical'/'idle'event 를 emit 하고 있었지만DaemonSessionManager는'idle'만 forward,daemon/index.ts는'activity.idle'만 broadcast,DaemonClient는'session.died'만 specific emit. 즉 local mode fix 만으로는 사용자 환경에서 0 효과. 신규DaemonNotificationRouter(src/main/notification/DaemonNotificationRouter.ts) 가 daemon broadcast event 5 종 (session:agent/active/critical/idle/died/destroyed) 을 listen 해서 PTYBridge 와 동일한 로직 실행.DaemonEventtype 에'activity.active'+'session.destroyed'추가,daemon/index.ts가 신규 type 모두 broadcast,DaemonClient가 specific emit. daemon 측AgentDetector의 dedup state 도 onActive burst 시점에 in-process 로 reset (main 에서 daemon process 의 detector 에 접근 불가하기 때문). - PTY echo / SIGWINCH redraw 가 false-positive idle 알림을 유발하던 결함 (사용자 발견) — 7-round review pipeline (CEO + Eng + Codex × 4 + Claude subagent) 가 catch 못 한 케이스. ActivityMonitor 는 byte count 휴리스틱이라 "agent task ending" 과 "외부 상태 변화로 인한 PTY redraw" 를 구분 못 함. (a) 사용자 keystroke 가 PTY echo 로 돌아와 active threshold 를 넘기고 잠시 멈추면 "Task may have finished" 가 사용자 입력 중에 발화. (b) workspace 전환 시
FitAddon.fit()→IPC.PTY_RESIZE→ SIGWINCH → TUI agent 의 full-screen redraw 가 active 진입 → 5s 후 idle timer 발화. 신규idleSuppression모듈 (src/main/notification/idleSuppression.ts) 이lastResizeAt/lastUserWriteAt을 per-ptyId 로 추적, 30 s window 내면 activity-fallback 알림 suppress. AgentDetector 의 precise event 는 gate 안 함 (정확한 신호이므로).pty.handler.ts의 4 path (write × 2 + resize × 2) 가markResize/markUserWrite호출. 사용자가 보고한 "타자 치는 중 알람" + "워크스페이스만 눌렀다가 다른 곳 가면 +1" 두 시나리오 모두 해결. - 사용자가 보고 있는 surface 에도 알림이 누적되던 결함 —
useNotificationListener가 active workspace 의 active surface 일치 여부 체크 없이 무조건addNotification+pushToast호출. 사용자가 직접 보고 있는 곳은 알림 의미 0 인데 unread 배지가 계속 올라갔다. 알림 발생 직전isActivePtySurface체크 → 일치하면 in-app surface (addNotification+pushToast) skip. OS toast 는ToastManager가 자체 focus gate 가지고 있어 변경 없음. - workspace 전환만으로는 unread 가 read 처리 되지 않던 결함 — 사용자 보고: "워크스페이스만 눌러서 들렀다가 다른 곳 가면 unread 가 +1." Pane click 만이 markRead 트리거였고 sidebar 의 workspace 타일 click 은 read 영향 0.
workspaceSlice.setActiveWorkspaceaction 이 해당 workspace 의 모든 unread 를 read 로 자동 처리하도록 변경.Array.isArray(state.notifications)가드로 workspaceSlice 단독 테스트 호환. - pushToast 가 사용자 toast 설정 무시하던 결함 —
useNotificationListener가 settings 의toastEnabled무시하고 매번 in-app overlay 띄움. 사용자가 "Toast notifications" 끄면 OS toast 만 suppress, in-app 은 그대로 표시되던 결함.state.toastEnabledgate 추가 (sound playback 패턴과 동일). - AgentDetector 의 Claude
esc to interrupt가 false-positive 'waiting' — 실제로는 "지금 response 가 진행 중, ESC 로 중단 가능" 힌트이지 idle 신호가 아니다. 패턴 제거. mid-turn 에 잘못된 알림 fire 차단. - AgentDetector enum 명명 불일치 —
AgentEvent.status: 'completed'vsWorkspaceMetadata.agentStatus: 'complete'.AgentStatusenum 으로 통일 (Aider 패턴'completed'→'complete'텍스트 변경 포함). 외부 consumer 없어 안전. - AgentDetector dedup 이 turn N+1 의 같은 prompt 를 영영 차단하던 결함 —
lastEmittedKey가 single global string 이라 한 번 emit 한 prompt 는 다시 emit 안 됨 → 사용자가 추가 입력해도 사이드바 dot 갱신 0.lastEmittedForMap 으로 per-(agent:status) 분리 +resetEmissionState()method 추가, ActivityMonitor 의 새 active burst 시점에 reset (turn boundary). local mode 는 PTYBridge 가 직접 호출, daemon mode 는DaemonPTYBridge.onActive콜백이 in-process 에서 호출. - AgentDetector 의 ANSI strip 이 private-mode prefix 를 못 잡던 결함 —
\x1b[?25h같은 cursor visibility 시퀀스 (?포함) 가[0-9;]*[a-zA-Z]regex 와 안 맞아clean에 잔존, gate 매칭 실패 가능.[0-9;?<=>]*[a-zA-Z@]로 확장. - AgentDetector 가 lone
\rredraw 를 한 라인으로 처리하던 결함 — Claude/Codex TUI footer 는 CR 단독으로 redraw.split(/\r?\n/)가 통째로 묶어 line-anchored regex 가 매칭 실패.split(/\r?\n|\r(?!\n)/)로 확장. - AgentDetector.onEvent/onCritical 이 unsubscribe 안 돌려주던 결함 —
void반환이라 PTY recycle 시마다 listener 누적. v2.7.2 의 PlaywrightEngine CDP 세션 누수와 동일 카테고리. unsubscribe 함수 반환으로 변경, PTYBridgecleanupInstance+ DaemonPTYBridgecleanup에서 호출. ActivityMonitor 의onActiveToIdle/onActive도 같은 패턴. - AgentDetector callback 내부 throw 가 후속 라인 감지를 죽이던 결함 — PTYBridge middleware 패턴과 일치시켜 onEvent/onActive 콜백 본문에 try/catch 가드 추가. 한 callback 의 실패가 PTY stream 전체를 죽이지 않게 격리.
AGENT_EVENT_SUPPRESSION_MS로 ActivityMonitor 의 fallback 알림 dedup — AgentDetector 가 precise event emit 직후 ActivityMonitor 가 또 idle 발화하면 같은 turn 에 알림 2 회. PTYBridge / DaemonNotificationRouter 가lastAgentEventAt추적, 10 s 이내면 fallback skip.notifyRPC 가 workspaceId 없이는 깨지던 결함 — preload signature 가ptyId: string강제,addNotification이surfaceId강제. RPC path 는 ptyId 가 없어 silent drop 되거나 type error. workspaceId optional 로 변경 (CLIwmux notifybackward compat 유지),Notification.surfaceIdoptional, useNotificationListener 가nullptyId 면 workspaceId 로 active surface resolve (or active workspace fallback).
Added
sendNotificationutility (src/main/notification/sendNotification.ts) — 모든IPC.NOTIFICATION송신의 단일 entry point. window null/destroyed 가드 +(ptyId | null, payload)시그니처 통일. PTYBridge 4 호출 지점 + notify.rpc + DaemonNotificationRouter 모두 import.broadcastMetadataUpdateutility (src/main/ipc/handlers/metadata.handler.ts) — 모든IPC.METADATA_UPDATE송신의 단일 entry point. MetadataUpdatePayload 단일 shape.idleSuppression모듈 (src/main/notification/idleSuppression.ts) — per-PTY resize/user-write 시점 추적. 30 s suppression window 로 ActivityMonitor 의 byte-count heuristic false-positive 차단.DaemonNotificationRouter(src/main/notification/DaemonNotificationRouter.ts) — daemon mode 에서 PTYBridge 의 알림 라우팅 역할 대체.DaemonClientevent 5 종 listen →sendNotification+broadcastMetadataUpdate+ toast.- AgentDetector 의 in-process API 확장 —
getActiveAgents()/getLastAgent()/resetEmissionState()public method 추가. PTYBridge 가 lastAgent name 을 onActive metadata 에 채워 넣을 수 있게. - 37 신규 unit test —
AgentDetector.test.ts(18, enum/unsubscribe/dedup/\rsplit/ANSI strip/getters/critical),ActivityMonitor.test.ts(+4, onActive cycle dedup),sendNotification.test.ts(4, null/destroyed/ptyId 분기),PTYBridge.notify.test.ts(5, METADATA_UPDATE + NOTIFICATION + try/catch + cleanup unsub),notify.rpc.test.ts(6, workspaceId optional + MCP path + type fallback + toast). IRON RULE 7 regression 중 6 cover, R7 (pushToast in renderer) 는 jsdom 필요해 manual.
Migration Notes
- 자동. 사용자 액션 불필요.
Notification.surfaceId를 optional 로 변경 —Pane.tsx의surfaceIds.has(n.surfaceId)에 undefined guard 추가됨. 다른 consumer 없음.AgentEvent.statusenum 변경 ('completed'→'complete') — wmux 내부에서 PTYBridgeonCritical만 consume 했고 onEvent 는 dead code 였으므로 외부 영향 없음.IPC.METADATA_UPDATEpayload shape 통일 — preloadmetadata.onUpdate시그니처가(payload)단일 인자로 변경. renderer 의useNotificationListener가 호환 처리. 외부 MCP / CLI consumer 영향 없음.notifyRPC 의workspaceId는 optional 신규 param. CLIwmux notify --title X --body Y는 그대로 동작. MCP 클라이언트가mcp.claimWorkspace의 workspaceId 를 함께 보내면 precise routing (active surface auto-select).
Deferred (follow-up issues)
DaemonNotificationRouterregression test suite — manual verification 으로 cover, daemon IPty pipeline mock 은 별도 작업.- session-restore sanitize regression test — session fixture builder 필요.
onExitelapsed=0 cosmetic (cleanupInstance 가 ptyCreatedAt 먼저 wipe 하는 path) — purely message-text, behavioural 영향 0.DaemonClient.removeAllListenerson disconnect — pre-existing, 본 PR 범위 외.TODOS.md에 cherry-picked deferral 추가: E3 (transient dot flash animation, P3), E4 (per-workspace notification mute, P2), E5 (tray icon unread badge — cross-platform, P2), Phase 2 Eureka (Claude Code stop-hook → OSC 9 BEL emit, P3).
Review Trail
| Pass | Reviewer | Findings | Status |
|---|---|---|---|
| Plan 1 | /plan-ceo-review |
5 proposals | SELECTIVE_EXPANSION, 2 accepted |
| Plan 1 | Codex round 1 | 10 | all addressed |
| Plan 1 | /plan-eng-review |
11, 1 critical | all addressed |
| Plan 1 | Codex round 2 | 8 | all addressed (daemon mode wiring 6 파일 추가) |
| Code 2 | Codex round 3 | 2 (P1+P2) | all addressed in 5aee27f |
| Code 3 | Codex round 4 | 3 (P2+P2+P3) | all addressed in cddd3bd |
| Code 3 | Claude subagent | 7 (P2+P2+P3×5) | 2 addressed, 5 deferred |
| Code 4 | 사용자 manual test | 2 (resize/typing FP) | addressed in 42f5bd3 |
7-round review pipeline 의 한계: AI review 가 PTY echo / SIGWINCH redraw 같은 runtime 동작 은 코드만 보고 모델링하기 어렵다. 사용자 manual test 가 마지막 안전망이 됐다는 점이 기록 가치 있음.