{"version":3,"sources":["../src/cli.tsx","../../../packages/core/src/lib/logger.ts","../../../packages/core/src/managers/ssh-manager.ts","../../../packages/core/src/managers/pty-manager.ts","../../../packages/core/src/managers/scrollback-buffer.ts","../../../packages/core/src/managers/scrollback-persister.ts","../../../packages/shared/src/constants.ts","../../../packages/core/src/managers/grid-emulator.ts","../../../node_modules/.pnpm/@xterm+addon-serialize@0.14.0/node_modules/src/common/Color.ts","../../../node_modules/.pnpm/@xterm+addon-serialize@0.14.0/node_modules/src/browser/Types.ts","../../../node_modules/.pnpm/@xterm+addon-serialize@0.14.0/node_modules/@xterm/addon-serialize/src/SerializeAddon.ts","../../../packages/core/src/protocol/binary-protocol.ts","../../../packages/core/src/managers/pty-lifecycle.ts","../../../packages/core/src/managers/authority-resolver.ts","../../../packages/core/src/managers/git-command.ts","../../../packages/core/src/managers/agent-detector.ts","../../../packages/core/src/managers/agent-sessions.ts","../../../packages/shared/src/utils.ts","../../../packages/core/src/multiplexer/none.ts","../../../packages/core/src/multiplexer/index.ts","../../../packages/core/src/managers/ws-manager.ts","../../../packages/shared/src/relay-config.ts","../../../packages/core/src/managers/pair-url.ts","../../../packages/core/src/managers/agent-runner.ts","../../../packages/core/src/managers/cockpit-parser-utils.ts","../../../packages/core/src/managers/cockpit-parser.ts","../../../packages/core/src/managers/file-upload-receiver.ts","../../../packages/core/src/managers/fcm-token-store.ts","../../../packages/core/src/create-agent-core.ts","../../../packages/core/src/managers/hook-receiver.ts","../../../packages/core/src/managers/claude-hooks.ts","../src/config/json-store.ts","../src/config/json-file.ts","../src/config/load-env.ts","../src/entry-mode.ts","../package.json","../src/identity.ts","../src/service/cli-command.ts","../src/pairing.ts","../src/service/platform.ts","../src/service/launchd.ts","../src/service/exec.ts","../src/service/launchd-plist.ts","../src/service/service-process.ts","../src/service/none.ts","../src/service/schtasks.ts","../src/service/schtasks-args.ts","../src/service/systemd.ts","../src/service/unit-template.ts","../src/service/systemd-cli.ts","../src/service/resolver.ts","../src/state.ts","../src/update.ts","../src/ui/App.tsx","../src/ui/screens/GetAppsScreen.tsx","../src/ui/constants.ts","../src/download-feed.ts","../src/ui/components/Divider.tsx","../src/ui/components/PairQrCell.tsx","../src/ui/qr-fit.ts","../src/ui/qr.ts","../src/ui/useTerminalSize.ts","../src/ui/screens/InfoScreen.tsx","../src/ui/pretty-url.ts","../src/ui/screens/MachinesScreen.tsx","../src/ui/components/ConfirmDialog.tsx","../src/ui/screens/MachineFormScreen.tsx","../src/config/validate.ts","../src/ui/components/Field.tsx","../src/ui/components/SelectField.tsx","../src/ui/useFieldFocus.ts","../src/ui/screens/machine-payload.ts","../src/ui/screens/MainScreen.tsx","../src/ui/components/LogLines.tsx","../src/ui/scroll-window.ts","../src/ui/screens/OptionsScreen.tsx","../src/ui/ManagerApp.tsx","../src/ui/log-tail.ts"],"sourcesContent":["import os from 'node:os'\r\nimport path from 'node:path'\r\n\r\nimport { type AgentCore, createAgentCore, createLogger, setConsoleLogging } from '@arva/core'\r\nimport { render } from 'ink'\r\nimport React from 'react'\r\n\r\nimport { HeadlessConfigStore } from './config/json-store'\r\nimport { loadDotEnv } from './config/load-env'\r\nimport { resolveEntryMode } from './entry-mode'\r\nimport { identity } from './identity'\r\nimport { getServiceBackend, isServiceProcess, runServiceCommand } from './service'\r\nimport { HeadlessStore } from './state'\r\nimport { checkLatestVersion } from './update'\r\nimport { App } from './ui/App'\r\nimport { ManagerApp } from './ui/ManagerApp'\r\n\r\n/**\r\n * `reins` 엔트리. {@link resolveEntryMode} 로 진입 모드를 결정해 분기한다(TTY 1차, 서비스 자신 2차):\r\n *\r\n * 0. `reins service <install|uninstall|status>` — systemd user 서비스 등록만 수행(코어/TUI 없음).\r\n * 1. `daemon` — non-TTY 이고 서비스 자신이거나 서비스 미가동 → {@link runDaemon}(TUI 없이 코어만).\r\n *    서비스는 controlling TTY 가 없어 풀 TUI 의 `useInput`(Ink raw mode)이 throw 하므로 이 경로다.\r\n * 2. `serviceBusy` — non-TTY 이고 서비스 가동 중인데 **마커 없는** 다른 호출(파이프/cron). 두 번째\r\n *    코어를 띄우지 않고(이중 relay·포트 EADDRINUSE 방지) 안내 후 종료한다.\r\n * 3. `manager` — TTY + 서비스 가동 중. 관리/상태 화면({@link ManagerApp})만(코어 미기동).\r\n * 4. `tui` — TTY + 서비스 미가동. 풀 TUI({@link App}, 코어 기동).\r\n *\r\n * 자기참조 주의: 서비스 자신도 non-TTY 라 `isServiceActive()` 가 자신을 active 로 오판한다.\r\n * {@link isServiceProcess}(마커/INVOCATION_ID)로 \"서비스 자신\"을 먼저 걸러 daemon 으로 보낸 뒤에야\r\n * active 가드를 적용한다 — 그래야 가드를 켜도 서비스 자신이 거절당해 플래핑하지 않는다.\r\n *\r\n * 시크릿: relay Bearer 는 `MAIN_VITE_RELAY_AGENT_SECRET`(또는 `VIBE_RELAY_AGENT_SECRET`) env 로\r\n * 오버라이드하고, 없으면 빌드 시 박은 기본값으로 fallback 한다(npm 설치 직후 무설정 접속 — json-store 참조).\r\n */\r\n// relay 시크릿/URL 을 `.env` → process.env 로(코어·config 가 읽기 전에). 셸 env 가 있으면 우선.\r\nconst loadedEnvFiles = loadDotEnv()\r\n\r\nconst config = new HeadlessConfigStore()\r\nconst args = process.argv.slice(2)\r\n\r\nif (args[0] === 'service') {\r\n  // 1) 서비스 등록 — 코어/TUI 없이 종료코드 반환.\r\n  process.exit(runServiceCommand(args[1], config))\r\n}\r\n\r\n/**\r\n * 코어를 와이어링해 기동하고, 부팅 진단(.env 로드·relay 타깃)을 store 로그에 남긴다.\r\n *\r\n * 데몬·풀 TUI 가 공유한다. 부수효과: createAgentCore 가 로컬 머신 등록 + 로컬 WS 서버 + relay\r\n * 자동 접속까지 기동한다(이 소켓들이 이후 프로세스를 살려둔다). 첫 페인트용 머신 시드 + QR 1회 요청.\r\n */\r\nfunction buildCore(store: HeadlessStore): AgentCore {\r\n  const logDir = path.join(os.homedir(), '.ai_remote_vibe_agent', 'logs')\r\n  const relayTarget = config.getRelayUrls().join(', ')\r\n  for (const f of loadedEnvFiles) store.log(`[boot] .env loaded: ${f}`)\r\n  // 어떤 relay 로 붙는지 부팅 즉시 표시 — env/.env 반영 여부 한눈에(연결 실패 진단 1순위).\r\n  store.log(`[boot] relay target: ${relayTarget}`)\r\n  store.log('[boot] starting agent core…')\r\n\r\n  const core = createAgentCore({\r\n    config,\r\n    identity,\r\n    logDir,\r\n    onHostMessage: msg => store.apply(msg),\r\n  })\r\n\r\n  // Sessions 카드 시드 — 코어가 등록한 머신(로컬 1개)을 첫 페인트에 표시. 이후 폰이 머신을 추가/삭제하면\r\n  // 코어의 machine_list broadcast 가 store 를 라이브 갱신한다.\r\n  store.apply({ type: 'machine_list', machines: core.ssh.getMachines() })\r\n  // 부팅 직후 1회 QR 요청(연결 전이면 LAN fallback). 연결되면 풀 TUI 가 relay QR 로 재요청.\r\n  core.ws.requestQr()\r\n\r\n  return core\r\n}\r\n\r\n// isActive() 는 systemctl/launchctl/schtasks spawn 이라 thunk 로 지연 — 마커 보유 데몬(서비스 주\r\n// 경로)에서는 호출되지 않아 불필요한 자기참조 조회를 피한다(resolveEntryMode 가 마커를 먼저 본다).\r\nconst mode = resolveEntryMode({\r\n  isTTY: !!process.stdout.isTTY,\r\n  isServiceProcess: isServiceProcess(process.env),\r\n  isServiceActive: () => getServiceBackend().isActive(),\r\n})\r\n\r\nif (mode === 'daemon') {\r\n  // non-TTY 데몬(서비스·파이프) — TUI 없이 코어만. console transport 는 끄지 않아\r\n  // journald(journalctl --user -u reins)로 로그가 흐른다(stdout 을 점유하는 TUI 가 없으므로 안전).\r\n  runDaemon()\r\n} else if (mode === 'serviceBusy') {\r\n  // 서비스 가동 중인데 끼어든 다른 non-TTY 호출(파이프/cron) — 두 번째 코어를 띄우지 않는다.\r\n  // 이중 relay 접속·로컬 포트 EADDRINUSE 방지. \"이미 가동 중\"=idempotent 성공이라 exit(0).\r\n  console.error('reins: service is already running — not starting a second instance (avoids double relay / port conflict).')\r\n  console.error('  to reconnect, run `reins` in a TTY; for logs use `journalctl --user -u reins -f`.')\r\n  process.exit(0)\r\n} else {\r\n  // TUI 경로(manager | tui) — Ink 가 stdout 을 화면 렌더에 쓰므로 core 로거 console transport 를 끈다.\r\n  setConsoleLogging(false)\r\n\r\n  // 풀스크린 TUI 진입 — alternate screen buffer. Ink 는 출력이 터미널 높이를 넘으면 이전 프레임(예:\r\n  // 큰 QR)의 스크롤된 줄을 erase 하지 못해 잔상이 남는다(`c` 로 QR 을 꺼도 지워지지 않음). alt screen 은\r\n  // 스크롤이 없어 erase 가 viewport 안에서 정확해진다. 모든 종료 경로에서 leave 를 보장하기 위해\r\n  // process.on('exit') 에 복원을 건다(Ink unmount 후 원래 화면 복귀).\r\n  process.stdout.write('\\x1b[?1049h')\r\n  process.on('exit', () => process.stdout.write('\\x1b[?1049l'))\r\n\r\n  if (mode === 'manager') {\r\n    // 서비스 가동 중 + 인터랙티브 재접속 — 관리/상태 화면(코어 미기동).\r\n    const { waitUntilExit } = render(<ManagerApp config={config} />)\r\n    // SIGTERM 시 'exit' 가 돌아 alt screen 이 복원되도록 명시 종료(코어가 없어 즉시 exit 안전).\r\n    process.on('SIGTERM', () => process.exit(0))\r\n    void waitUntilExit().finally(() => process.exit(0))\r\n  } else {\r\n    // 기본 — 풀 TUI(코어 기동).\r\n    const store = new HeadlessStore()\r\n    const core = buildCore(store)\r\n\r\n    // 버전 체크 — TUI 경로 전용. registry HTTP fetch(비동기) 라 이벤트 루프를 블로킹하지 않는다.\r\n    // buildCore 내부에 두면 runDaemon 경로도 실행되므로 여기서만 호출한다.\r\n    void checkLatestVersion(identity.version)\r\n      .then(result => {\r\n        if (result?.hasUpdate) store.setLatestVersion(result.latest)\r\n      })\r\n      // fire-and-forget — 버전 체크는 부팅을 막지 않는다. 어떤 실패도 삼켜 unhandled rejection 방지.\r\n      .catch(() => {})\r\n\r\n    const { waitUntilExit } = render(<App store={store} core={core} config={config} />)\r\n\r\n    // 단일 teardown 경로. Ink 가 Ctrl-C / q(exit) / unmount 시 waitUntilExit 를 resolve 하므로 그\r\n    // 지점에서만 cleanup + exit(cleanup 은 createAgentCore 가 재진입 가드). SIGINT 는 Ink 처리.\r\n    const finish = (code: number): void => {\r\n      core.cleanup()\r\n      process.exit(code)\r\n    }\r\n    process.on('SIGTERM', () => finish(0))\r\n    void waitUntilExit()\r\n      .then(() => finish(0))\r\n      .catch(() => finish(1))\r\n  }\r\n}\r\n\r\n/**\r\n * non-TTY 데몬 모드 — Ink TUI 없이 코어만 띄운다(systemd 서비스의 주 실행 경로).\r\n *\r\n * 프로세스 keep-alive 는 코어가 연 소켓에 의존한다: relay 클라이언트 소켓 + 로컬 WS listen 서버 +\r\n * relay 재접속 타이머가 이벤트 루프를 비우지 않아 데몬이 상주한다(별도 인공 keep-alive 불필요).\r\n * systemd stop/restart 의 SIGTERM(및 파이프라인 Ctrl-C 의 SIGINT)에서 코어를 정리하고 종료해\r\n * 포트·relay 를 깨끗이 해제한다(다음 인스턴스의 EADDRINUSE 방지).\r\n */\r\nfunction runDaemon(): void {\r\n  const log = createLogger('ws')\r\n  const store = new HeadlessStore()\r\n  const core = buildCore(store)\r\n  log.info(`[daemon] headless service started — relay=${config.getRelayUrls().join(', ')}`)\r\n\r\n  const shutdown = (): void => {\r\n    core.cleanup()\r\n    process.exit(0)\r\n  }\r\n  process.on('SIGTERM', shutdown)\r\n  process.on('SIGINT', shutdown)\r\n}\r\n","// electron-log/node: Electron 런타임에 의존하지 않는 노드 변형. 본 factory 가 packages/core\r\n// (Phase A4)로 이동해 헤드리스(plain Node)에서도 동작하려면 default `electron-log`(내부에서\r\n// `require('electron')`)가 아니라 `/node` 엔트리여야 한다 — core 의 no-electron CI 가드 통과 조건.\r\n// `.js` 명시 필수: electron-log 5.x 는 exports 맵이 없어 ESM(tsup 헤드리스 번들)에서 확장자 자동\r\n// 추가가 안 된다(`electron-log/node` → ERR_MODULE_NOT_FOUND). CJS(데스크탑)·ESM 양쪽에서 동일.\r\nimport log from 'electron-log/node.js'\r\nimport path from 'node:path'\r\nimport os from 'node:os'\r\n\r\n/**\r\n * scoped 로그 파일이 기록될 디렉터리. 호스트 앱이 시작 시 {@link configureLogDir} 로 주입한다.\r\n *\r\n * 기본값은 비-Electron(헤드리스) 안전 경로(`~/.ai_remote_vibe_agent/logs`). Electron 데스크탑은\r\n * `index.ts` 가 `app.getPath('logs')`(= 기존 `userData/logs`)로 덮어써 로그 위치를 그대로 유지한다\r\n * (무회귀). electron-log/node 는 Electron 의 app 경로를 모르므로 디렉터리를 명시 주입해야 한다.\r\n */\r\nlet scopedLogDir = path.join(os.homedir(), '.ai_remote_vibe_agent', 'logs')\r\n\r\n/**\r\n * scoped 로그 디렉터리를 설정한다. 호스트 앱이 부팅 초반(첫 로그 기록 전)에 1회 호출.\r\n *\r\n * createLogger 는 모듈 top-level 에서 import 시점에 실행되지만, 파일 경로는 `resolvePathFn` 으로\r\n * **lazy** 해석되므로(첫 로그 write 시점) 본 주입이 그보다 늦어도 안전하다.\r\n */\r\nexport function configureLogDir(dir: string): void {\r\n  scopedLogDir = dir\r\n}\r\n\r\n/**\r\n * 지금까지 {@link createLogger} 로 만든 모든 scoped logger. {@link setConsoleLogging} 가\r\n * console transport 를 일괄 토글하려면 인스턴스 핸들을 모아둬야 한다(로거는 모듈 top-level 에서\r\n * import 시점에 생성되므로, 호스트가 끄기 전에 이미 다 만들어져 있다).\r\n */\r\nconst loggers: Array<ReturnType<typeof log.create>> = []\r\n\r\n/** console transport 활성 여부. 신규 로거가 생성될 때 이 값을 따른다. */\r\nlet consoleEnabled = true\r\n\r\n/**\r\n * 모든(이미 생성된·앞으로 생성될) scoped logger 의 console transport 를 일괄 on/off.\r\n *\r\n * **왜**: 헤드리스 TUI(Ink)는 **stdout 을 화면 렌더에 쓴다**. core 로그가 console transport 로\r\n * stdout 에 새면 TUI 위로 로그가 끼어들어 화면이 계속 밀려 올라간다. 헤드리스 cli 는 부팅 시\r\n * 1회 `setConsoleLogging(false)` 로 끈다 — file transport(`logs/*.log`)와 TUI 자체 verbose 뷰는\r\n * 그대로 유지되므로 디버깅 정보는 손실 없다. 데스크탑(Electron)은 호출하지 않아 기존 동작을 유지한다.\r\n *\r\n * 재활성화 레벨은 electron-log 기본값 `'silly'`(console.js 기본).\r\n */\r\nexport function setConsoleLogging(enabled: boolean): void {\r\n  consoleEnabled = enabled\r\n  for (const scoped of loggers) {\r\n    scoped.transports.console.level = enabled ? 'silly' : false\r\n  }\r\n}\r\n\r\n/**\r\n * Per-component scoped logger factory.\r\n *\r\n * 각 component 마다 electron-log 의 별도 인스턴스 + 별도 파일에 로그를 분리.\r\n * 디버깅 시 한 component 만 골라서 보기 위함 (예: `tail -f logs/git.log`).\r\n *\r\n * **파일 위치**: `{configureLogDir 로 주입된 디렉터리}/{component}.log`\r\n * - 데스크탑: `app.getPath('logs')` = 기존 `userData/logs/{component}.log` (변동 없음)\r\n * - 헤드리스: `~/.ai_remote_vibe_agent/logs/{component}.log`\r\n *\r\n * **포맷**: `[YYYY-MM-DD HH:MM:SS.ms] [level] [component] message`\r\n *\r\n * **사용 예**:\r\n * ```ts\r\n * import { createLogger } from '@arva/core'   // 코어 내부에선 상대경로 '../lib/logger'\r\n * const log = createLogger('ws')\r\n * log.info('session_attach machine=...')   // → logs/ws.log\r\n * ```\r\n *\r\n * **주의 (WHY)**:\r\n * - 데스크탑 default logger (`index.ts` 의 `import log from 'electron-log'`)는 `logs/main.log`\r\n *   에 main 프로세스 로그를 모은다. 본 factory 는 그것과 별개 인스턴스(`.create`)라 default 를\r\n *   건드리지 않고 component 별 파일로만 분기한다.\r\n * - `logId` 가 같으면 같은 인스턴스 재사용 가능하지만 transports 설정 한 번이면\r\n *   충분하므로 caller 가 모듈 top-level 에서 한 번만 호출하면 됨.\r\n */\r\nexport function createLogger(component: string) {\r\n  const scoped = log.create({ logId: component })\r\n\r\n  // 파일 transport — {scopedLogDir}/{component}.log. resolvePathFn 은 write 시점 lazy 평가라\r\n  // import 순서와 무관하게 configureLogDir 주입 결과가 반영된다.\r\n  scoped.transports.file.fileName = `${component}.log`\r\n  scoped.transports.file.resolvePathFn = () =>\r\n    path.join(scopedLogDir, `${component}.log`)\r\n  scoped.transports.file.format =\r\n    '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] [{logId}] {text}'\r\n  scoped.transports.file.maxSize = 5 * 1024 * 1024 // 5MB rotation\r\n\r\n  // Console transport — 개발 환경에서 가독성 우선 (날짜 제외, 시간만)\r\n  scoped.transports.console.format =\r\n    '[{h}:{i}:{s}.{ms}] [{level}] [{logId}] {text}'\r\n\r\n  // 레지스트리에 등록하고 현재 console 정책을 반영. 헤드리스가 setConsoleLogging(false) 를\r\n  // 먼저 호출한 뒤 새 로거가 만들어져도 console 이 켜지지 않도록(stdout 오염 방지).\r\n  loggers.push(scoped)\r\n  if (!consoleEnabled) scoped.transports.console.level = false\r\n\r\n  return scoped\r\n}\r\n","import { Client } from 'ssh2'\r\nimport type { ClientChannel, ConnectConfig } from 'ssh2'\r\nimport fs from 'fs'\r\nimport os from 'os'\r\nimport path from 'path'\r\nimport crypto from 'crypto'\r\nimport { StringDecoder } from 'string_decoder'\r\n\r\nimport simpleGit from 'simple-git'\r\nimport type { BranchSummary, StatusResult } from 'simple-git'\r\nimport pLimit from 'p-limit'\r\n\r\nimport { createNodePty } from './pty-manager'\r\nimport { PtyScrollbackBuffer } from './scrollback-buffer'\r\nimport { ScrollbackPersister } from './scrollback-persister'\r\nimport { PtyLifecycleManager } from './pty-lifecycle'\r\nimport { AuthorityResolver } from './authority-resolver'\r\nimport {\r\n  buildGitCommand,\r\n  classifyDeleteError,\r\n  validateGitRef,\r\n} from './git-command'\r\nimport { detectRunningAgent } from './agent-detector'\r\nimport {\r\n  listAgentSessions,\r\n  mergeRunningSessions,\r\n  readSessionTranscript,\r\n  scheduleSessionRescan,\r\n} from './agent-sessions'\r\nimport { SCROLLBACK_BUFFER_BYTES_DEFAULT } from '@arva/shared/constants'\r\nimport { normalizeCwd } from '@arva/shared/utils'\r\nimport type {\r\n  Machine,\r\n  BroadcastFn,\r\n  ConnectionState,\r\n  MultiplexerSetting,\r\n  ServerMessage,\r\n  PtyProcess,\r\n  GitAction,\r\n  GitStatusData,\r\n  GitBranchData,\r\n  GitFileData,\r\n  GitCommitData,\r\n  RunningAgentKind,\r\n} from '@arva/shared/types'\r\nimport type { SSHManagerConnectOptions } from './interfaces/ssh-manager.interface'\r\nimport { createSessionManager, type SessionManager } from '../multiplexer'\r\nimport { createLogger } from '../lib/logger'\r\n\r\n// Per-component scoped loggers — git 작업 노이즈 (5s 폴링) 와 ssh/pty 이벤트\r\n// 를 별도 파일로 분리. `tail -f logs/git.log` / `tail -f logs/ssh.log` 가능.\r\nconst gitLog = createLogger('git')\r\nconst sshLog = createLogger('ssh')\r\n\r\nconst homeDir = os.homedir()\r\n\r\n/**\r\n * 경로가 실제로 존재하는 디렉토리인지 검사한다.\r\n *\r\n * PTY spawn 의 cwd 검증용. 없는 cwd 를 ConPTY/CreateProcess 에 넘기면 Windows 에서\r\n * `CreateProcess` 가 error 267 (ERROR_DIRECTORY, \"디렉토리 이름이 잘못됨\") 로 spawn\r\n * 자체를 실패시킨다 — localCwd 가 다른 OS 의 경로이거나 삭제된 폴더인 경우 발생.\r\n * 사전에 걸러 homeDir 로 fallback 하기 위함. statSync throw(ENOENT 등) 는 \"없음\" 으로 간주.\r\n */\r\nexport function isExistingDir(p: string): boolean {\r\n  try {\r\n    return fs.statSync(p).isDirectory()\r\n  } catch {\r\n    return false\r\n  }\r\n}\r\n\r\n// ─── Persistence ──────────────────────────────────────────────────────────────\r\n\r\nconst DATA_DIR = path.join(homeDir, '.claude_code_agent')\r\nconst MACHINES_FILE = path.join(DATA_DIR, 'machines.json')\r\nconst KEYRING_FILE = path.join(DATA_DIR, 'keyring.key')\r\nconst ALGORITHM = 'aes-256-gcm'\r\n\r\n/**\r\n * Scrollback 디스크 persist state dir 위치 (v1.5.0).\r\n * Windows: %LOCALAPPDATA%/ai-rva/scrollback/\r\n * POSIX:   $XDG_STATE_HOME/ai-rva/scrollback/, fallback ~/.local/state/ai-rva/scrollback/\r\n */\r\nfunction resolveScrollbackStateDir(): string {\r\n  const baseDir =\r\n    process.platform === 'win32'\r\n      ? (process.env.LOCALAPPDATA ?? path.join(homeDir, 'AppData', 'Local'))\r\n      : (process.env.XDG_STATE_HOME ?? path.join(homeDir, '.local', 'state'))\r\n  return path.join(baseDir, 'ai-rva', 'scrollback')\r\n}\r\n\r\ntype PersistedMachine = Omit<\r\n  Machine,\r\n  'status' | 'password' | 'passwordLost'\r\n> & {\r\n  encryptedPassword?: string\r\n}\r\n\r\nlet _keyCache: Buffer | null = null\r\n\r\nfunction loadOrCreateKey(): Buffer {\r\n  if (_keyCache) return _keyCache\r\n  try {\r\n    _keyCache = fs.readFileSync(KEYRING_FILE)\r\n    return _keyCache\r\n  } catch {\r\n    if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true })\r\n    const key = crypto.randomBytes(32)\r\n    fs.writeFileSync(KEYRING_FILE, key, { mode: 0o600 })\r\n    _keyCache = key\r\n    return key\r\n  }\r\n}\r\n\r\nfunction encryptPassword(password: string): string {\r\n  const key = loadOrCreateKey()\r\n  const iv = crypto.randomBytes(12)\r\n  const cipher = crypto.createCipheriv(ALGORITHM, key, iv)\r\n  const encrypted = Buffer.concat([\r\n    cipher.update(password, 'utf8'),\r\n    cipher.final(),\r\n  ])\r\n  const tag = cipher.getAuthTag()\r\n  return Buffer.concat([iv, tag, encrypted]).toString('base64')\r\n}\r\n\r\nfunction decryptPassword(\r\n  encoded: string,\r\n  machineId: string\r\n): string | undefined {\r\n  try {\r\n    const key = loadOrCreateKey()\r\n    const buf = Buffer.from(encoded, 'base64')\r\n    const iv = buf.subarray(0, 12)\r\n    const tag = buf.subarray(12, 28)\r\n    const ciphertext = buf.subarray(28)\r\n    const decipher = crypto.createDecipheriv(ALGORITHM, key, iv)\r\n    decipher.setAuthTag(tag)\r\n    return decipher.update(ciphertext).toString('utf8') + decipher.final('utf8')\r\n  } catch {\r\n    sshLog.warn(\r\n      `[ssh-manager] password decrypt failed for ${machineId} — keyring.key 손실 또는 파일 손상?`\r\n    )\r\n    return undefined\r\n  }\r\n}\r\n\r\nfunction loadPersistedMachines(): Machine[] | null {\r\n  try {\r\n    const raw = fs.readFileSync(MACHINES_FILE, 'utf-8')\r\n    const parsed: PersistedMachine[] = JSON.parse(raw)\r\n    if (!Array.isArray(parsed)) return null\r\n    return parsed.map(({ encryptedPassword, ...rest }) => {\r\n      const password = encryptedPassword\r\n        ? decryptPassword(encryptedPassword, rest.id)\r\n        : undefined\r\n      const isLocalHost = rest.host === '127.0.0.1' || rest.host === 'localhost'\r\n      const machineType = rest.machineType ?? (isLocalHost ? 'local' : 'ssh')\r\n      return {\r\n        ...rest,\r\n        machineType,\r\n        status: 'disconnected' as const,\r\n        ...(encryptedPassword && !password ? { passwordLost: true } : {}),\r\n        ...(password ? { password } : {}),\r\n      }\r\n    })\r\n  } catch {\r\n    return null\r\n  }\r\n}\r\n\r\nfunction savePersistedMachines(machines: Machine[]): void {\r\n  try {\r\n    if (!fs.existsSync(DATA_DIR)) {\r\n      fs.mkdirSync(DATA_DIR, { recursive: true })\r\n    }\r\n    const toSave: PersistedMachine[] = machines.map(\r\n      ({ status: _s, password, passwordLost: _pl, ...rest }) => ({\r\n        ...rest,\r\n        ...(password ? { encryptedPassword: encryptPassword(password) } : {}),\r\n      })\r\n    )\r\n    fs.writeFileSync(MACHINES_FILE, JSON.stringify(toSave, null, 2), 'utf-8')\r\n  } catch (err) {\r\n    sshLog.error('[ssh-manager] Failed to save machines:', err)\r\n  }\r\n}\r\n\r\nconst SSH_KEY_PATHS = [\r\n  path.join(homeDir, '.ssh', 'id_ed25519'),\r\n  path.join(homeDir, '.ssh', 'id_ecdsa'),\r\n  path.join(homeDir, '.ssh', 'id_rsa'),\r\n]\r\n\r\nconst SSH_KEY_EXCLUDE = new Set([\r\n  'known_hosts',\r\n  'known_hosts.old',\r\n  'config',\r\n  'authorized_keys',\r\n])\r\n\r\nfunction findPrivateKey(): Buffer | null {\r\n  for (const keyPath of SSH_KEY_PATHS) {\r\n    try {\r\n      return fs.readFileSync(keyPath)\r\n    } catch {\r\n      // try next\r\n    }\r\n  }\r\n  return null\r\n}\r\n\r\ntype ActiveSession = {\r\n  client: Client\r\n  stream: ClientChannel | null\r\n}\r\n\r\n// git status 폴링 주기 (status timer 의 5초 tick 단위). 12 tick = 60초.\r\n// 탐지(detectAndBroadcastRunningAgent)는 매 tick 이지만 git 은 로그/네트워크 절감 위해\r\n// 1분에 1회. 모바일 git 패널 반영도 최대 60초 지연되는 trade-off (사용자 요청 2026-06-01).\r\nconst GIT_STATUS_EVERY_N_TICKS = 12\r\n\r\n// [Cockpit 신선도] 빈 세션 목록 재스캔 지연(ms). 방금 시작한 claude 가 `.jsonl` 을 flush\r\n// 하는 타이밍 창(보통 첫 턴 직후 수백 ms~수 초)을 덮는다. 유한 3회 — surface 되면 조기 중단.\r\nconst AGENT_SESSIONS_RESCAN_DELAYS_MS = [800, 2500, 6000]\r\n\r\n// PTY 그리드 경계 — node-pty crash 방지 + 비현실적인 값 차단.\r\n// NaN/0/negative → MIN, oversize → MAX.\r\nexport const PTY_MIN_COLS = 20\r\nexport const PTY_MAX_COLS = 500\r\nexport const PTY_MIN_ROWS = 5\r\nexport const PTY_MAX_ROWS = 200\r\n\r\nexport function clampCols(v: number): number {\r\n  const n = Math.round(v)\r\n  if (!Number.isFinite(n) || n <= 0) return PTY_MIN_COLS\r\n  return Math.min(Math.max(n, PTY_MIN_COLS), PTY_MAX_COLS)\r\n}\r\n\r\nexport function clampRows(v: number): number {\r\n  const n = Math.round(v)\r\n  if (!Number.isFinite(n) || n <= 0) return PTY_MIN_ROWS\r\n  return Math.min(Math.max(n, PTY_MIN_ROWS), PTY_MAX_ROWS)\r\n}\r\n\r\n/**\r\n * spawn cue 의 cols/rows 가 정상 viewport 측정값일 가능성이 있는지 판정.\r\n *\r\n * client (renderer/mobile) 의 첫 terminal_resize 가 spawn cue 로 사용되는데,\r\n * renderer 측 layout pending 시점의 measurement 가 비정상값 (예: 20×5, 80×10) 으로\r\n * 도착하면 inner-shell PTY 가 그 size 로 spawn 되어 footer/status-line 위치\r\n * stuck 회귀 (psmux race, task 13) + spawn 직후 PTY exit 회귀 (task 13 v2,\r\n * 2026-05-09 dogfood 의 PTY spawn loop).\r\n *\r\n * 임계값:\r\n *  - cols >= 40: 정상 PC 화면의 최소 너비\r\n *  - rows >= 24: classic VT terminal default (80×24). PowerShell / cmd.exe 가\r\n *    안정적으로 prompt + history 표시할 수 있는 최소 grid. 10 으로 두면 mobile 의\r\n *    layout pending 첫 측정값 80×10 이 통과해 PTY 가 spawn 직후 exit 하는 회귀.\r\n *\r\n * clamp 의 PTY_MIN (20×5) 보다 큰 임계 — clamp 는 PTY 자체의 hard bound 이고,\r\n * 본 함수는 \"spawn cue 로 신뢰할 만한가\" 판단.\r\n */\r\nexport function isSpawnCueValid(cols: number, rows: number): boolean {\r\n  return cols >= 40 && rows >= 24\r\n}\r\n\r\n/**\r\n * spawn 시점의 cols/rows 결정. stored cue 가 [isSpawnCueValid] 를 통과하면 그대로,\r\n * 아니면 fallback. log 마킹용 source 도 함께 반환.\r\n */\r\nexport function resolveSpawnSize(\r\n  stored: { cols: number; rows: number } | undefined,\r\n  fallbackCols: number,\r\n  fallbackRows: number\r\n): {\r\n  cols: number\r\n  rows: number\r\n  source: 'client' | 'fallback' | 'fallback-invalid-cue'\r\n} {\r\n  if (!stored)\r\n    return { cols: fallbackCols, rows: fallbackRows, source: 'fallback' }\r\n  if (!isSpawnCueValid(stored.cols, stored.rows)) {\r\n    return {\r\n      cols: fallbackCols,\r\n      rows: fallbackRows,\r\n      source: 'fallback-invalid-cue',\r\n    }\r\n  }\r\n  return { cols: stored.cols, rows: stored.rows, source: 'client' }\r\n}\r\n\r\nexport function mapSimpleGitState(\r\n  index: string,\r\n  workingDir: string\r\n): GitFileData['state'] {\r\n  const char = index !== ' ' && index !== '?' ? index : workingDir\r\n  if (char === 'A') return 'added'\r\n  if (char === 'D') return 'deleted'\r\n  if (char === '?' || workingDir === '?') return 'untracked'\r\n  if (char === 'R') return 'renamed'\r\n  if (char === 'U') return 'conflicted'\r\n  return 'modified'\r\n}\r\n\r\n/**\r\n * Machine 의 multiplexer 설정 fallback resolver.\r\n *\r\n * `machine.multiplexer` 가 undefined (미설정/구버전 machine) 일 때 'none' 으로 떨어진다.\r\n * 멀티플렉서 제거(2026-06-23) 후 createSessionManager 는 setting 과 무관하게 항상 None 이라\r\n * 실질 분기 영향은 없으나, 저장된 값 해석 일관성을 위해 유지(회귀 가드 테스트 존재).\r\n */\r\nexport function resolveMultiplexerSetting(\r\n  machine: Pick<Machine, 'multiplexer'>\r\n): MultiplexerSetting {\r\n  return machine.multiplexer ?? 'none'\r\n}\r\n\r\nexport class SSHManager {\r\n  private machines: Machine[] = []\r\n  private sessions = new Map<string, ActiveSession>()\r\n  private localSessions = new Map<string, PtyProcess>()\r\n  private reconnectAttempts = new Map<string, number>()\r\n  private ptySizes = new Map<string, { cols: number; rows: number }>()\r\n  // 첫 spawn 직전 클라이언트의 cols/rows를 기다리는 resolver 맵.\r\n  // resize()가 도착하면 ptySizes를 채운 뒤 resolver를 호출해 spawn을 깨운다.\r\n  private pendingFirstResize = new Map<string, () => void>()\r\n  private pendingDisconnects = new Set<string>()\r\n  // ── PTY 권위 모델 (Phase 1, 20260519 기획안) ────────────────────────────────\r\n  // 60s TTL 휴리스틱(mobileAuthorityUntil) 을 명시적 handover 모달 + grace timer 로\r\n  // 대체. AuthorityResolver 가 머신별 상태기계 (no-mobile / awaiting-handover /\r\n  // mobile-leading / mobile-rejected / grace-pending) 를 관리하고, resize() 는 source\r\n  // 가 현재 leader 와 일치할 때만 통과시킨다.\r\n  private lastDesktopSize = new Map<string, { cols: number; rows: number }>()\r\n  public readonly authority: AuthorityResolver\r\n  /**\r\n   * [Reclaim guard 20260606] 에이전트 turn-busy 조회 provider. HookReceiver(claude_state)가\r\n   * index.ts 에서 늦게 생성되므로 생성자 시점엔 미지정 — `setSessionBusyProvider` 로 후주입.\r\n   * AuthorityResolver 의 `isSessionBusy` dep 클로저가 매 tick 본 필드를 읽는다(late-bound).\r\n   */\r\n  private sessionBusyProvider: ((machineId: string) => boolean) | null = null\r\n  /**\r\n   * [Cockpit 신선도 20260614] running 콕핏 세션 id provider. AgentRunner 가 ws-manager 에서\r\n   * 늦게 생성되므로 생성자 시점엔 미지정 — `setActiveSessionProvider` 로 후주입(sessionBusyProvider\r\n   * 와 동일 패턴). `broadcastAgentSessions` 가 디스크 flush 전 running 장수명 세션을 목록에 합류시킬 때 읽는다.\r\n   */\r\n  private activeSessionProvider: ((machineId: string) => string[]) | null = null\r\n  // ── Multiplexer SessionManager cache (Phase 1, docs/plans/0001-...) ────────\r\n  // setting 별 1개. machine 의 multiplexer 값이 같으면 매니저 공유.\r\n  private sessionManagers = new Map<MultiplexerSetting, SessionManager>()\r\n  // PTY 영속화 (Approach A) — 머신별 scrollback ring buffer.\r\n  // PTY 의 모든 stdout/stderr 가 들어옴. ws-manager 가 attach 시 청크 분할해서 송신.\r\n  // restartPty() 시 폐기. cleanup() 에서 전체 폐기.\r\n  private scrollbacks = new Map<string, PtyScrollbackBuffer>()\r\n  // v1.5.0 — scrollback 디스크 persist + restore-on-launch.\r\n  // 앱 종료 시 buffers → 디스크 JSON, 다음 launch 의 첫 attach 시 deserialize 복원.\r\n  // 평문 저장 (CHANGELOG privacy note). 암호화는 v1.6+ 후속.\r\n  private persister: ScrollbackPersister\r\n  // restartPty() 더블 클릭 방지 lock. CRITICAL GAP #5 fix.\r\n  // 200ms 후 자동 release 로 debounce 효과. connect() 가 async 라 promise 기반은 복잡함.\r\n  private restartingMachines = new Set<string>()\r\n  private broadcast: BroadcastFn\r\n  // PTY lifecycle policy (count cap, idle timer, pty_status broadcast).\r\n  // ssh-manager 의 lifecycle 책임을 추출 — CLAUDE.md 10-메서드 규칙 + 미래 E1\r\n  // daemon 분리 시 자연 boundary. 자세한 설계는 docs/adr/0001-pty-persistence.md.\r\n  public readonly lifecycle: PtyLifecycleManager\r\n  /**\r\n   * A3 (Sub-Part 1F-ii-b) — GRID_DIFF (0x05) binary frame broadcast 콜백.\r\n   *\r\n   * ws-manager 가 [setBinaryBroadcast] 로 inject. cap=`grid.diff` 인 client 에만\r\n   * fan-out 책임은 ws-manager 가짐 (capability filter).\r\n   *\r\n   * 본 콜백이 set 되어 있으면 PTY data flush 시점 (16ms 디바운스 후) 에\r\n   * `lifecycle.flushDiff(machineId, frame => binaryBroadcast(machineId, frame))` 호출.\r\n   * undefined 면 silent skip — emulator 는 grid 만 누적, 송신 0 (예: production mobile\r\n   * 이 모두 cap=pty.data 인 경우).\r\n   */\r\n  private binaryBroadcast?: (machineId: string, frame: Buffer) => void\r\n  private _destroyed = false\r\n  private cwdMap = new Map<string, string>()\r\n  private statusTimers = new Map<string, ReturnType<typeof setInterval>>()\r\n  private gitLimiters = new Map<string, ReturnType<typeof pLimit>>()\r\n  // [Cockpit 신선도] 머신별 빈-세션 재스캔 취소 핸들. 새 agent.sessions.list 요청·disconnect·\r\n  // cleanup 시 직전 스케줄을 취소해 중복 broadcast / stale 타이머를 막는다.\r\n  private agentSessionsRescanCancel = new Map<string, () => void>()\r\n\r\n  /**\r\n   * task 10 C7 (2026-05-10) — session_attach 시점에 mobile 측 viewport size 를\r\n   * 첫 PTY spawn cue 로 주입한다. 내부적으로 [ptySizes] 에 그대로 set 해서\r\n   * [resolveSpawnSize] 의 stored cue 경로를 그대로 활용한다 (별 hint Map 불필요).\r\n   *\r\n   * 호출처: ws-manager 의 handleSessionAttach (PTY 가 아직 없는 경우만).\r\n   * PTY 가 이미 살아있으면 호출자가 [resize] 를 직접 호출 — hint path 와 분리.\r\n   *\r\n   * Validity guard: cols/rows 가 [isSpawnCueValid] (cols>=40, rows>=24) 미달이면\r\n   * resolveSpawnSize 가 fallback 으로 자동 대체 → set 자체는 무해.\r\n   */\r\n  setSpawnSizeHint(machineId: string, cols: number, rows: number): void {\r\n    this.ptySizes.set(machineId, { cols, rows })\r\n  }\r\n\r\n  /**\r\n   * 현재 머신의 PTY 폭/높이를 반환한다 (없으면 undefined).\r\n   *\r\n   * 호출처: ws-manager 의 handleSessionAttach — mobile resize *직전* 에 호출해\r\n   * scrollback 이 **생성된 당시의 폭**을 얻어 session_replay 에 동봉한다. 모바일은 그 폭으로\r\n   * raw replay 를 먼저 그린 뒤 viewport 폭으로 reflow → 재접속 시 스크롤백이 좁은 폭으로\r\n   * 재-wrap 되어 좌측으로 붕괴하던 버그(20260620) 회피. (ring buffer 자체는 raw 바이트만\r\n   * 보관해 per-line 폭을 모르므로, attach 직전 현재 폭을 tail 의 생성 폭 근사로 사용.)\r\n   */\r\n  getPtySize(machineId: string): { cols: number; rows: number } | undefined {\r\n    return this.ptySizes.get(machineId)\r\n  }\r\n\r\n  /**\r\n   * A3 (Sub-Part 1F-ii-b) — GRID_DIFF binary broadcast 콜백 inject.\r\n   *\r\n   * ws-manager constructor 가 인스턴스화 직후 호출:\r\n   * ```ts\r\n   * ssh.setBinaryBroadcast((mid, frame) => this.broadcastGridFrame(mid, frame))\r\n   * ```\r\n   * 한 번만 set — 중복 호출 시 마지막 콜백이 승.\r\n   */\r\n  setBinaryBroadcast(fn: (machineId: string, frame: Buffer) => void): void {\r\n    this.binaryBroadcast = fn\r\n  }\r\n\r\n  constructor(broadcast: BroadcastFn) {\r\n    this.broadcast = broadcast\r\n    this.lifecycle = new PtyLifecycleManager({\r\n      currentPtyCount: () =>\r\n        this.sessions.size +\r\n        this.localSessions.size +\r\n        this.pendingFirstResize.size,\r\n      evictMachine: id => this.disconnect(id),\r\n      isRestarting: id => this.restartingMachines.has(id),\r\n      // pty-lifecycle 은 항상 renderer + mobile 양쪽 fan-out (skipRenderer 옵션 사용 안 함)\r\n      broadcast: msg => this.broadcast(msg),\r\n    })\r\n    // Phase 1 — AuthorityResolver 위임. resize() 의 source 가드, mobile attach/detach\r\n    // 시 ws-manager 가 호출, expireGrace 가 deps.applyResize 로 desktop-restore 트리거.\r\n    this.authority = new AuthorityResolver({\r\n      broadcast: this.broadcast,\r\n      applyResize: (mid, cols, rows, source) =>\r\n        this.resize(mid, cols, rows, source),\r\n      getLastDesktopSize: mid => this.lastDesktopSize.get(mid),\r\n      // [Reclaim guard 20260606] handed_off watchdog 가 응답 스트리밍/에이전트 턴 중 회수를\r\n      // 보류하도록 PTY 출력 활동 + busy 신호 주입. getLastPtyActivity 는 lifecycle 의\r\n      // lastActivity(출력/입력으로 갱신) 재사용. isSessionBusy 는 후주입 provider(late-bound).\r\n      getLastPtyActivity: mid => this.lifecycle.getLastActivity(mid),\r\n      isSessionBusy: mid => this.sessionBusyProvider?.(mid) ?? false,\r\n    })\r\n    this.persister = new ScrollbackPersister(resolveScrollbackStateDir())\r\n    const saved = loadPersistedMachines()\r\n    if (saved && saved.length > 0) {\r\n      this.machines = saved\r\n    }\r\n\r\n    const hasLocal = this.machines.some(m => m.machineType === 'local')\r\n    if (!hasLocal) {\r\n      this.addMachine({\r\n        name: 'localhost',\r\n        host: '127.0.0.1',\r\n        port: 22,\r\n        username: os.userInfo().username,\r\n        machineType: 'local',\r\n      })\r\n    }\r\n\r\n    // v1.5.0 — orphan scrollback 파일 정리. UI 에서 삭제됐는데 디스크에 남은 backlog 제거.\r\n    // 동기 호출 — 시작 비용 작음 (readdir + 비교 + unlink). 실패는 swallow.\r\n    try {\r\n      const known = new Set(this.machines.map(m => m.id))\r\n      this.persister.cleanupStale(known)\r\n    } catch {\r\n      // ignore — orphan 정리 실패해도 라이브 동작에 영향 없음.\r\n    }\r\n  }\r\n\r\n  /**\r\n   * [Reclaim guard 20260606] 에이전트 turn-busy provider 후주입.\r\n   *\r\n   * index.ts 가 HookReceiver(claude_state) 생성 직후 호출 — `mid => hookReceiver.isAgentBusy(mid)`.\r\n   * HookReceiver 가 비활성(`CLAUDE_HOOKS_DISABLED`/초기화 실패)이면 미호출 → provider=null →\r\n   * AuthorityResolver 는 busy=false 로 graceful degrade(PTY 게이트 + 절대 상한만으로 동작).\r\n   */\r\n  setSessionBusyProvider(fn: (machineId: string) => boolean): void {\r\n    this.sessionBusyProvider = fn\r\n  }\r\n\r\n  /**\r\n   * [Cockpit 신선도 20260614] running 콕핏 세션 id provider 후주입.\r\n   *\r\n   * ws-manager 가 AgentRunner 생성 직후 호출 — `mid => agentRunner.getActiveSessionIds(mid)`.\r\n   * 미주입(null)이면 [broadcastAgentSessions] 는 디스크 스캔 결과를 그대로 broadcast(graceful degrade).\r\n   */\r\n  setActiveSessionProvider(fn: (machineId: string) => string[]): void {\r\n    this.activeSessionProvider = fn\r\n  }\r\n\r\n  getMachines(): Machine[] {\r\n    return this.machines\r\n  }\r\n\r\n  broadcastMachines(): void {\r\n    this.broadcast({ type: 'machine_list', machines: this.machines })\r\n  }\r\n\r\n  broadcastSshKeys(): void {\r\n    this.broadcast({ type: 'ssh_keys', keys: this.listAvailableKeys() })\r\n  }\r\n\r\n  listAvailableKeys(): string[] {\r\n    try {\r\n      const dir = path.join(homeDir, '.ssh')\r\n      return fs\r\n        .readdirSync(dir)\r\n        .filter(f => !f.endsWith('.pub') && !SSH_KEY_EXCLUDE.has(f))\r\n        .map(f => path.join(dir, f))\r\n    } catch {\r\n      return []\r\n    }\r\n  }\r\n\r\n  editMachine(data: Omit<Machine, 'status'>): Machine {\r\n    let updated: Machine | undefined\r\n\r\n    this.machines = this.machines.map(item => {\r\n      if (item.id === data.id) {\r\n        updated = { ...item, ...data }\r\n        return updated\r\n      }\r\n      return item\r\n    })\r\n\r\n    if (!updated) {\r\n      throw new Error('not found Machine')\r\n    }\r\n\r\n    savePersistedMachines(this.machines)\r\n    return updated\r\n  }\r\n\r\n  addMachine(data: Omit<Machine, 'id' | 'status'>): Machine {\r\n    const machine: Machine = {\r\n      ...data,\r\n      id: crypto.randomUUID(),\r\n      status: 'disconnected',\r\n    }\r\n    this.machines.push(machine)\r\n    savePersistedMachines(this.machines)\r\n    return machine\r\n  }\r\n\r\n  removeMachine(id: string): void {\r\n    this.disconnect(id)\r\n    this.machines = this.machines.filter(m => m.id !== id)\r\n    this.ptySizes.delete(id)\r\n    // disconnect() 가 lifecycle.onDisconnect 호출하지만, removeMachine 은 의도가\r\n    // 다르므로 explicit cleanup 도 추가 (defensive — 추후 disconnect chain 변경 대비).\r\n    this.lifecycle.onRemoveMachine(id)\r\n    // Phase 1 [Review P1] — authority resolver state + grace/pending timer 정리.\r\n    // lastDesktopSize 도 같이 — 머신 삭제 후 잔존하면 다음 same-id 머신 시 stale.\r\n    this.authority.removeMachine(id)\r\n    this.lastDesktopSize.delete(id)\r\n    savePersistedMachines(this.machines)\r\n  }\r\n\r\n  connect(\r\n    machineId: string,\r\n    { forceReConnect = false }: SSHManagerConnectOptions = {}\r\n  ): void {\r\n    const machine = this.machines.find(m => m.id === machineId)\r\n\r\n    if (!machine) return\r\n\r\n    if (machine.status === 'connected' && forceReConnect === false) {\r\n      return\r\n    }\r\n\r\n    // PTY count cap 검사 + (필요 시) LRU eviction. **첫 await 전 동기**.\r\n    // 같은 tick 동시 connect 호출은 pendingFirstResize.set 이 await 전이라 보호.\r\n    // Approach A 의 영속 PTY 가 32개 cap 초과하지 않도록.\r\n    this.lifecycle.onConnect(machineId)\r\n\r\n    if (machine.machineType === 'local') {\r\n      // connectLocal은 spawn-deferring 때문에 async — fire-and-forget으로 호출.\r\n      void this.connectLocal(machineId, machine)\r\n      return\r\n    }\r\n\r\n    // Approach A — PTY 영속화: 기존 SSH 세션이 살아있으면 재사용한다.\r\n    // 모바일이 머신을 다시 선택했을 때 자식 프로세스 (claude/npm/vim/...) 가 죽지\r\n    // 않고 그대로 살아남도록. ws-manager 가 attach 시 scrollback 을 replay 함.\r\n    if (this.sessions.has(machineId)) {\r\n      // 이미 connected 상태로 보고 종료. 호출자(ws-manager)가 attach replay 송신.\r\n      this.updateStatus(machineId, 'connected')\r\n      return\r\n    }\r\n\r\n    const connectOptions: ConnectConfig = {\r\n      host: machine.host,\r\n      port: machine.port,\r\n      username: machine.username,\r\n      hostVerifier: () => true,\r\n    }\r\n\r\n    if (machine.authMethod === 'password') {\r\n      if (!machine.password) {\r\n        this.updateStatus(machineId, 'error', 'Password is required')\r\n        return\r\n      }\r\n      connectOptions.password = machine.password\r\n    } else {\r\n      if (machine.privateKeyPath) {\r\n        try {\r\n          connectOptions.privateKey = fs.readFileSync(machine.privateKeyPath)\r\n        } catch {\r\n          this.updateStatus(\r\n            machineId,\r\n            'error',\r\n            `Key file not found: ${machine.privateKeyPath}`\r\n          )\r\n          return\r\n        }\r\n      } else {\r\n        const privateKey = findPrivateKey()\r\n        if (!privateKey) {\r\n          this.updateStatus(\r\n            machineId,\r\n            'error',\r\n            'SSH key not found. Select a key or use password auth.'\r\n          )\r\n          return\r\n        }\r\n        connectOptions.privateKey = privateKey\r\n      }\r\n    }\r\n\r\n    this.updateStatus(machineId, 'connecting')\r\n    const client = new Client()\r\n    this.sessions.set(machineId, { client, stream: null })\r\n\r\n    client.on('ready', async () => {\r\n      // spawn-deferring: 클라이언트의 첫 terminal_resize를 spawn cue로 사용해\r\n      // shell이 모바일 측정 cols/rows로 직접 spawn되도록 한다 (race fix).\r\n      // machine.spawnDeferTimeoutMs (default 600ms) 안에 도착 안 하면 fallback. 재연결이면 ptySizes 캐시로 즉시 진행.\r\n      await this.waitForFirstResize(\r\n        machineId,\r\n        machine.spawnDeferTimeoutMs ?? 600\r\n      )\r\n      // 대기 중 disconnect 또는 새 connect로 client 교체 가능 — 정체성 확인 후 종료.\r\n      const currentSession = this.sessions.get(machineId)\r\n      if (!currentSession || currentSession.client !== client) return\r\n      if (this.pendingDisconnects.has(machineId)) {\r\n        this.pendingDisconnects.delete(machineId)\r\n        return\r\n      }\r\n      const stored = this.ptySizes.get(machineId)\r\n      // F2 — spawn cue sanity guard (task 13). resolveSpawnSize 가 비정상 cue (cols<40\r\n      // 또는 rows<10) 를 fallback 으로 대체. 자세한 동기는 resolveSpawnSize doc 참조.\r\n      const {\r\n        cols: spawnCols,\r\n        rows: spawnRows,\r\n        source,\r\n      } = resolveSpawnSize(\r\n        stored,\r\n        machine.spawnFallbackCols ?? 80,\r\n        machine.spawnFallbackRows ?? 24\r\n      )\r\n      if (source === 'fallback-invalid-cue') {\r\n        sshLog.warn(\r\n          `[pty] spawn cue 비정상 ssh machine=${machineId} stored=${stored?.cols}x${stored?.rows} — fallback ${spawnCols}x${spawnRows}`\r\n        )\r\n      }\r\n      sshLog.info(\r\n        `[pty] spawn ssh machine=${machineId} ${spawnCols}x${spawnRows} source=${source}`\r\n      )\r\n\r\n      client.shell(\r\n        { term: 'xterm-256color', cols: spawnCols, rows: spawnRows },\r\n        (err, stream) => {\r\n          if (err) {\r\n            this.updateStatus(machineId, 'error', err.message)\r\n            this.sessions.delete(machineId)\r\n            return\r\n          }\r\n\r\n          const session = this.sessions.get(machineId)\r\n          if (session) session.stream = stream\r\n\r\n          this.updateStatus(machineId, 'connected')\r\n          this.startStatusTimer(machineId)\r\n\r\n          // shell이 spawnCols/Rows로 직접 spawn되므로 spawn 직후 setWindow 불필요.\r\n          // ack는 클라이언트에 spawn 사이즈를 통지하기 위해 1회 보낸다.\r\n          this.broadcast({\r\n            type: 'terminal_resize_ack',\r\n            machineId,\r\n            cols: spawnCols,\r\n            rows: spawnRows,\r\n          })\r\n\r\n          // ── 16ms 배치 처리 (IPC 성능) ──────────────────────────────────────\r\n          // ⚠️ 반드시 연결 setup 내부에 선언 — 모듈 레벨이면 모든 머신이 버퍼 공유 (버그)\r\n          let _buffer = ''\r\n          let _flushTimer: ReturnType<typeof setTimeout> | null = null\r\n\r\n          // Stateful UTF-8 decoder — chunk 경계에서 잘린 멀티바이트(한글 받침,\r\n          // 이모지, ANSI 시퀀스 후속 byte)를 다음 chunk와 합쳐 안전하게 디코드한다.\r\n          // stdout/stderr가 인터리브되면 시퀀스가 섞일 수 있어 각각 분리 인스턴스.\r\n          const stdoutDecoder = new StringDecoder('utf8')\r\n          const stderrDecoder = new StringDecoder('utf8')\r\n\r\n          // Approach A — scrollback ring buffer. 머신별 1개. 첫 attach 까지 누적되며\r\n          // 5MB 도달 시 head 부터 자동 drop. ws-manager.handleAttach() 가 청크 분할 송신.\r\n          const scrollback = this.getOrCreateScrollback(machineId)\r\n\r\n          const flushData = () => {\r\n            this.broadcast({ type: 'terminal_data', machineId, data: _buffer })\r\n            // A3 (Sub-Part 1F-ii-b): emulator 의 dirty cell → GRID_DIFF (0x05) broadcast.\r\n            // 16ms 디바운스 안에서 1회만 — 효율적. flushDiff 가 dirty 0 이면 silent skip.\r\n            // capability filter (cap=grid.diff client 만) 는 ws-manager 의 broadcastGridFrame 책임.\r\n            if (this.binaryBroadcast) {\r\n              this.lifecycle.flushDiff(machineId, frame =>\r\n                this.binaryBroadcast!(machineId, frame)\r\n              )\r\n            }\r\n            _buffer = ''\r\n            _flushTimer = null\r\n          }\r\n\r\n          stream.on('data', (data: Buffer) => {\r\n            const str = stdoutDecoder.write(data)\r\n            this.parseOsc7(str, machineId)\r\n            scrollback.append(str)\r\n            this.persister.scheduleFlush(machineId, scrollback)\r\n            this.lifecycle.onDataActivity(machineId)\r\n            // A3 (Sub-Part 1F-i): emulator append — grid 누적. broadcast 자체는 본 turn 에서 미발사\r\n            // (cap.ack 가 항상 'pty.data' fallback 이라 cap=grid.diff client 0). silent.\r\n            this.lifecycle.onPtyData(machineId, str)\r\n            _buffer += str\r\n            if (!_flushTimer) _flushTimer = setTimeout(flushData, 16)\r\n          })\r\n\r\n          stream.stderr.on('data', (data: Buffer) => {\r\n            const str = stderrDecoder.write(data)\r\n            scrollback.append(str)\r\n            this.persister.scheduleFlush(machineId, scrollback)\r\n            this.lifecycle.onDataActivity(machineId)\r\n            // A3 (Sub-Part 1F-i): stderr 도 ANSI sequence 가질 수 있어 emulator 에 fed.\r\n            this.lifecycle.onPtyData(machineId, str)\r\n            _buffer += str\r\n            if (!_flushTimer) _flushTimer = setTimeout(flushData, 16)\r\n          })\r\n\r\n          stream.on('close', () => {\r\n            // PTY 자연 종료 — pty_status('dead', natural) broadcast.\r\n            // F2 (task 13 v2) — exit 진단 log. local 측과 동일.\r\n            sshLog.info(`[pty] exit ssh machine=${machineId}`)\r\n            // [A2 spike 20260523] mobile 권위 상태에서 PTY 죽으면 viewer_mode='error'\r\n            // broadcast — desktop UI 의 rose-tone \"Connection lost — phone offline too\"\r\n            // (Design Fix-E D2-δ row 9). leader=mobile 아니면 resolver 내부에서 no-op.\r\n            this.authority.handleAgentCrash(machineId)\r\n            this.lifecycle.onPtyExit(machineId)\r\n            // decoder 잔여 byte를 마지막으로 flush\r\n            const tail = stdoutDecoder.end() + stderrDecoder.end()\r\n            if (tail) _buffer += tail\r\n            if (_buffer || _flushTimer) {\r\n              if (_flushTimer) {\r\n                clearTimeout(_flushTimer)\r\n                _flushTimer = null\r\n              }\r\n              if (_buffer)\r\n                this.broadcast({\r\n                  type: 'terminal_data',\r\n                  machineId,\r\n                  data: _buffer,\r\n                })\r\n              _buffer = ''\r\n            }\r\n            this.clearStatusTimer(machineId)\r\n            this.updateStatus(machineId, 'disconnected')\r\n            this.sessions.delete(machineId)\r\n          })\r\n        }\r\n      )\r\n    })\r\n\r\n    client.on('error', err => {\r\n      this.updateStatus(machineId, 'error', err.message)\r\n      this.sessions.delete(machineId)\r\n    })\r\n\r\n    client.connect(connectOptions)\r\n  }\r\n\r\n  private async connectLocal(\r\n    machineId: string,\r\n    machine: Machine\r\n  ): Promise<void> {\r\n    // Approach A — PTY 영속화: 기존 PTY 가 살아있으면 kill 하지 않고 재사용.\r\n    // 사용자 우려의 핵심 해결: claude/npm/vim/dev-server 등 자식 프로세스가 SIGHUP 으로\r\n    // 죽지 않도록. 호출자(ws-manager)가 attach 직후 scrollback replay 송신.\r\n    if (this.localSessions.has(machineId)) {\r\n      this.updateStatus(machineId, 'connected')\r\n      return\r\n    }\r\n\r\n    this.updateStatus(machineId, 'connecting')\r\n\r\n    try {\r\n      const userShell = machine.localShell\r\n      const envShell = process.env.SHELL\r\n      const shell =\r\n        userShell && userShell.length > 0\r\n          ? userShell\r\n          : envShell && envShell.length > 0\r\n            ? envShell\r\n            : process.platform === 'win32'\r\n              ? (process.env.COMSPEC ?? 'cmd.exe')\r\n              : '/bin/zsh'\r\n\r\n      const env: Record<string, string> = {\r\n        HOME: homeDir,\r\n        LOGNAME: os.userInfo().username,\r\n        USER: os.userInfo().username,\r\n        SHELL: shell,\r\n        ...(process.env as Record<string, string>),\r\n        TERM: 'xterm-256color',\r\n        COLORTERM: 'truecolor',\r\n      }\r\n      if (process.platform === 'win32') {\r\n        const shellLower = shell.toLowerCase()\r\n        if (\r\n          shellLower.includes('bash') ||\r\n          shellLower === 'wsl' ||\r\n          shellLower === 'wsl.exe'\r\n        ) {\r\n          const gitBashBin = 'C:\\\\Program Files\\\\Git\\\\usr\\\\bin'\r\n          env.PATH = `${gitBashBin};${env.PATH || env.Path || ''}`\r\n        }\r\n      } else {\r\n        const defaultPaths = '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'\r\n        env.PATH = `${env.PATH || ''}:${defaultPaths}`\r\n      }\r\n\r\n      // spawn-deferring: 클라이언트의 첫 terminal_resize를 기다린다 (machine.spawnDeferTimeoutMs ?? 600 cap).\r\n      // ptySizes 캐시가 있으면 즉시 진행. cmd.exe가 처음부터 모바일 cols로 출력하도록.\r\n      await this.waitForFirstResize(\r\n        machineId,\r\n        machine.spawnDeferTimeoutMs ?? 600\r\n      )\r\n      // 대기 중 disconnect 가능 — pending 표시되어 있으면 종료.\r\n      if (this.pendingDisconnects.has(machineId)) {\r\n        this.pendingDisconnects.delete(machineId)\r\n        return\r\n      }\r\n      const stored = this.ptySizes.get(machineId)\r\n      // F2 — spawn cue sanity guard (task 13). 자세한 동기는 resolveSpawnSize doc 참조.\r\n      const {\r\n        cols: spawnCols,\r\n        rows: spawnRows,\r\n        source: spawnSource,\r\n      } = resolveSpawnSize(\r\n        stored,\r\n        machine.spawnFallbackCols ?? 80,\r\n        machine.spawnFallbackRows ?? 24\r\n      )\r\n      if (spawnSource === 'fallback-invalid-cue') {\r\n        sshLog.warn(\r\n          `[pty] spawn cue 비정상 local machine=${machineId} stored=${stored?.cols}x${stored?.rows} — fallback ${spawnCols}x${spawnRows}`\r\n        )\r\n      }\r\n\r\n      // ── SessionManager 위임 ───────────────────────────────────────────────\r\n      // 멀티플렉서 제거(2026-06-23) 후 sessionMgr 는 항상 NoneSessionManager — hint 를\r\n      // 그대로 SpawnSpec 으로 변환(직접 spawn). attachOrCreate 직접 호출.\r\n      // cwd 존재 검증 — 없는 디렉토리를 ConPTY/CreateProcess 에 넘기면 Windows 에서\r\n      // error 267 (ERROR_DIRECTORY) 로 spawn 자체가 실패한다 (localCwd 가 다른 OS 의\r\n      // 경로이거나 삭제된 폴더인 경우). 존재하지 않으면 homeDir 로 fallback 하고, 연결 직후\r\n      // 터미널 첫 줄에 경고를 남겨 사용자가 지정한 cwd 가 무시됐음을 알 수 있게 한다.\r\n      const requestedCwd = machine.localCwd?.trim()\r\n      let effectiveCwd = requestedCwd || homeDir\r\n      let cwdWarningLine: string | null = null\r\n      if (requestedCwd && !isExistingDir(requestedCwd)) {\r\n        effectiveCwd = homeDir\r\n        sshLog.warn(\r\n          `[pty] localCwd 없음 machine=${machineId} cwd=${requestedCwd} → homeDir(${homeDir}) fallback`\r\n        )\r\n        cwdWarningLine =\r\n          `\\r\\n\\x1b[33m⚠ 작업 디렉토리 '${requestedCwd}' 없음\\x1b[0m\\r\\n` +\r\n          `\\x1b[33m  → 홈(${homeDir})에서 연결합니다.\\x1b[0m\\r\\n`\r\n      }\r\n\r\n      const sessionMgr = this.getSessionManager(machine)\r\n      const hint = {\r\n        shell,\r\n        args: machine.args,\r\n        cwd: effectiveCwd,\r\n        env,\r\n        cols: spawnCols,\r\n        rows: spawnRows,\r\n      }\r\n      const spec = await sessionMgr.attachOrCreate(machineId, hint)\r\n      sshLog.info(\r\n        `[pty] spawn local machine=${machineId} ${spawnCols}x${spawnRows} mux=${sessionMgr.kind} source=${spawnSource}`\r\n      )\r\n\r\n      const proc = createNodePty({\r\n        shell: spec.shell,\r\n        args: spec.args,\r\n        cols: spec.cols,\r\n        rows: spec.rows,\r\n        cwd: spec.cwd,\r\n        env: spec.env,\r\n      })\r\n      ;(proc as { _spawnAt?: number })._spawnAt = Date.now()\r\n\r\n      this.localSessions.set(machineId, proc)\r\n      this.reconnectAttempts.delete(machineId)\r\n      this.updateStatus(machineId, 'connected')\r\n      this.startStatusTimer(machineId)\r\n\r\n      // spawn이 spawnCols/Rows로 직접 시작되므로 사후 resize 불필요. ack는 1회 통지.\r\n      this.broadcast({\r\n        type: 'terminal_resize_ack',\r\n        machineId,\r\n        cols: spawnCols,\r\n        rows: spawnRows,\r\n      })\r\n\r\n      // ── 16ms 배치 처리 ────────────────────────────────────────────────────\r\n      // ⚠️ 반드시 연결 setup 내부에 선언 — 모듈 레벨이면 모든 머신이 버퍼 공유 (버그)\r\n      let _buffer = ''\r\n      let _flushTimer: ReturnType<typeof setTimeout> | null = null\r\n\r\n      // Approach A — scrollback ring buffer (머신별 1개, 5MB cap).\r\n      const scrollback = this.getOrCreateScrollback(machineId)\r\n\r\n      // cwd fallback 경고를 터미널 첫 줄에 주입한다. PTY onData 는 async 로 들어오므로\r\n      // 동기 broadcast 가 항상 먼저 정렬된다. scrollback 에도 넣어 재접속/모바일 attach\r\n      // 시 backlog 에 보존.\r\n      if (cwdWarningLine) {\r\n        scrollback.append(cwdWarningLine)\r\n        this.broadcast({ type: 'terminal_data', machineId, data: cwdWarningLine })\r\n      }\r\n\r\n      const flushData = () => {\r\n        this.broadcast({ type: 'terminal_data', machineId, data: _buffer })\r\n        // A3 (Sub-Part 1F-ii-b): local PTY path — SSH path 와 동일 emitter 패턴.\r\n        if (this.binaryBroadcast) {\r\n          this.lifecycle.flushDiff(machineId, frame =>\r\n            this.binaryBroadcast!(machineId, frame)\r\n          )\r\n        }\r\n        _buffer = ''\r\n        _flushTimer = null\r\n      }\r\n\r\n      proc.onData((data: string) => {\r\n        this.parseOsc7(data, machineId)\r\n        scrollback.append(data)\r\n        this.persister.scheduleFlush(machineId, scrollback)\r\n        this.lifecycle.onDataActivity(machineId)\r\n        // A3 (Sub-Part 1F-i): emulator append — grid 누적 (local PTY path).\r\n        this.lifecycle.onPtyData(machineId, data)\r\n        _buffer += data\r\n        if (!_flushTimer) _flushTimer = setTimeout(flushData, 16)\r\n      })\r\n\r\n      proc.onExit((_code: number) => {\r\n        // PTY 자연 종료 — pty_status('dead', natural) broadcast.\r\n        // F2 (task 13 v2) — exit 진단용 명시 log. spawn cycle 회귀 (PTY 가 ~5초 안에\r\n        // 죽으면서 mobile 의 connect msg 가 매번 새 spawn 트리거하는 loop) 의 root cause\r\n        // 추적. uptime + exit code 로 cmd.exe / psmux 자체 종료 vs SIGHUP / 외부 kill 구분.\r\n        const spawnAt = (proc as { _spawnAt?: number })._spawnAt ?? Date.now()\r\n        const uptimeMs = Date.now() - spawnAt\r\n        sshLog.info(\r\n          `[pty] exit local machine=${machineId} code=${_code} uptime=${uptimeMs}ms`\r\n        )\r\n        // [A2 spike 20260523] mobile 권위 상태에서 PTY 죽으면 viewer_mode='error'\r\n        // broadcast — desktop UI 의 rose-tone error placeholder (Design D2-δ row 9).\r\n        this.authority.handleAgentCrash(machineId)\r\n        this.lifecycle.onPtyExit(machineId)\r\n        if (_flushTimer) {\r\n          clearTimeout(_flushTimer)\r\n          _flushTimer = null\r\n        }\r\n        this.clearStatusTimer(machineId)\r\n        this.localSessions.delete(machineId)\r\n\r\n        // v1.5.0 — C2 shell-exit orphan fix (carry-over from v1.3.4 P-LR).\r\n        // 사용자가 inner shell 에서 `exit` 시 bridged psmux/tmux 세션도 kill.\r\n        // 안 하면 sleep 86400 dummy 가 영구 살아남음 (v1.3.4 production 누적 확인).\r\n        // findCachedSessionManagerFor 는 어떤 매니저든 캐시된 것 반환 — psmux/tmux 양쪽 호환.\r\n        const sessionMgr = this.findCachedSessionManagerFor(machineId)\r\n        if (sessionMgr) {\r\n          try {\r\n            void Promise.resolve(sessionMgr.kill(machineId)).catch(\r\n              () => undefined\r\n            )\r\n          } catch {\r\n            // kill sync throw — 무해.\r\n          }\r\n        }\r\n\r\n        if (this.pendingDisconnects.has(machineId)) {\r\n          this.pendingDisconnects.delete(machineId)\r\n          return\r\n        }\r\n\r\n        const currentMachine = this.machines.find(m => m.id === machineId)\r\n        if (!currentMachine) return\r\n\r\n        const attempts = (this.reconnectAttempts.get(machineId) ?? 0) + 1\r\n        if (attempts >= 3) {\r\n          this.reconnectAttempts.delete(machineId)\r\n          this.updateStatus(\r\n            machineId,\r\n            'error',\r\n            `PTY 비정상 종료 (${shell}) — 3회 재시도 실패`\r\n          )\r\n          return\r\n        }\r\n        this.reconnectAttempts.set(machineId, attempts)\r\n        this.updateStatus(machineId, 'disconnected')\r\n        setTimeout(() => {\r\n          const m = this.machines.find(m => m.id === machineId)\r\n          if (m) void this.connectLocal(machineId, m)\r\n        }, 2000)\r\n      })\r\n    } catch (err) {\r\n      const userShell = machine.localShell\r\n      const envShell = process.env.SHELL\r\n      const failedShell =\r\n        userShell ||\r\n        envShell ||\r\n        (process.platform === 'win32' ? 'cmd.exe' : '/bin/zsh')\r\n      sshLog.error(`Local PTY 실패 (${failedShell}): ${(err as Error).message}`)\r\n      this.updateStatus(\r\n        machineId,\r\n        'error',\r\n        `Local PTY 실패 (${failedShell}): ${(err as Error).message}`\r\n      )\r\n    }\r\n  }\r\n\r\n  disconnect(machineId: string): void {\r\n    this.clearStatusTimer(machineId)\r\n    // pty_status('dead', 'natural') broadcast FIRST — 모바일이 dead 먼저 받아\r\n    // UI 전환, 그 후 connection_status('disconnected'). lifecycle metadata cleanup 도 같이.\r\n    this.lifecycle.onDisconnect(machineId, 'natural')\r\n\r\n    // [Cockpit 신선도] 진행 중인 빈-세션 재스캔 타이머 해제 (머신이 사라졌으므로 무의미).\r\n    this.cancelAgentSessionsRescan(machineId)\r\n\r\n    // pendingDisconnects는 in-flight spawn coroutine을 abort하기 위한 플래그.\r\n    // 실제로 abort할 대상이 있을 때만 세팅 — 무조건 add하면 다음 connect의\r\n    // post-await 가드가 이 플래그를 보고 spawn을 abort해 \"connecting\" 멈춤.\r\n    const wake = this.pendingFirstResize.get(machineId)\r\n    const localProc = this.localSessions.get(machineId)\r\n    const session = this.sessions.get(machineId)\r\n    const hasInFlight =\r\n      wake !== undefined || localProc !== undefined || session !== undefined\r\n\r\n    if (hasInFlight) {\r\n      this.pendingDisconnects.add(machineId)\r\n    }\r\n    if (wake) wake() // 200ms timeout 대기 즉시 해제\r\n\r\n    // 사용자 명시 disconnect — PTY 와 scrollback 모두 폐기. session_detach (모바일이\r\n    // 화면을 떠남) 와 다른 의미. 머신 자체를 끄려는 사용자 의도이므로 진짜 종료.\r\n    // v1.5.0: 디스크 persist 파일도 함께 제거 (다음 launch 에서 빈 backlog).\r\n    this.scrollbacks.delete(machineId)\r\n    try {\r\n      this.persister.remove(machineId)\r\n    } catch {\r\n      // remove 실패 — 무해.\r\n    }\r\n\r\n    if (localProc) {\r\n      localProc.kill()\r\n      this.localSessions.delete(machineId)\r\n      this.updateStatus(machineId, 'disconnected')\r\n      return\r\n    }\r\n\r\n    if (session) {\r\n      session.stream?.end()\r\n      session.client.end()\r\n      this.sessions.delete(machineId)\r\n    }\r\n    this.updateStatus(machineId, 'disconnected')\r\n  }\r\n\r\n  // ─── PTY persistence (Approach A) — public API for ws-manager ────────────────\r\n\r\n  /** 머신 ID 의 PTY 가 살아있는지. ws-manager 가 attach 전에 spawn 필요 여부 판단. */\r\n  hasSession(machineId: string): boolean {\r\n    return this.localSessions.has(machineId) || this.sessions.has(machineId)\r\n  }\r\n\r\n  /**\r\n   * 현재 살아있는 PTY (local + SSH) 총합.\r\n   *\r\n   * 업데이트 install 직전 mobile-safe gate (autoplan E1) 에서 사용 — count > 0 이면\r\n   * 사용자에게 confirmation 다이얼로그를 띄워 quitAndInstall 를 보류한다.\r\n   *\r\n   * `pendingFirstResize` 는 제외 — 아직 spawn 전 단계이고, 사용자에게는 \"활성 세션\"\r\n   * 으로 인지되지 않는다. 모바일 attach 도중인 race 윈도우는 본 gate 가 그 시점의\r\n   * snapshot 만 보기 때문에 false negative 가능하지만, 다음 update 트리거에서 다시\r\n   * 잡힌다 (1시간 폴링).\r\n   */\r\n  getActivePtyCount(): number {\r\n    return this.sessions.size + this.localSessions.size\r\n  }\r\n\r\n  /** 머신 ID 의 scrollback. 없으면 null. ws-manager 가 attach 시 청크 분할 송신. */\r\n  getScrollback(machineId: string): PtyScrollbackBuffer | null {\r\n    return this.scrollbacks.get(machineId) ?? null\r\n  }\r\n\r\n  /**\r\n   * PTY 강제 재시작 (E4). 진행중인 자식 프로세스 모두 SIGHUP, scrollback 비움, 새 spawn.\r\n   * 사용자가 모바일에서 명시적으로 호출. 200ms debounce lock 으로 더블 클릭 방어.\r\n   */\r\n  restartPty(machineId: string): void {\r\n    if (this.restartingMachines.has(machineId)) {\r\n      sshLog.info(\r\n        `[ssh-manager] restartPty ignored (in-flight) machine=${machineId}`\r\n      )\r\n      return\r\n    }\r\n    this.restartingMachines.add(machineId)\r\n    try {\r\n      sshLog.info(`[ssh-manager] restartPty machine=${machineId}`)\r\n      const machine = this.machines.find(m => m.id === machineId)\r\n      if (!machine) return\r\n\r\n      // 기존 PTY kill\r\n      const localProc = this.localSessions.get(machineId)\r\n      if (localProc) {\r\n        localProc.kill()\r\n        this.localSessions.delete(machineId)\r\n      }\r\n      const session = this.sessions.get(machineId)\r\n      if (session) {\r\n        session.stream?.end()\r\n        session.client.end()\r\n        this.sessions.delete(machineId)\r\n      }\r\n      // scrollback 비움 — 새 PTY 의 출력만 남도록\r\n      // v1.5.0: 디스크 파일도 제거 (restart 는 backlog 명시 reset 의도).\r\n      this.scrollbacks.delete(machineId)\r\n      try {\r\n        this.persister.remove(machineId)\r\n      } catch {\r\n        // ignore\r\n      }\r\n      // lifecycle metadata reset (lastAttach=now, attachCount+1) + dead broadcast\r\n      // 후 새 connect() 가 onConnect 호출해 새 metadata 생성.\r\n      this.lifecycle.onRestartPty(machineId)\r\n\r\n      // status reset 후 재spawn — connect() 의 already-connected fastpath 를 우회\r\n      machine.status = 'disconnected'\r\n      this.connect(machineId, { forceReConnect: true })\r\n    } finally {\r\n      // connect() 가 async 라서 즉시 release 하면 race. 200ms 후 release 로 debounce.\r\n      setTimeout(() => this.restartingMachines.delete(machineId), 200)\r\n    }\r\n  }\r\n\r\n  /**\r\n   * 머신별 scrollback 을 lazily 생성. PTY 출력 callback 에서 호출.\r\n   *\r\n   * v1.5.0: 첫 생성 시 디스크에서 restore 시도 — 있으면 deserialize, 없으면 새 buffer.\r\n   * 디스크 재생성된 buffer 는 이후 append 흐름에서 자연스럽게 누적된다 (mobile attach 시\r\n   * snapshotChunks 가 옛 backlog + 새 출력 모두 포함).\r\n   */\r\n  private getOrCreateScrollback(machineId: string): PtyScrollbackBuffer {\r\n    let buf = this.scrollbacks.get(machineId)\r\n    if (!buf) {\r\n      // v1.7.8 — per-machine cap. Machine.ringBufferBytes 명시 시 우선, 미설정 시\r\n      // default 1MB (SCROLLBACK_BUFFER_BYTES_DEFAULT). machine 조회 실패 (race\r\n      // 또는 LRU eviction 직후) 시 default fallback — fail-open.\r\n      // cross-repo with `claude_code_mobile` PR #48 의 overlay 노출 시간 비례 단축.\r\n      const machine = this.machines.find(m => m.id === machineId)\r\n      const cap = machine?.ringBufferBytes ?? SCROLLBACK_BUFFER_BYTES_DEFAULT\r\n\r\n      const snapshot = this.persister.restoreSync(machineId)\r\n      if (snapshot) {\r\n        try {\r\n          buf = PtyScrollbackBuffer.deserialize(cap, snapshot)\r\n        } catch (err) {\r\n          // 손상된 snapshot — 무시하고 새 buffer (caller 가 backlog 빈 상태로 시작).\r\n          sshLog.warn(\r\n            `[ssh-manager] scrollback restore failed for ${machineId}: ${(err as Error).message}`\r\n          )\r\n          buf = new PtyScrollbackBuffer(cap)\r\n        }\r\n      } else {\r\n        buf = new PtyScrollbackBuffer(cap)\r\n      }\r\n      this.scrollbacks.set(machineId, buf)\r\n    }\r\n    return buf\r\n  }\r\n\r\n  sendInput(machineId: string, data: string): void {\r\n    const localProc = this.localSessions.get(machineId)\r\n    if (localProc) {\r\n      localProc.write(data)\r\n      this.lifecycle.onUserInput(machineId)\r\n      return\r\n    }\r\n\r\n    const session = this.sessions.get(machineId)\r\n    if (session?.stream) {\r\n      session.stream.write(data)\r\n      this.lifecycle.onUserInput(machineId)\r\n    }\r\n  }\r\n\r\n  resize(\r\n    machineId: string,\r\n    cols: number,\r\n    rows: number,\r\n    source: 'desktop' | 'mobile' | 'desktop-restore' = 'desktop'\r\n  ): void {\r\n    const safeCols = clampCols(cols)\r\n    const safeRows = clampRows(rows)\r\n\r\n    // Phase 1 (20260519) — TTL 휴리스틱 제거, AuthorityResolver 위임.\r\n    // invariant: source 가 현재 leader 와 일치할 때만 적용. 그 외 drop. handover 협상\r\n    // 중이면 모든 source drop (사이즈 깜빡임 차단). desktop-restore 는 무조건 통과.\r\n    if (!this.authority.allowsResize(machineId, source)) {\r\n      if (process.env.NODE_ENV === 'development') {\r\n        sshLog.info(\r\n          `[pty-resize] dropped by authority resolver machine=${machineId} source=${source} ${safeCols}x${safeRows}`\r\n        )\r\n      }\r\n      return\r\n    }\r\n\r\n    // desktop source 면 lastDesktopSize 갱신 — grace expire 시 복귀 사이즈로 사용.\r\n    // desktop-restore 는 이미 lastDesktopSize 가 source 인 경로라 재기록 불필요.\r\n    if (source === 'desktop') {\r\n      this.lastDesktopSize.set(machineId, { cols: safeCols, rows: safeRows })\r\n    }\r\n\r\n    const prev = this.ptySizes.get(machineId)\r\n    // prev=undefined (첫 resize) 면 isNoop=false → 정상 spawn 경로. 의도된 동작.\r\n    const isNoop = prev?.cols === safeCols && prev?.rows === safeRows\r\n\r\n    if (process.env.NODE_ENV === 'development') {\r\n      sshLog.info('[pty-resize]', {\r\n        machineId,\r\n        source,\r\n        raw: { cols, rows },\r\n        safe: { cols: safeCols, rows: safeRows },\r\n        prev,\r\n        isNoop,\r\n      })\r\n    }\r\n\r\n    // ptySizes 갱신 — non-noop 만. wake 보다 먼저 update 해서 spawn awaiter 가\r\n    // 새 ptySizes 를 읽도록 (microtask 순서 의존 없는 robust 패턴).\r\n    if (!isNoop) {\r\n      this.ptySizes.set(machineId, { cols: safeCols, rows: safeRows })\r\n      sshLog.info(\r\n        `[pty-resize] machine=${machineId} source=${source} ${safeCols}x${safeRows} (prev=${prev?.cols ?? '-'}x${prev?.rows ?? '-'})`\r\n      )\r\n      // [attach frame-snapshot 20260610] GridEmulator 를 적용된 PTY 사이즈로 동기화 — 안 하면\r\n      // emulator 가 onConnect 기본(80×24)에 고정돼 serializeAnsi()(FRAME_SNAPSHOT)·GRID_SNAPSHOT\r\n      // 이 잘못된 geometry 로 직렬화돼 모바일 화면이 깨진다(review C1). lifecycle.onResize 는\r\n      // emulator 미존재 시 no-op 이라 cap=pty.data 경로에서도 안전.\r\n      this.lifecycle.onResize(machineId, safeCols, safeRows)\r\n    }\r\n\r\n    // spawn-deferring: 첫 resize 도착 시 spawn 코루틴을 깨운다. noop 여도 wake 해야\r\n    // 재연결/탭 복귀 시 cached 사이즈와 동일한 첫 resize 메시지에 코루틴이 막혀\r\n    // timeout 까지 가는 회귀 방지 (spawn-deferring 정합성).\r\n    const wake = this.pendingFirstResize.get(machineId)\r\n    if (wake) wake()\r\n\r\n    if (isNoop) {\r\n      // D-1 — 동일 사이즈 echo: setWindow / mgr.resize / broadcast 모두 skip.\r\n      // psmux refresh-client 무한 누적 차단 → freeze 증상 즉시 사라짐.\r\n      // 동일 사이즈 broadcast 도 불필요 (renderer/mobile 모두 이미 그 사이즈).\r\n      return\r\n    }\r\n\r\n    const localProc = this.localSessions.get(machineId)\r\n    if (localProc) {\r\n      localProc.resize(safeCols, safeRows)\r\n    } else {\r\n      const session = this.sessions.get(machineId)\r\n      session?.stream?.setWindow(safeRows, safeCols, 0, 0)\r\n    }\r\n\r\n    // SessionManager 측에도 sync — 멀티플렉서 제거 후 NoneSessionManager.resize 는 no-op\r\n    // (인터페이스 보존: 향후 multiplexer 재도입 시 alt-screen redraw 훅 자리).\r\n    const mgr = this.findCachedSessionManagerFor(machineId)\r\n    if (mgr) {\r\n      // fire-and-forget — resize 호출 자체가 throw 흡수 처리되므로 unhandled rejection 위험 0.\r\n      void mgr.resize(machineId, safeCols, safeRows, source).catch(() => {\r\n        /* noop */\r\n      })\r\n    }\r\n\r\n    // Phase 1 — 모든 viewer 동일 사이즈 sync. AuthorityResolver 가 source 필터로\r\n    // invariant 보장하므로 renderer self-echo 도 안전 (renderer 의 resizeTerminal 안\r\n    // noop guard 가 동일 사이즈 echo 차단). source-aware (skipRenderer) 분기 제거.\r\n    this.authority.recordAppliedSize(machineId, safeCols, safeRows)\r\n    this.broadcast({\r\n      type: 'terminal_resize_ack',\r\n      machineId,\r\n      cols: safeCols,\r\n      rows: safeRows,\r\n    })\r\n  }\r\n\r\n  /**\r\n   * 캐시된 SessionManager 중 본 머신이 사용 중인 것 반환. resize 시점은 connectLocal 의\r\n   * getSessionManager() 가 이미 cache 를 채운 상태라 여기서는 lookup 만.\r\n   *\r\n   * Note: 같은 setting 의 매니저를 여러 머신이 공유하므로, 머신별 정확한 매니저 lookup 은\r\n   * machine.multiplexer 를 모르면 불가능. 다만 `auto` 가 default 이므로 cache 의 첫 매니저로\r\n   * 안전 fallback — Phase 1 단계에서는 한 사용자 환경의 multiplexer 설정이 동일한 게 일반적.\r\n   */\r\n  private findCachedSessionManagerFor(\r\n    _machineId: string\r\n  ): SessionManager | undefined {\r\n    if (this.sessionManagers.size === 0) return undefined\r\n    return this.sessionManagers.values().next().value\r\n  }\r\n\r\n  /**\r\n   * machine 의 multiplexer setting 별 SessionManager 를 lazy 로 만들어 반환(setting 동일 시 공유).\r\n   * 멀티플렉서 제거(2026-06-23) 후 createSessionManager 는 항상 NoneSessionManager 를 반환한다.\r\n   */\r\n  private getSessionManager(machine: Machine): SessionManager {\r\n    const setting = resolveMultiplexerSetting(machine)\r\n    const mouse = machine.multiplexerMouse ?? false\r\n    const cacheKey = `${setting}:mouse=${mouse}` as MultiplexerSetting\r\n    const cached = this.sessionManagers.get(cacheKey)\r\n    if (cached) return cached\r\n    const mgr = createSessionManager(setting)\r\n    this.sessionManagers.set(cacheKey, mgr)\r\n    return mgr\r\n  }\r\n\r\n  /// 첫 spawn 직전 클라이언트의 첫 terminal_resize 메시지를 기다린다.\r\n  /// - ptySizes에 이미 값이 있으면 즉시 resolve (재연결 fast path).\r\n  /// - timeoutMs 안에 안 오면 resolve (호출부가 fallback 80x24 사용).\r\n  /// 동일 machineId의 중복 호출은 마지막 호출만 유효 (이전 resolver는 cleanup).\r\n  private waitForFirstResize(\r\n    machineId: string,\r\n    timeoutMs = 600\r\n  ): Promise<void> {\r\n    if (this.ptySizes.has(machineId)) return Promise.resolve()\r\n    const prev = this.pendingFirstResize.get(machineId)\r\n    if (prev) prev() // resolve any stale waiter\r\n    return new Promise<void>(resolve => {\r\n      let done = false\r\n      const finish = () => {\r\n        if (done) return\r\n        done = true\r\n        this.pendingFirstResize.delete(machineId)\r\n        clearTimeout(timer)\r\n        resolve()\r\n      }\r\n      const timer = setTimeout(finish, timeoutMs)\r\n      this.pendingFirstResize.set(machineId, finish)\r\n    })\r\n  }\r\n\r\n  runInitCommands(machineId: string): void {\r\n    const machine = this.machines.find(m => m.id === machineId)\r\n    if (!machine) return\r\n    if (machine.initCommands?.length) {\r\n      this.sendInput(\r\n        machineId,\r\n        machine.initCommands.map(command => command.trim()).join(' && ') + '\\r'\r\n      )\r\n    }\r\n  }\r\n\r\n  // ── [Cockpit] running-agent 탐지 + interactive PTY agent 종료 ────────────────\r\n\r\n  /**\r\n   * 머신의 interactive PTY foreground 에서 도는 agent 를 탐지해 [Machine.runningAgent]\r\n   * 를 갱신하고, 값이 바뀌었으면 `machine_list` 를 broadcast 한다.\r\n   *\r\n   * 로컬 PTY 만 탐지 가능 (SSH 원격은 pid 가 원격 호스트 것이라 OS 조회 불가) — local\r\n   * session 이 없으면 'none' 으로 두고 즉시 반환. `agent.pid` 자식 트리를 OS 로 조회하는\r\n   * [detectRunningAgent] 위임. 폴링(startStatusTimer 5s)과 cockpit 진입 직전에 호출된다.\r\n   */\r\n  /**\r\n   * 머신의 현재 탐지된 foreground agent 를 반환한다(없으면 'none').\r\n   *\r\n   * `detectAndBroadcastRunningAgent` 가 갱신한 `Machine.runningAgent` 의 동기 조회.\r\n   * 콕핏→터미널 핸드오프 주입 직전 **main 측 권위 가드**로 쓴다 — renderer 의 runningAgent 는\r\n   * 5s 폴링이라 stale 일 수 있어, 살아있는 claude 프롬프트에 `claude --resume` 이 박히는 것을\r\n   * main 이 최종 차단한다(호출 전 detect 를 await 해 최신화할 것).\r\n   */\r\n  getRunningAgent(machineId: string): RunningAgentKind {\r\n    return this.machines.find(m => m.id === machineId)?.runningAgent ?? 'none'\r\n  }\r\n\r\n  async detectAndBroadcastRunningAgent(machineId: string): Promise<void> {\r\n    const proc = this.localSessions.get(machineId)\r\n    const machine = this.machines.find(m => m.id === machineId)\r\n    if (!machine) return\r\n\r\n    let detected: RunningAgentKind = 'none'\r\n    if (proc && typeof proc.pid === 'number') {\r\n      detected = await detectRunningAgent(proc.pid)\r\n    }\r\n    if (machine.runningAgent !== detected) {\r\n      sshLog.info(\r\n        `[cockpit] runningAgent changed machine=${machineId} ${machine.runningAgent ?? 'none'} → ${detected} — broadcasting`\r\n      )\r\n      machine.runningAgent = detected\r\n      this.broadcastMachines()\r\n      // [Cockpit 신선도] claude 가 새로 떴으면(→claude 전환) 세션 목록을 자동 갱신한다 —\r\n      // 모바일이 재요청하지 않아도 방금 시작한 세션이 picker 에 나타난다. broadcastAgentSessions\r\n      // 가 flush 전이면 bounded 재스캔까지 예약하므로 타이밍 창도 덮는다.\r\n      if (detected === 'claude') {\r\n        this.broadcastAgentSessions(machineId)\r\n      }\r\n    }\r\n  }\r\n\r\n  /**\r\n   * 사용자 \"닫고 새로 열기\" — interactive PTY 의 foreground agent 에 SIGINT(Ctrl-C)\r\n   * 를 주입한다. PTY 자체는 유지(shell 복귀) — agent.run 으로 띄운 별도 프로세스가\r\n   * 아니라 사용자가 PTY 안에서 직접 띄운 claude TUI 를 종료하는 경로.\r\n   *\r\n   * 모바일은 stdin 을 직접 못 건드리므로(claude 가 점유) 이 종료도 데스크탑이 한다.\r\n   * Ctrl-C 2회를 약간의 간격으로 보내 claude 의 \"한 번 더 누르면 종료\" 프롬프트까지 통과.\r\n   */\r\n  killPtyAgent(machineId: string): void {\r\n    sshLog.info(`[cockpit] killPtyAgent machine=${machineId}`)\r\n    // \\x03 = Ctrl-C (ETX). claude TUI 는 첫 Ctrl-C 에 확인 프롬프트, 둘째에 종료.\r\n    this.sendInput(machineId, '\\x03')\r\n    setTimeout(() => this.sendInput(machineId, '\\x03'), 200)\r\n  }\r\n\r\n  /**\r\n   * [Cockpit] 머신 cwd 의 claude 세션 목록을 스캔해 `agent.sessions` 로 broadcast.\r\n   *\r\n   * cwd 는 [resolveLocalCwd] (OSC7 추적 → localCwd → process.cwd → home) 로 결정.\r\n   * 로컬 머신 기준 — SSH 원격은 cwd 가 원격 경로라 데스크탑 디스크의 ~/.claude 와\r\n   * 무관(빈 목록). [listAgentSessions] 가 mtime 내림차순 정렬 + 미리보기 추출.\r\n   */\r\n  broadcastAgentSessions(machineId: string): void {\r\n    // 새 요청이 최신 기준 — 직전 머신의 빈-세션 재스캔 스케줄을 먼저 취소(중복 방지).\r\n    this.cancelAgentSessionsRescan(machineId)\r\n\r\n    const cwd = this.resolveLocalCwd(machineId)\r\n    const disk = listAgentSessions(cwd)\r\n    // [Cockpit 신선도 20260614] 방금 콕핏으로 연 장수명 세션은 `.jsonl` flush 전이라 디스크\r\n    // 스캔에서 빠진다. 데스크탑이 이미 아는 running id(AgentRunner)를 합류시켜 picker 가 \"현재\r\n    // 세션\"을 즉시 보이게 한다. provider 미주입이면 disk 그대로(graceful degrade).\r\n    const runningIds = this.activeSessionProvider?.(machineId) ?? []\r\n    const sessions = mergeRunningSessions(disk, runningIds, Date.now())\r\n    sshLog.info(\r\n      `[cockpit] agent.sessions machine=${machineId} cwd=${cwd} disk=${disk.length} running=${runningIds.length} count=${sessions.length}`\r\n    )\r\n    this.broadcast({ type: 'agent.sessions', machineId, cwd, sessions })\r\n\r\n    // [Cockpit 신선도] claude 가 방금 떠 `.jsonl` flush 전이면 목록이 0건일 수 있다. running\r\n    // 중인데 비었으면 bounded 재스캔을 예약 — flush 직후 세션이 surface 되면 자동 재방출하고\r\n    // 멈춘다. 순수 0-바이트(첫 턴 전) 영구 케이스는 디스크에 파일이 없어 범위 밖(설계 문서 참조).\r\n    const runningAgent = this.machines.find(m => m.id === machineId)?.runningAgent\r\n    if (sessions.length === 0 && runningAgent === 'claude') {\r\n      const cancel = scheduleSessionRescan({\r\n        delaysMs: AGENT_SESSIONS_RESCAN_DELAYS_MS,\r\n        rescan: () => listAgentSessions(this.resolveLocalCwd(machineId)),\r\n        onSurfaced: surfaced => {\r\n          const surfacedCwd = this.resolveLocalCwd(machineId)\r\n          sshLog.info(\r\n            `[cockpit] agent.sessions rescan machine=${machineId} count=${surfaced.length} — surfaced, broadcasting`\r\n          )\r\n          this.broadcast({\r\n            type: 'agent.sessions',\r\n            machineId,\r\n            cwd: surfacedCwd,\r\n            sessions: surfaced,\r\n          })\r\n          this.agentSessionsRescanCancel.delete(machineId)\r\n        },\r\n      })\r\n      this.agentSessionsRescanCancel.set(machineId, cancel)\r\n    }\r\n  }\r\n\r\n  /** [Cockpit 신선도] 머신의 빈-세션 재스캔 스케줄을 취소·해제한다(없으면 no-op). */\r\n  private cancelAgentSessionsRescan(machineId: string): void {\r\n    const cancel = this.agentSessionsRescanCancel.get(machineId)\r\n    if (cancel) {\r\n      cancel()\r\n      this.agentSessionsRescanCancel.delete(machineId)\r\n    }\r\n  }\r\n\r\n  /**\r\n   * [Cockpit] 한 세션의 이전 대화(transcript)를 읽어 `agent.session.transcript` 로 broadcast.\r\n   *\r\n   * cwd 는 [broadcastAgentSessions] 와 동일하게 [resolveLocalCwd] 로 결정 — 세션 목록을\r\n   * 뽑은 그 디렉토리와 일치해야 파일을 찾는다. [readSessionTranscript] 가 user/assistant\r\n   * 텍스트만 최근분(상한)으로 추린다.\r\n   */\r\n  broadcastSessionTranscript(machineId: string, sessionId: string): void {\r\n    const cwd = this.resolveLocalCwd(machineId)\r\n    const messages = readSessionTranscript(cwd, sessionId)\r\n    sshLog.info(\r\n      `[cockpit] agent.session.transcript machine=${machineId} session=${sessionId} count=${messages.length}`\r\n    )\r\n    this.broadcast({ type: 'agent.session.transcript', machineId, sessionId, messages })\r\n  }\r\n\r\n  cleanup(): void {\r\n    this._destroyed = true\r\n\r\n    for (const [id] of this.statusTimers) {\r\n      this.clearStatusTimer(id)\r\n    }\r\n\r\n    // [Cockpit 신선도] 진행 중인 빈-세션 재스캔 타이머 전량 해제.\r\n    for (const [, cancel] of this.agentSessionsRescanCancel) {\r\n      cancel()\r\n    }\r\n    this.agentSessionsRescanCancel.clear()\r\n\r\n    for (const [id] of this.localSessions) {\r\n      this.pendingDisconnects.add(id)\r\n    }\r\n\r\n    // spawn-deferring 대기 중인 코루틴이 있으면 즉시 wake — _destroyed 체크 없이도\r\n    // post-await에서 pendingDisconnects로 abort. 메모리 누수 방지.\r\n    for (const [, wake] of this.pendingFirstResize) {\r\n      wake()\r\n    }\r\n    this.pendingFirstResize.clear()\r\n\r\n    for (const [, proc] of this.localSessions) {\r\n      proc.kill()\r\n    }\r\n    this.localSessions.clear()\r\n\r\n    for (const [id] of this.sessions) {\r\n      this.disconnect(id)\r\n    }\r\n\r\n    // v1.5.0 — buffers 디스크 flush. before-quit (sync handler) 안에서 안전.\r\n    // flushSync 는 빈 buffer 면 unlink, 아니면 JSON write. 실패는 swallow.\r\n    try {\r\n      this.persister.flushAllSync(this.scrollbacks)\r\n    } catch {\r\n      // flush 실패 — 종료 흐름 막지 않음. 다음 launch 에서 이전 상태 restore.\r\n    }\r\n    this.scrollbacks.clear()\r\n    this.restartingMachines.clear()\r\n    // PtyLifecycleManager 의 idle timer + Map 모두 해제.\r\n    this.lifecycle.dispose()\r\n    // Phase 1 — AuthorityResolver 의 grace/handover timer 취소 + state Map clear.\r\n    this.authority.destroy()\r\n\r\n    // SessionManager lifecycle 종료 hook — NoneSessionManager 는 dispose 미구현이라 skip.\r\n    // Promise 는 fire-and-forget — cleanup 흐름을 막지 않는다.\r\n    for (const mgr of this.sessionManagers.values()) {\r\n      void mgr.dispose?.().catch(() => undefined)\r\n    }\r\n    this.sessionManagers.clear()\r\n  }\r\n\r\n  private parseOsc7(data: string, machineId: string): void {\r\n    const match = data.match(/\\x1b\\]7;file:\\/\\/[^/]*(\\/.+?)(?:\\x07|\\x1b\\\\)/)\r\n    if (!match) return\r\n    const next = decodeURIComponent(match[1])\r\n    const prev = this.cwdMap.get(machineId)\r\n    this.cwdMap.set(machineId, next)\r\n    // [Git 첫 진입 지연 fix 20260705] cwd 가 실제로 바뀌면 git status 를 즉시 재조회한다.\r\n    // 첫 진입 시 cwd 는 process.cwd() fallback(headless 데몬 실행 위치 = 대개 non-repo)이라\r\n    // git 이 \"not connected\" 로 뜨는데, 셸이 OSC7 로 실제 작업 디렉터리를 보고하는 순간\r\n    // 즉시 반영(다음 60초 폴링 대기 제거). 값이 불변이면(매 프롬프트 OSC7) skip → 조회 폭주 방지.\r\n    if (next !== prev) this.refreshGitStatus(machineId)\r\n  }\r\n\r\n  /**\r\n   * git status 폴링을 machine 별 `pLimit(1)` 로 **직렬화**해 1회 조회한다.\r\n   *\r\n   * 타이머(첫 발사+주기)·OSC7 cwd 변경·모바일 attach 가 공유하는 단일 진입점 —\r\n   * 각 호출부가 limiter 를 직접 다루면 동시 git status 가 중복 spawn 되거나(attach 처럼\r\n   * 우회 시) 결과가 뒤섞일 수 있어, 직렬화를 여기 한 곳에 모은다. limiter 는 lazy 생성하고\r\n   * {@link clearStatusTimer} 가 정리한다. mutation 액션(checkout/pull/push 등)은 본 경로가\r\n   * 아니라 `executeGitAction` 직접 호출이라 재귀 self-refresh 로 인한 self-deadlock 이 없다.\r\n   */\r\n  refreshGitStatus(machineId: string): void {\r\n    let limit = this.gitLimiters.get(machineId)\r\n    if (!limit) {\r\n      limit = pLimit(1)\r\n      this.gitLimiters.set(machineId, limit)\r\n    }\r\n    limit(() => this.executeGitAction(machineId, { kind: 'status' })).catch(\r\n      () => {}\r\n    )\r\n  }\r\n\r\n  private startStatusTimer(machineId: string): void {\r\n    this.clearStatusTimer(machineId)\r\n    // [Git 첫 진입 지연 fix 20260705] setInterval 은 첫 콜백이 5초 뒤에야 실행된다.\r\n    // 첫 git status 를 즉시 1회 발사해 초기 로드 지연(최소 5초)을 없앤다. 이후는 아래\r\n    // 타이머가 60초 주기로 이어받는다(tick=0 시작이므로 다음 git 은 ~60초 후).\r\n    this.refreshGitStatus(machineId)\r\n    // 타이머 자체는 5초 tick (cockpit 탐지의 배너 반응성 유지). git status 는 12 tick\r\n    // 마다(=60초) 만 호출해 [git_result] 로그/네트워크를 줄인다. 위에서 즉시 1회\r\n    // 발사했으므로 count 를 0 으로 시작해 다음 git 은 ~60초 후에 발사한다.\r\n    let tick = 0\r\n    const timer = setInterval(() => {\r\n      if (++tick > GIT_STATUS_EVERY_N_TICKS) {\r\n        tick = 1\r\n        this.refreshGitStatus(machineId)\r\n      }\r\n      // [Cockpit] running-agent 탐지 — 매 tick(5초). 변경 시에만 broadcast(내부 dedup).\r\n      void this.detectAndBroadcastRunningAgent(machineId).catch(() => {})\r\n    }, 5000)\r\n    this.statusTimers.set(machineId, timer)\r\n  }\r\n\r\n  private clearStatusTimer(machineId: string): void {\r\n    const timer = this.statusTimers.get(machineId)\r\n    if (timer) {\r\n      clearInterval(timer)\r\n      this.statusTimers.delete(machineId)\r\n    }\r\n    this.gitLimiters.delete(machineId)\r\n  }\r\n\r\n  /**\r\n   * 로컬 머신의 git 작업 디렉터리 결정 (폴백 체인).\r\n   * 1) OSC7로 추적된 cwd\r\n   * 2) 머신 등록 시 지정한 localCwd\r\n   * 3) electron 자체 process.cwd()\r\n   * 4) homeDir\r\n   *\r\n   * public: [Cockpit v2 장수명] ws-manager 의 agent.session.open 핸들러가 콕핏 claude 의 spawn\r\n   * cwd 로 쓴다 — 터미널과 같은 cwd 라 같은 `~/.claude` 세션 풀을 공유(Phase C 토대).\r\n   */\r\n  resolveLocalCwd(machineId: string): string {\r\n    const tracked = this.cwdMap.get(machineId)\r\n    if (tracked) return tracked\r\n    const machine = this.machines.find(m => m.id === machineId)\r\n    if (machine?.localCwd) return machine.localCwd\r\n    try {\r\n      const procCwd = process.cwd()\r\n      if (procCwd) return procCwd\r\n    } catch {\r\n      /* ignore */\r\n    }\r\n    return homeDir\r\n  }\r\n\r\n  /**\r\n   * [Push-Notif Phase 1] Claude 훅이 준 cwd 를 로컬 머신 id 로 역매핑한다.\r\n   *\r\n   * Claude 훅은 데스크탑 PTY 와 무관한 것까지 머신의 **모든** claude 세션에 발화하므로\r\n   * (Phase 0 실측: 동시 4개 세션), 데스크탑이 호스팅하는 로컬 머신의 작업 디렉토리와\r\n   * 일치하는 이벤트만 골라야 한다 — 나머지는 무관 세션으로 drop. cwd 표기가 소스마다\r\n   * 다르므로([resolveLocalCwd] 의 OSC7/localCwd/process.cwd) [normalizeCwd] 로 통일 비교.\r\n   * SSH 원격 머신은 cwd 가 원격 경로라 제외. 일치 없으면 null.\r\n   *\r\n   * @param cwd 훅 stdin 의 `cwd` (절대 경로).\r\n   * @returns 일치하는 로컬 머신 id, 없으면 null.\r\n   */\r\n  findLocalMachineIdByCwd(cwd: string): string | null {\r\n    const target = normalizeCwd(cwd)\r\n    for (const machine of this.machines) {\r\n      if (machine.machineType === 'ssh') continue\r\n      if (normalizeCwd(this.resolveLocalCwd(machine.id)) === target) {\r\n        return machine.id\r\n      }\r\n    }\r\n    return null\r\n  }\r\n\r\n  /** 후보 디렉터리에서 실제 git 저장소 루트로 정규화. 저장소가 아니면 null. */\r\n  private async normalizeToGitRoot(candidate: string): Promise<string | null> {\r\n    try {\r\n      const git = simpleGit({ baseDir: candidate })\r\n      const top = (await git.revparse(['--show-toplevel'])).trim()\r\n      return top || null\r\n    } catch {\r\n      return null\r\n    }\r\n  }\r\n\r\n  async executeGitAction(machineId: string, action: GitAction): Promise<void> {\r\n    const machine = this.machines.find(m => m.id === machineId)\r\n    if (!machine) return\r\n\r\n    if (machine.machineType === 'local') {\r\n      const candidate = this.resolveLocalCwd(machineId)\r\n      const gitRoot = await this.normalizeToGitRoot(candidate)\r\n      const cwd = gitRoot ?? candidate\r\n      return this.executeGitActionLocal(\r\n        machineId,\r\n        action,\r\n        cwd,\r\n        gitRoot !== null\r\n      )\r\n    }\r\n\r\n    // SSH 경로 — 원격 cwd는 OSC7로만 추적, 없으면 home\r\n    const cwd = this.cwdMap.get(machineId) ?? homeDir\r\n\r\n    try {\r\n      // validateGitRef/buildGitCommand 를 try 안에 둬, invalid ref 나 미지원 kind 가\r\n      // 조용한 unhandled rejection 대신 git_result{success:false} 로 표면화되게 한다.\r\n      // (검증은 shell 명령을 조립하는 SSH 경로 전용 — relay 경로는 simple-git argv 라\r\n      //  shell injection 이 불가능하고, Unicode/CJK 브랜치명을 막으면 안 된다.)\r\n      if (action.kind === 'checkout') validateGitRef(action.branch, 'branch')\r\n      if (action.kind === 'create_branch') validateGitRef(action.name, 'name')\r\n      if (action.kind === 'delete_branch') {\r\n        validateGitRef(action.branch, 'branch')\r\n        // F-E-4 권한 가드 — 기본 브랜치(origin/HEAD)는 force 와 무관하게 거부.\r\n        // git branch -D <default> 는 체크아웃 안 돼 있으면 그냥 지워지므로 (active 와\r\n        // 달리 git 이 안 막음) 서버측 방어선이 필요하다. 모바일 caller-side 가드의\r\n        // 우회/구버전 케이스 대비 defense-in-depth.\r\n        const def = await this.resolveDefaultBranchSsh(machineId, cwd)\r\n        if (def && def === action.branch) {\r\n          gitLog.warn({\r\n            machineId,\r\n            action: 'delete_branch',\r\n            cwd,\r\n            branch: action.branch,\r\n            blocked: 'default_branch',\r\n          })\r\n          this.broadcast({\r\n            type: 'git_result',\r\n            machineId,\r\n            action: action.kind,\r\n            success: false,\r\n            error: `Refusing to delete the default branch '${action.branch}'`,\r\n            errorCode: 'generic',\r\n          })\r\n          return\r\n        }\r\n      }\r\n      const cmd = buildGitCommand(action, cwd)\r\n      const output = await this.execSsh(machineId, cmd)\r\n      if (action.kind === 'status') {\r\n        const data = this.parseGitStatus(output)\r\n        data.cwd = cwd\r\n        gitLog.info({\r\n          machineId,\r\n          action: 'status',\r\n          cwd,\r\n          isGitRepo: data.isGitRepo,\r\n          files: data.files.length,\r\n        })\r\n        this.broadcast({\r\n          type: 'git_result',\r\n          machineId,\r\n          action: action.kind,\r\n          success: true,\r\n          data,\r\n        })\r\n      } else if (action.kind === 'log') {\r\n        const commits = this.parseGitLog(output)\r\n        const data: GitStatusData = {\r\n          branch: '',\r\n          ahead: 0,\r\n          behind: 0,\r\n          branches: [],\r\n          files: [],\r\n          isGitRepo: true,\r\n          changedFiles: 0,\r\n          commits,\r\n          cwd,\r\n        }\r\n        gitLog.info({\r\n          machineId,\r\n          action: 'log',\r\n          cwd,\r\n          commits: commits.length,\r\n        })\r\n        this.broadcast({\r\n          type: 'git_result',\r\n          machineId,\r\n          action: action.kind,\r\n          success: true,\r\n          data,\r\n        })\r\n      } else if (action.kind === 'delete_branch') {\r\n        // execSsh 는 exit code 를 보지 않고 stdout+stderr 를 합쳐 resolve 하므로,\r\n        // git 삭제 실패는 throw 가 아니라 \"error:\"/\"fatal:\" 텍스트로 온다.\r\n        // 텍스트로 성공/실패를 판정해 SSH 경로의 false-success 를 차단한다.\r\n        if (/^(?:error|fatal):/m.test(output)) {\r\n          const msg = output.trim()\r\n          gitLog.info({ machineId, action: 'delete_branch', cwd, error: msg })\r\n          this.broadcast({\r\n            type: 'git_result',\r\n            machineId,\r\n            action: action.kind,\r\n            success: false,\r\n            error: msg,\r\n            errorCode: classifyDeleteError(msg),\r\n          })\r\n        } else {\r\n          gitLog.warn({\r\n            machineId,\r\n            action: 'delete_branch',\r\n            cwd,\r\n            branch: action.branch,\r\n            force: action.force,\r\n            success: true,\r\n          })\r\n          this.broadcast({\r\n            type: 'git_result',\r\n            machineId,\r\n            action: action.kind,\r\n            success: true,\r\n          })\r\n          setTimeout(\r\n            () =>\r\n              this.executeGitAction(machineId, { kind: 'status' }).catch(\r\n                () => {}\r\n              ),\r\n            500\r\n          )\r\n        }\r\n      } else {\r\n        gitLog.info({\r\n          machineId,\r\n          action: action.kind,\r\n          cwd,\r\n          success: true,\r\n        })\r\n        this.broadcast({\r\n          type: 'git_result',\r\n          machineId,\r\n          action: action.kind,\r\n          success: true,\r\n        })\r\n        setTimeout(\r\n          () =>\r\n            this.executeGitAction(machineId, { kind: 'status' }).catch(\r\n              () => {}\r\n            ),\r\n          500\r\n        )\r\n      }\r\n    } catch (err) {\r\n      const message = (err as Error).message\r\n      gitLog.info({\r\n        machineId,\r\n        action: action.kind,\r\n        cwd,\r\n        error: message,\r\n      })\r\n      this.broadcast({\r\n        type: 'git_result',\r\n        machineId,\r\n        action: action.kind,\r\n        success: false,\r\n        error: message,\r\n        errorCode:\r\n          action.kind === 'delete_branch'\r\n            ? classifyDeleteError(message)\r\n            : undefined,\r\n      })\r\n    }\r\n  }\r\n\r\n  private async executeGitActionLocal(\r\n    machineId: string,\r\n    action: GitAction,\r\n    cwd: string,\r\n    isGitRepo: boolean\r\n  ): Promise<void> {\r\n    const git = simpleGit({ baseDir: cwd })\r\n\r\n    // git 저장소가 아니면 status/log는 빈 응답으로 broadcast (모바일이 cwd 표시 가능)\r\n    if (!isGitRepo) {\r\n      if (action.kind === 'status' || action.kind === 'log') {\r\n        const data: GitStatusData = {\r\n          branch: '',\r\n          ahead: 0,\r\n          behind: 0,\r\n          branches: [],\r\n          files: [],\r\n          isGitRepo: false,\r\n          changedFiles: 0,\r\n          commits: [],\r\n          cwd,\r\n        }\r\n        gitLog.info({\r\n          machineId,\r\n          action: action.kind,\r\n          cwd,\r\n          isGitRepo: false,\r\n        })\r\n        this.broadcast({\r\n          type: 'git_result',\r\n          machineId,\r\n          action: action.kind,\r\n          success: true,\r\n          data,\r\n        })\r\n        return\r\n      }\r\n      // 변경 액션은 git repo 아니면 실패\r\n      gitLog.info({\r\n        machineId,\r\n        action: action.kind,\r\n        cwd,\r\n        error: 'not a git repository',\r\n      })\r\n      this.broadcast({\r\n        type: 'git_result',\r\n        machineId,\r\n        action: action.kind,\r\n        success: false,\r\n        error: `Not a git repository: ${cwd}`,\r\n      })\r\n      return\r\n    }\r\n\r\n    try {\r\n      if (action.kind === 'status') {\r\n        const status: StatusResult = await git.status()\r\n        const branchSummary: BranchSummary = await git.branch(['-vva'])\r\n\r\n        const files: GitFileData[] = status.files.map(f => ({\r\n          path: f.path,\r\n          state: mapSimpleGitState(f.index, f.working_dir),\r\n          isStaged: f.index !== ' ' && f.index !== '?',\r\n        }))\r\n\r\n        const branches: GitBranchData[] = Object.values(\r\n          branchSummary.branches\r\n        ).map(b => ({\r\n          name: b.name.replace('remotes/', ''),\r\n          isRemote: b.name.startsWith('remotes/'),\r\n          isActive: b.current,\r\n          lastCommitHash: b.commit || undefined,\r\n          ahead: b.current ? status.ahead : 0,\r\n          behind: b.current ? status.behind : 0,\r\n        }))\r\n\r\n        const data: GitStatusData = {\r\n          branch: status.current ?? '',\r\n          ahead: status.ahead,\r\n          behind: status.behind,\r\n          branches,\r\n          files,\r\n          isGitRepo: true,\r\n          changedFiles: files.length,\r\n          cwd,\r\n        }\r\n\r\n        gitLog.info({\r\n          machineId,\r\n          action: 'status',\r\n          cwd,\r\n          isGitRepo: true,\r\n          branch: data.branch,\r\n          files: files.length,\r\n        })\r\n        this.broadcast({\r\n          type: 'git_result',\r\n          machineId,\r\n          action: 'status',\r\n          success: true,\r\n          data,\r\n        })\r\n      } else if (action.kind === 'log') {\r\n        const limit = Math.max(1, Math.min(action.limit | 0, 200))\r\n        const fmt = '%H%x09%h%x09%an%x09%ar%x09%D%x09%s'\r\n        const output = await git.raw([\r\n          'log',\r\n          `--pretty=format:${fmt}`,\r\n          '-n',\r\n          String(limit),\r\n        ])\r\n        const commits = this.parseGitLog(output)\r\n        const data: GitStatusData = {\r\n          branch: '',\r\n          ahead: 0,\r\n          behind: 0,\r\n          branches: [],\r\n          files: [],\r\n          isGitRepo: true,\r\n          changedFiles: 0,\r\n          commits,\r\n          cwd,\r\n        }\r\n        gitLog.info({\r\n          machineId,\r\n          action: 'log',\r\n          cwd,\r\n          commits: commits.length,\r\n        })\r\n        this.broadcast({\r\n          type: 'git_result',\r\n          machineId,\r\n          action: 'log',\r\n          success: true,\r\n          data,\r\n        })\r\n      } else {\r\n        switch (action.kind) {\r\n          case 'checkout':\r\n            await git.checkout(action.branch)\r\n            break\r\n          case 'create_branch':\r\n            if (action.checkout) {\r\n              await git.checkoutLocalBranch(action.name)\r\n            } else {\r\n              await git.branch([action.name])\r\n            }\r\n            break\r\n          case 'delete_branch': {\r\n            // F-E-4 권한 가드 — 기본 브랜치(origin/HEAD)는 force 와 무관하게 거부.\r\n            // 모바일 caller-side 차단의 우회/구버전 대비 server 측 defense-in-depth.\r\n            const def = await this.resolveDefaultBranchLocal(git)\r\n            if (def && def === action.branch) {\r\n              gitLog.warn({\r\n                machineId,\r\n                action: 'delete_branch',\r\n                cwd,\r\n                branch: action.branch,\r\n                blocked: 'default_branch',\r\n              })\r\n              throw new Error(\r\n                `Refusing to delete the default branch '${action.branch}'`\r\n              )\r\n            }\r\n            // safe(-d) 는 미머지면 simple-git 이 throw → catch 에서 errorCode 분류 →\r\n            // 모바일이 force CTA 노출. force(-D) 는 미머지도 삭제 (커밋 유실 가능).\r\n            if (action.force) {\r\n              await git.raw(['branch', '-D', action.branch])\r\n              gitLog.warn({\r\n                machineId,\r\n                action: 'delete_branch',\r\n                cwd,\r\n                branch: action.branch,\r\n                force: true,\r\n              })\r\n            } else {\r\n              await git.deleteLocalBranch(action.branch)\r\n            }\r\n            break\r\n          }\r\n          case 'pull':\r\n            await git.pull()\r\n            break\r\n          case 'push':\r\n            await git.push()\r\n            break\r\n          default: {\r\n            // exhaustiveness 가드 — 미지원 kind 가 조용히 success:true 로\r\n            // broadcast 되던 것을 차단 (autoplan F-CEO-2). 새 GitAction kind 는\r\n            // 컴파일 단계(never)에서 case 추가를 강제받는다.\r\n            const _exhaustive: never = action\r\n            throw new Error(\r\n              `Unsupported git action: ${(_exhaustive as GitAction).kind}`\r\n            )\r\n          }\r\n        }\r\n        gitLog.info({\r\n          machineId,\r\n          action: action.kind,\r\n          cwd,\r\n          success: true,\r\n        })\r\n        this.broadcast({\r\n          type: 'git_result',\r\n          machineId,\r\n          action: action.kind,\r\n          success: true,\r\n        })\r\n        setTimeout(\r\n          () =>\r\n            this.executeGitAction(machineId, { kind: 'status' }).catch(\r\n              () => {}\r\n            ),\r\n          500\r\n        )\r\n      }\r\n    } catch (err) {\r\n      const message = (err as Error).message\r\n      gitLog.info({\r\n        machineId,\r\n        action: action.kind,\r\n        cwd,\r\n        error: message,\r\n      })\r\n      this.broadcast({\r\n        type: 'git_result',\r\n        machineId,\r\n        action: action.kind,\r\n        success: false,\r\n        error: message,\r\n        // 삭제 실패만 errorCode 분류 (모바일 l10n 매핑 + force CTA 판단). 그 외\r\n        // 액션은 기존대로 raw error 만 전달 (스키마 optional).\r\n        errorCode:\r\n          action.kind === 'delete_branch'\r\n            ? classifyDeleteError(message)\r\n            : undefined,\r\n      })\r\n    }\r\n  }\r\n\r\n  /**\r\n   * 기본 브랜치명(origin/HEAD 의 short ref)을 resolve. 못 찾으면 null.\r\n   *\r\n   * delete_branch 권한 가드용(F-E-4) — null 이면 가드 미적용(git 의 active-branch\r\n   * 보호 + 모바일 caller-side 차단에 의존). simple-git(relay) 경로.\r\n   */\r\n  private async resolveDefaultBranchLocal(\r\n    git: ReturnType<typeof simpleGit>\r\n  ): Promise<string | null> {\r\n    try {\r\n      const ref = await git.raw([\r\n        'symbolic-ref',\r\n        '--quiet',\r\n        '--short',\r\n        'refs/remotes/origin/HEAD',\r\n      ])\r\n      return ref.trim().replace(/^origin\\//, '') || null\r\n    } catch {\r\n      return null\r\n    }\r\n  }\r\n\r\n  /**\r\n   * SSH(shell) 경로용 기본 브랜치 resolver. git 명령이라 cmd/bash 양쪽 동작.\r\n   * 실패(origin/HEAD 미설정 등)면 null → 가드 미적용.\r\n   */\r\n  private async resolveDefaultBranchSsh(\r\n    machineId: string,\r\n    cwd: string\r\n  ): Promise<string | null> {\r\n    try {\r\n      const out = await this.execSsh(\r\n        machineId,\r\n        `cd ${JSON.stringify(cwd)} && git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null`\r\n      )\r\n      return out.trim().replace(/^origin\\//, '') || null\r\n    } catch {\r\n      return null\r\n    }\r\n  }\r\n\r\n  private parseGitLog(output: string): GitCommitData[] {\r\n    const commits: GitCommitData[] = []\r\n    let dropped = 0\r\n    for (const line of output.split('\\n')) {\r\n      if (!line) continue\r\n      const parts = line.split('\\t')\r\n      if (parts.length < 6) {\r\n        dropped += 1\r\n        continue\r\n      }\r\n      const [hash, shortHash, author, dateRelative, refsRaw, ...subjectParts] =\r\n        parts\r\n      const subject = subjectParts.join('\\t')\r\n      const refs = refsRaw\r\n        ? refsRaw\r\n            .split(',')\r\n            .map(s => s.trim())\r\n            .filter(Boolean)\r\n        : []\r\n      commits.push({\r\n        hash: hash ?? '',\r\n        shortHash: shortHash || (hash ? hash.slice(0, 7) : ''),\r\n        subject: subject ?? '',\r\n        author: author ?? '',\r\n        dateRelative: dateRelative ?? '',\r\n        refs,\r\n      })\r\n    }\r\n    if (dropped > 0) {\r\n      gitLog.warn('parseGitLog dropped malformed lines', {\r\n        dropped,\r\n        parsed: commits.length,\r\n      })\r\n    }\r\n    return commits\r\n  }\r\n\r\n  private parseGitStatus(output: string): GitStatusData {\r\n    const sections: Record<string, string> = {}\r\n    let current = ''\r\n    for (const line of output.split('\\n')) {\r\n      if (line.startsWith('___') && line.endsWith('___')) {\r\n        current = line.slice(3, -3)\r\n        sections[current] = ''\r\n      } else if (current) {\r\n        sections[current] =\r\n          (sections[current] ? sections[current] + '\\n' : '') + line\r\n      }\r\n    }\r\n\r\n    const isGitRepo = (sections['GIT_REPO'] ?? '').trim() === 'true'\r\n    if (!isGitRepo) {\r\n      return {\r\n        branch: '',\r\n        ahead: 0,\r\n        behind: 0,\r\n        branches: [],\r\n        files: [],\r\n        isGitRepo: false,\r\n        changedFiles: 0,\r\n      }\r\n    }\r\n\r\n    const branch = (sections['BRANCH'] ?? '').trim()\r\n\r\n    const files: GitFileData[] = []\r\n    for (const line of (sections['STATUS'] ?? '').split('\\n')) {\r\n      if (line.length < 3) continue\r\n      const xy = line.slice(0, 2)\r\n      const filePath = line.slice(3).trim()\r\n      const isStaged = xy[0] !== ' ' && xy[0] !== '?'\r\n      let state: GitFileData['state'] = 'modified'\r\n      const char = isStaged ? xy[0] : xy[1]\r\n      if (char === 'A') state = 'added'\r\n      else if (char === 'D') state = 'deleted'\r\n      else if (char === '?') state = 'untracked'\r\n      else if (char === 'R') state = 'renamed'\r\n      else if (char === 'U') state = 'conflicted'\r\n      files.push({ path: filePath, state, isStaged })\r\n    }\r\n\r\n    const branches: GitBranchData[] = []\r\n    for (const line of (sections['BRANCHES'] ?? '').split('\\n')) {\r\n      if (!line.trim()) continue\r\n      const isActive = line.startsWith('*')\r\n      const isRemote = line.includes('remotes/')\r\n      const parts = line.replace(/^\\*?\\s+/, '').split(/\\s+/)\r\n      const name = parts[0]?.replace('remotes/', '') ?? ''\r\n      const lastCommitHash =\r\n        parts[1] && !parts[1].startsWith('->') ? parts[1] : undefined\r\n      if (name) branches.push({ name, isRemote, isActive, lastCommitHash })\r\n    }\r\n\r\n    let ahead = 0\r\n    let behind = 0\r\n    const abLine = (sections['AHEAD_BEHIND'] ?? '').trim()\r\n    if (abLine) {\r\n      const nums = abLine.split(/\\s+/)\r\n      ahead = parseInt(nums[0] ?? '0', 10) || 0\r\n      behind = parseInt(nums[1] ?? '0', 10) || 0\r\n    }\r\n\r\n    return {\r\n      branch,\r\n      ahead,\r\n      behind,\r\n      branches,\r\n      files,\r\n      isGitRepo: true,\r\n      changedFiles: files.length,\r\n    }\r\n  }\r\n\r\n  private execSsh(machineId: string, cmd: string): Promise<string> {\r\n    return new Promise((resolve, reject) => {\r\n      const session = this.sessions.get(machineId)\r\n      if (!session) {\r\n        reject(new Error('no ssh session'))\r\n        return\r\n      }\r\n      session.client.exec(cmd, (err, stream) => {\r\n        if (err) {\r\n          reject(err)\r\n          return\r\n        }\r\n        let out = ''\r\n        stream.on('data', (d: Buffer) => {\r\n          out += d.toString()\r\n        })\r\n        stream.stderr.on('data', (d: Buffer) => {\r\n          out += d.toString()\r\n        })\r\n        stream.on('close', () => resolve(out))\r\n      })\r\n    })\r\n  }\r\n\r\n  private updateStatus(\r\n    machineId: string,\r\n    status: ConnectionState,\r\n    error?: string\r\n  ): void {\r\n    if (this._destroyed) return\r\n    const machine = this.machines.find(m => m.id === machineId)\r\n    if (machine) {\r\n      machine.status = status\r\n      if (error) {\r\n        machine.errorMessage = error\r\n      } else if (status === 'connected') {\r\n        machine.errorMessage = undefined\r\n        machine.lastConnected = new Date()\r\n      }\r\n      this.broadcast({ type: 'connection_status', machineId, status, error })\r\n    }\r\n  }\r\n}\r\n","import pty from 'node-pty'\r\n\r\nimport type { PtyProcess, PtyCreateOptions } from '@arva/shared/types'\r\n\r\nexport function createNodePty(opts: PtyCreateOptions): PtyProcess {\r\n  const shell =\r\n    process.platform === 'win32'\r\n      ? (opts.shell || process.env.COMSPEC || 'cmd.exe')\r\n      : (opts.shell || process.env.SHELL || '/bin/bash')\r\n  \r\n  const proc = pty.spawn(shell, opts.args ?? [], {\r\n    name: 'xterm-256color',\r\n    cols: opts.cols,\r\n    rows: opts.rows,\r\n    cwd: opts.cwd,\r\n    env: opts.env as Record<string, string>,\r\n  })\r\n\r\n  let dataCallback: ((d: string) => void) | null = null\r\n  let exitCallback: ((code: number) => void) | null = null\r\n\r\n  proc.onData((data) => dataCallback?.(data))\r\n  proc.onExit(({ exitCode }) => exitCallback?.(exitCode))\r\n\r\n  return {\r\n    get pid() {\r\n      return proc.pid\r\n    },\r\n    write: (data) => proc.write(data),\r\n    resize: (cols, rows) => proc.resize(cols, rows),\r\n    kill: () => proc.kill(),\r\n    onData: (cb) => {\r\n      dataCallback = cb\r\n    },\r\n    onExit: (cb) => {\r\n      exitCallback = cb\r\n    },\r\n  }\r\n}\r\n","// PTY scrollback ring buffer.\r\n//\r\n// 본 PR (PTY 영속화) 의 핵심 구성요소. 모든 PTY stdout/stderr 를 머신별로 보관하여\r\n// 모바일 attach 시 session_replay 청크로 재전송한다.\r\n//\r\n// 설계 원칙:\r\n//   - byte cap 기준 (UTF-8 byte length). 한계 초과 시 가장 오래된 input chunk 부터 폐기.\r\n//   - 청크 단위 폐기라 정확한 byte cap 이 아니라 cap ± (drop 직전 chunk size).\r\n//     PTY 출력은 보통 ~KB/chunk 라서 5MB cap 에서는 무시 가능한 오차.\r\n//   - WS frame 한계 (1MB 기본) 안에서 송신하기 위해 snapshotChunks() 가 분할.\r\n//   - alt-screen mid-state 복원 문제 (vim/less 한가운데 attach) 는 호출자(ws-manager)\r\n//     또는 클라이언트(모바일 xterm)가 \\x1bc full-reset 을 prepend 하는 방식으로 처리.\r\n//     본 클래스는 그 책임을 가지지 않는다 — raw 출력만 보관/재생.\r\n\r\ninterface BufferEntry {\r\n  readonly data: string\r\n  readonly bytes: number\r\n}\r\n\r\n/**\r\n * 디스크 persist 직렬화 형식. v1.5.0 신규.\r\n *\r\n * `v` 는 schema version — future migration 용. 현재 v: 1 만 지원, 다른 값은 deserialize 시 거부.\r\n * `chunks` 는 append 단위 그대로 보존 (snapshot() 의 concat 과 다름) — restore 시 byte-cap 재적용 가능하도록.\r\n * `savedAt` 은 ISO 8601 — debug / stale-file age 판정용.\r\n */\r\nexport interface ScrollbackSnapshot {\r\n  readonly v: 1\r\n  readonly chunks: readonly string[]\r\n  readonly totalBytes: number\r\n  readonly savedAt: string\r\n}\r\n\r\nexport class PtyScrollbackBuffer {\r\n  private chunks: BufferEntry[] = []\r\n  private totalBytes = 0\r\n\r\n  constructor(private readonly maxBytes: number) {\r\n    if (maxBytes <= 0)\r\n      throw new Error(\r\n        `PtyScrollbackBuffer: maxBytes must be > 0, got ${maxBytes}`\r\n      )\r\n  }\r\n\r\n  /** append(data) — PTY 출력 한 청크 저장. cap 초과 시 head 부터 자동 drop. */\r\n  append(data: string): void {\r\n    if (data.length === 0) return\r\n    const bytes = Buffer.byteLength(data, 'utf8')\r\n    this.chunks.push({ data, bytes })\r\n    this.totalBytes += bytes\r\n    while (this.totalBytes > this.maxBytes && this.chunks.length > 0) {\r\n      const dropped = this.chunks.shift()!\r\n      this.totalBytes -= dropped.bytes\r\n    }\r\n  }\r\n\r\n  /** 현재 보관 중인 byte 수 (UTF-8 기준). */\r\n  size(): number {\r\n    return this.totalBytes\r\n  }\r\n\r\n  /** byte 한계 (생성 시 지정). */\r\n  capacity(): number {\r\n    return this.maxBytes\r\n  }\r\n\r\n  /** 비었으면 true. */\r\n  isEmpty(): boolean {\r\n    return this.chunks.length === 0\r\n  }\r\n\r\n  /**\r\n   * 전체 내용을 하나의 string 으로 반환. 큰 buffer 에서는 snapshotChunks() 권장\r\n   * (WS frame 한계 회피).\r\n   */\r\n  snapshot(): string {\r\n    if (this.chunks.length === 0) return ''\r\n    if (this.chunks.length === 1) return this.chunks[0].data\r\n    return this.chunks.map(c => c.data).join('')\r\n  }\r\n\r\n  /**\r\n   * 한 청크 당 maxChunkBytes 이하로 분할한 배열을 반환.\r\n   * 빈 buffer → 빈 배열. UTF-8 char 가 input chunk 경계 안에서만 잘림 (PTY 가 이미\r\n   * 그 경계로 emit 한 것이라 안전 가정).\r\n   *\r\n   * 단일 input chunk 가 maxChunkBytes 보다 크면 자르지 않고 그대로 한 청크로 push.\r\n   * (PTY 출력은 보통 KB 단위라 거의 일어나지 않음. WS 측에서 이런 경우는 별도 처리 필요.)\r\n   */\r\n  snapshotChunks(maxChunkBytes: number): string[] {\r\n    if (maxChunkBytes <= 0)\r\n      throw new Error(`snapshotChunks: maxChunkBytes must be > 0`)\r\n    if (this.chunks.length === 0) return []\r\n\r\n    const result: string[] = []\r\n    let currentData = ''\r\n    let currentBytes = 0\r\n\r\n    for (const c of this.chunks) {\r\n      // 단일 입력 청크가 limit 보다 큰 경우 — 그대로 단독 청크로 emit\r\n      if (c.bytes > maxChunkBytes) {\r\n        if (currentData.length > 0) {\r\n          result.push(currentData)\r\n          currentData = ''\r\n          currentBytes = 0\r\n        }\r\n        result.push(c.data)\r\n        continue\r\n      }\r\n      // 추가하면 limit 넘는 경우 → flush 후 새 청크 시작\r\n      if (currentBytes + c.bytes > maxChunkBytes && currentData.length > 0) {\r\n        result.push(currentData)\r\n        currentData = ''\r\n        currentBytes = 0\r\n      }\r\n      currentData += c.data\r\n      currentBytes += c.bytes\r\n    }\r\n    if (currentData.length > 0) result.push(currentData)\r\n    return result\r\n  }\r\n\r\n  /**\r\n   * attach 시점 송신 전용 — 최근 [maxTailBytes] 이내만 추출해 [maxChunkBytes]\r\n   * 단위로 분할한 배열을 반환한다. 본 인스턴스는 *변경되지 않는다*.\r\n   *\r\n   * cross-repo with `claude_code_mobile` autoplan 2026-05-22 UC-3 — replay flood\r\n   * 의 source 를 desktop 측에서 cap. mobile attach 시 ReplayProgressOverlay\r\n   * (PR #48 v1.7.19) 노출 시간 단축. `append()` 의 cap (=`maxBytes`) 는 그대로\r\n   * 보존되어 사용자 스크롤백은 desktop 측에 누적된다 — 본 메서드는 *송신 한도*\r\n   * 만 제한.\r\n   *\r\n   * 구현: 임시 buffer 에 모든 chunk 를 append 해 기존 cap 정책 (head drop) 으로\r\n   * 자연스럽게 tail 만 유지 → [snapshotChunks] 재사용으로 분할 알고리즘 중복 0.\r\n   *\r\n   * - `maxTailBytes <= 0` 또는 `maxChunkBytes <= 0` → throw.\r\n   * - 빈 buffer → 빈 배열.\r\n   * - 단일 chunk 가 [maxTailBytes] 보다 크면 (현실적으로 거의 없음) [snapshotChunks]\r\n   *   의 oversized 분기와 동일하게 단독 청크로 emit — 송신 자체는 보장.\r\n   */\r\n  snapshotChunksForAttach(\r\n    maxTailBytes: number,\r\n    maxChunkBytes: number\r\n  ): string[] {\r\n    if (maxTailBytes <= 0)\r\n      throw new Error('snapshotChunksForAttach: maxTailBytes must be > 0')\r\n    if (maxChunkBytes <= 0)\r\n      throw new Error('snapshotChunksForAttach: maxChunkBytes must be > 0')\r\n    if (this.chunks.length === 0) return []\r\n\r\n    const tmp = new PtyScrollbackBuffer(maxTailBytes)\r\n    for (const c of this.chunks) tmp.append(c.data)\r\n    return tmp.snapshotChunks(maxChunkBytes)\r\n  }\r\n\r\n  /** 전체 비움. pty_restart (E4) 시 호출. */\r\n  clear(): void {\r\n    this.chunks = []\r\n    this.totalBytes = 0\r\n  }\r\n\r\n  /**\r\n   * 디스크 persist 직렬화 (v1.5.0). chunk 단위 보존 — snapshot() 의 concat 과 달리\r\n   * restore 시 byte-cap 정확 재적용 가능.\r\n   *\r\n   * 비어 있으면 chunks=[] / totalBytes=0 반환 — caller 가 noop 판단 가능.\r\n   */\r\n  serialize(): ScrollbackSnapshot {\r\n    return {\r\n      v: 1,\r\n      chunks: this.chunks.map(c => c.data),\r\n      totalBytes: this.totalBytes,\r\n      savedAt: new Date().toISOString(),\r\n    }\r\n  }\r\n\r\n  /**\r\n   * 디스크에서 로드한 snapshot 으로부터 새 instance 복원 (v1.5.0).\r\n   *\r\n   * `maxBytes` 는 *현재* 인스턴스의 cap — snapshot 이 더 크면 head 부터 자동 drop\r\n   * (constructor 의 cap 적용 로직 그대로 사용). caller 가 cap 변경 시 안전 전환.\r\n   *\r\n   * snapshot.v !== 1 또는 chunks 가 array 아님 / 손상된 entry 등의 경우 throw —\r\n   * caller 가 try/catch 후 새 buffer 로 fallback.\r\n   */\r\n  static deserialize(\r\n    maxBytes: number,\r\n    snapshot: ScrollbackSnapshot\r\n  ): PtyScrollbackBuffer {\r\n    if (!snapshot || typeof snapshot !== 'object') {\r\n      throw new Error(\r\n        'PtyScrollbackBuffer.deserialize: snapshot is not an object'\r\n      )\r\n    }\r\n    if (snapshot.v !== 1) {\r\n      throw new Error(\r\n        `PtyScrollbackBuffer.deserialize: unsupported version ${snapshot.v}`\r\n      )\r\n    }\r\n    if (!Array.isArray(snapshot.chunks)) {\r\n      throw new Error('PtyScrollbackBuffer.deserialize: chunks is not an array')\r\n    }\r\n    const buf = new PtyScrollbackBuffer(maxBytes)\r\n    for (const data of snapshot.chunks) {\r\n      if (typeof data !== 'string') {\r\n        throw new Error(\r\n          'PtyScrollbackBuffer.deserialize: chunk is not a string'\r\n        )\r\n      }\r\n      buf.append(data)\r\n    }\r\n    return buf\r\n  }\r\n}\r\n","/**\r\n * ScrollbackPersister — PtyScrollbackBuffer 의 디스크 영속화 (v1.5.0).\r\n *\r\n * 분리 동기: v1.3.4 까지 scrollback 은 in-memory only. 데스크탑 앱 종료 시 모든 backlog 가\r\n * 사라져 모바일 reattach 가 빈 화면. v1.5.0 은 머신별로 JSON 직렬화해 디스크에 저장하고\r\n * relaunch 시 첫 attach 에서 복원해 진짜 session resume 을 제공한다.\r\n *\r\n * 정책:\r\n *   - 머신별 1 파일 — `<stateDir>/<sanitized-machineId>.json`. machineId 는 `[^a-zA-Z0-9_-]`\r\n *     문자를 `_` 로 sanitize.\r\n *   - 라이브 append 직후 5s debounce 로 비동기 write 예약. 종료 직전 `flushAllSync` 가\r\n *     남은 dirty buffer 를 동기 직렬화 — Electron `before-quit` (sync handler) 안에서 안전.\r\n *   - 손상된 JSON / version mismatch / non-existent 는 `restore` 에서 null 반환 (caller 가\r\n *     새 buffer 생성). silent recovery — 사용자 영향은 그 머신 첫 reattach 가 빈 backlog 뿐.\r\n *   - `cleanupStale(known)` 는 앱 시작 시 호출 — store 에 없는 machineId 의 파일 unlink.\r\n *     orphan 누적 방지 (최대 5MB × N).\r\n *   - 사용자 명시 disconnect 시 `remove(machineId)` 호출 — 그 머신 backlog 영구 삭제.\r\n *\r\n * 보안: 평문 JSON. 비밀번호/토큰 echo 가 디스크에 잔존 가능. v1.5.0 CHANGELOG 에서 사용자\r\n * 고지. v1.6+ 에서 OS keychain 기반 암호화 후속 작업.\r\n */\r\n\r\nimport {\r\n  existsSync,\r\n  mkdirSync,\r\n  readdirSync,\r\n  readFileSync,\r\n  unlinkSync,\r\n  writeFileSync,\r\n} from 'node:fs'\r\nimport { join } from 'node:path'\r\n\r\nimport type {\r\n  PtyScrollbackBuffer,\r\n  ScrollbackSnapshot,\r\n} from './scrollback-buffer'\r\n\r\n/** debounce 간격 — 라이브 append 후 디스크 write 까지의 지연. */\r\nconst FLUSH_DEBOUNCE_MS = 5000\r\n\r\n/**\r\n * 머신별 backlog 의 디스크 영속화 핸들. 1 인스턴스 = 1 stateDir = 1 ssh-manager.\r\n *\r\n * 생성자 의존: stateDir (예: `%LOCALAPPDATA%/ai-rva/scrollback/`). caller (ssh-manager) 가\r\n * platform 별 path 결정. 미존재 시 `mkdirSync(recursive: true)` 로 자동 생성.\r\n */\r\nexport class ScrollbackPersister {\r\n  private readonly stateDir: string\r\n  /** machineId → 예약된 setTimeout 의 Timer. clear/reschedule 용. */\r\n  private readonly pendingFlush = new Map<string, NodeJS.Timeout>()\r\n\r\n  constructor(stateDir: string) {\r\n    this.stateDir = stateDir\r\n    // mkdirSync 는 EEXIST 에 영향받지 않는 recursive 모드. EACCES 등은 caller 가 처리.\r\n    try {\r\n      mkdirSync(stateDir, { recursive: true, mode: 0o700 })\r\n    } catch {\r\n      // 디렉토리 생성 실패 → 라이브 운영에 큰 영향 없음 (write 시 다시 시도). 로그만.\r\n      console.warn(`[ScrollbackPersister] mkdirSync failed for ${stateDir}`)\r\n    }\r\n  }\r\n\r\n  /**\r\n   * machineId 에 대한 디스크 파일 경로. sanitize 는 file system 충돌 방지용.\r\n   * 같은 sanitized 결과가 다른 machineId 둘 사이에서 발생할 수 있는 risk —\r\n   * 현재 machineId 포맷이 UUID-like 라 충돌 가능성 무시 가능 수준.\r\n   */\r\n  resolveFilePath(machineId: string): string {\r\n    const safe = machineId.replace(/[^a-zA-Z0-9_-]/g, '_')\r\n    return join(this.stateDir, `${safe || 'default'}.json`)\r\n  }\r\n\r\n  /**\r\n   * 디스크에서 ScrollbackSnapshot 복원. 파일 없거나 손상이면 null 반환.\r\n   *\r\n   * sync — caller (ssh-manager.getOrCreateScrollback) 가 hot path 의 동기 호출 안에서\r\n   * 즉시 사용 가능해야 한다. 파일 사이즈 5MB cap 이라 sync read 부담 작음.\r\n   *\r\n   * caller 는 null 일 때 새 buffer 생성, 아니면 `PtyScrollbackBuffer.deserialize(maxBytes, snapshot)` 로 복원.\r\n   */\r\n  restoreSync(machineId: string): ScrollbackSnapshot | null {\r\n    const path = this.resolveFilePath(machineId)\r\n    if (!existsSync(path)) return null\r\n    try {\r\n      const raw = readFileSync(path, 'utf8')\r\n      const parsed = JSON.parse(raw) as ScrollbackSnapshot\r\n      if (!parsed || typeof parsed !== 'object' || parsed.v !== 1) {\r\n        console.warn(\r\n          `[ScrollbackPersister] discarding ${path} — bad version/shape`\r\n        )\r\n        return null\r\n      }\r\n      return parsed\r\n    } catch (err) {\r\n      console.warn(\r\n        `[ScrollbackPersister] restore failed for ${path}: ${(err as Error).message}`\r\n      )\r\n      return null\r\n    }\r\n  }\r\n\r\n  /**\r\n   * 5s debounce 된 비동기 write 예약. 같은 machineId 에 대해 이미 예약된 게 있으면\r\n   * 그것 cancel 하고 다시 예약 — 마지막 호출만 유효 (디스크 throttle).\r\n   */\r\n  scheduleFlush(machineId: string, buf: PtyScrollbackBuffer): void {\r\n    const existing = this.pendingFlush.get(machineId)\r\n    if (existing) clearTimeout(existing)\r\n    const timer = setTimeout(() => {\r\n      this.pendingFlush.delete(machineId)\r\n      this.flushSync(machineId, buf)\r\n    }, FLUSH_DEBOUNCE_MS)\r\n    // setTimeout 결과는 Electron 종료를 막을 수 있어 unref — Electron quit 시 자연\r\n    // 폐기. 종료 흐름에서는 어차피 flushAllSync 가 sync 로 처리.\r\n    if (typeof timer.unref === 'function') timer.unref()\r\n    this.pendingFlush.set(machineId, timer)\r\n  }\r\n\r\n  /**\r\n   * 동기 디스크 write. before-quit / 명시 detach 등에서 호출. 빈 buffer 면 unlink\r\n   * (이전 데이터 정리).\r\n   *\r\n   * write 실패는 swallow + warn — 종료 흐름을 막지 않는다. 사용자 영향: 다음 launch 에서\r\n   * 그 머신 backlog 는 이전 상태 유지 (write 못 한 변경분만 손실).\r\n   */\r\n  flushSync(machineId: string, buf: PtyScrollbackBuffer): void {\r\n    // 예약된 비동기 write 가 있으면 cancel — 우리가 sync 로 대체.\r\n    const pending = this.pendingFlush.get(machineId)\r\n    if (pending) {\r\n      clearTimeout(pending)\r\n      this.pendingFlush.delete(machineId)\r\n    }\r\n    const path = this.resolveFilePath(machineId)\r\n    if (buf.isEmpty()) {\r\n      // 비었으면 stale file 도 제거 — 다음 launch 에서 None 반환되도록.\r\n      try {\r\n        if (existsSync(path)) unlinkSync(path)\r\n      } catch {\r\n        // unlink 실패 — 무해.\r\n      }\r\n      return\r\n    }\r\n    try {\r\n      const snap = buf.serialize()\r\n      writeFileSync(path, JSON.stringify(snap), {\r\n        encoding: 'utf8',\r\n        mode: 0o600,\r\n      })\r\n    } catch (err) {\r\n      console.warn(\r\n        `[ScrollbackPersister] flushSync failed for ${path}: ${(err as Error).message}`\r\n      )\r\n    }\r\n  }\r\n\r\n  /**\r\n   * cleanup 에서 호출. 모든 dirty buffer 를 동기 write — Electron before-quit 의 sync\r\n   * handler 안에서 안전. Map<machineId, PtyScrollbackBuffer> 받음.\r\n   */\r\n  flushAllSync(buffers: Map<string, PtyScrollbackBuffer>): void {\r\n    for (const [machineId, buf] of buffers) {\r\n      this.flushSync(machineId, buf)\r\n    }\r\n  }\r\n\r\n  /** 머신 명시 삭제 (사용자 disconnect). sync — 종료 흐름과 분리됐지만 sync I/O 가 단순. */\r\n  remove(machineId: string): void {\r\n    const pending = this.pendingFlush.get(machineId)\r\n    if (pending) {\r\n      clearTimeout(pending)\r\n      this.pendingFlush.delete(machineId)\r\n    }\r\n    const path = this.resolveFilePath(machineId)\r\n    try {\r\n      if (existsSync(path)) unlinkSync(path)\r\n    } catch (err) {\r\n      console.warn(\r\n        `[ScrollbackPersister] remove failed for ${path}: ${(err as Error).message}`\r\n      )\r\n    }\r\n  }\r\n\r\n  /**\r\n   * 앱 launch 시 호출. stateDir 의 모든 *.json 중 known set 에 없는 것 unlink.\r\n   * machineId 가 UI 에서 삭제됐는데 남아있는 파일 정리. 반환: 정리된 파일 수.\r\n   *\r\n   * sanitize 결과를 다시 reverse-mapping 하지 않고 known 의 sanitize 결과와 비교.\r\n   * sync — 시작 시 1회 호출이라 부담 적음.\r\n   */\r\n  cleanupStale(knownMachineIds: Set<string>): number {\r\n    if (!existsSync(this.stateDir)) return 0\r\n    const knownSafe = new Set<string>()\r\n    for (const id of knownMachineIds) {\r\n      const safe = id.replace(/[^a-zA-Z0-9_-]/g, '_') || 'default'\r\n      knownSafe.add(safe)\r\n    }\r\n    let removed = 0\r\n    let entries: string[]\r\n    try {\r\n      entries = readdirSync(this.stateDir)\r\n    } catch {\r\n      return 0\r\n    }\r\n    for (const name of entries) {\r\n      if (!name.endsWith('.json')) continue\r\n      const safe = name.slice(0, -'.json'.length)\r\n      if (knownSafe.has(safe)) continue\r\n      try {\r\n        unlinkSync(join(this.stateDir, name))\r\n        removed += 1\r\n      } catch {\r\n        // 무해 — 다음 launch 에서 다시 시도.\r\n      }\r\n    }\r\n    return removed\r\n  }\r\n}\r\n","export const ENVIRONMENT = {\r\n  IS_DEV: process.env.NODE_ENV === 'development',\r\n}\r\n\r\nexport const PLATFORM = {\r\n  IS_MAC: process.platform === 'darwin',\r\n  IS_WINDOWS: process.platform === 'win32',\r\n  IS_LINUX: process.platform === 'linux',\r\n}\r\n\r\n// ─── Relay Protocol ───────────────────────────────────────────────────────────\r\n// v3: ttyd-style 1-byte opcode prefix + binary frame. v2 (JSON-only text frame) 의\r\n// application JSON 누출을 protocol-level 차단. spec: claude_code_mobile/docs/specs/binary-protocol-v3.md.\r\n// 모바일 측 (claude_code_mobile/lib/services/relay_service.dart) 의 kRelayProtocolVersion\r\n// 과 *반드시 동시* bump — 한쪽만 변경 시 첫 hello 단계에서 silent mismatch + 사용자\r\n// dialog 발화. v3 는 hard switch — v2 호환 0.\r\nexport const PROTOCOL_VERSION = 3\r\n\r\n// 머신당 PTY 출력 보관 한계 default. v1.7.8 에서 5MB → 1MB 로 변경\r\n// (cross-repo with `claude_code_mobile` PR #48 autoplan UC-3 결정 반영).\r\n//\r\n// 사용자 선택: 모바일 측 overlay 노출 시간 단축이 주 목적 — 5MB chunks 보다\r\n// 1MB chunks 가 5배 빨리 끝남. 큰 scrollback 필요 시 머신 편집 UI 에서\r\n// 5MB / 10MB stepper 선택 가능 (`Machine.ringBufferBytes`).\r\n//\r\n// 1MB ≈ 80x24 기준 ~10K-20K 줄, 32 머신 worst case 합 = 32MB scrollback (이전 5배\r\n// 절감). ADR-0001 의 worst-case 메모리 산정 같이 갱신 필요 (별 issue).\r\nexport const SCROLLBACK_BUFFER_BYTES_DEFAULT = 1 * 1024 * 1024\r\n\r\n/**\r\n * @deprecated v1.7.8 — `SCROLLBACK_BUFFER_BYTES_DEFAULT` 사용. 기존 caller\r\n * 호환을 위해 별칭 유지. 다음 PR 에서 모든 호출처 신규 이름으로 교체 후 제거.\r\n */\r\nexport const SCROLLBACK_BUFFER_BYTES = SCROLLBACK_BUFFER_BYTES_DEFAULT\r\n\r\n// PTY 동시 보유 상한. spawn 직전 LRU eviction 트리거. 근거 (ADR-0001):\r\n// Windows conpty ~30MB/PTY → 32 = ~960MB worst case. macOS/Linux ~8MB/PTY → 256MB.\r\n// Windows 보수적 기준으로 32 채택. 추후 platform 별 override 검토.\r\nexport const PTY_COUNT_MAX = 32\r\n\r\n// PTY 가 attach/activity 없이 살아있을 수 있는 최대 시간. 초과 시 idle eviction.\r\n// 7d = 일반 사용자 1주 휴가/주말 cycle 보전. <7d 면 PTO 후 잃을 위험.\r\nexport const PTY_IDLE_TIMEOUT_MS = 7 * 24 * 60 * 60 * 1000\r\n\r\n// last-activity (출력/입력) 가 이 시간 이내면 'running', 아니면 'idle' status broadcast.\r\n// 30s = 사용자가 명령 입력 후 결과 기다리는 typical 시간 < 30s.\r\nexport const PTY_ACTIVITY_THRESHOLD_MS = 30_000\r\n\r\n// pty_status broadcast 의 throttle 간격. 같은 status 가 이 시간 안에 또 나오면 skip.\r\n// 단 status 변경 (running→idle 등) 은 즉시 송신 (bypass throttle).\r\nexport const PTY_STATUS_BROADCAST_THROTTLE_MS = 5_000\r\n\r\n// session_replay 메시지 1개당 최대 payload 바이트.\r\n// WS 기본 frame 한계 (1MB) 보다 충분히 작게 — JSON 인코딩 오버헤드 + UTF-8 escape 고려.\r\nexport const REPLAY_CHUNK_BYTES = 256 * 1024\r\n\r\n// session_attach 응답 송신용 tail cap.\r\n//\r\n// cross-repo with `claude_code_mobile` autoplan 2026-05-22 UC-3 — replay flood\r\n// 의 source 를 desktop 측에서 cap 하여 모바일 ReplayProgressOverlay (PR #48 v1.7.19)\r\n// 노출 시간 단축. 모바일 mid-session reconnect UX (claude_code_mobile v1.7.20+51)\r\n// 와 짝.\r\n//\r\n// 16KB ≈ 80 cols × ~200 lines (rotation 여유). scrollback buffer 자체\r\n// (`SCROLLBACK_BUFFER_BYTES_DEFAULT` = 1MB) 는 변경 없음 — append 그대로 누적\r\n// 되어 모바일 측 ReplayProgressOverlay 가 사라진 뒤에도 이론적으로는 desktop 의\r\n// 전체 backlog 가 PTY 측 ANSI \\x1b[?1049l/h 또는 사용자 명령으로 회복 가능.\r\n//\r\n// 본 cap 은 *송신 한도* 만 제한 — `PtyScrollbackBuffer.snapshotChunksForAttach`\r\n// 가 tail 부터 16KB 만 송신. 사용자가 attach 직후 위로 스크롤하면 그 너머 데이터는\r\n// 모바일 xterm 의 scrollback 에 없다 (추후 pagination 으로 확장 가능).\r\nexport const REPLAY_ATTACH_TAIL_BYTES = 16 * 1024\r\n\r\n// session_attach 후 session_replay 가 안 올 때 모바일이 legacy connect 로 fallback 하는 시간.\r\n// 신모바일 + 구데스크탑 시나리오에서만 의미. 정상 케이스는 ms 단위로 도착.\r\nexport const REPLAY_FALLBACK_TIMEOUT_MS = 5000\r\n\r\n// ─── Auto-Update Polling ──────────────────────────────────────────────────────\r\n\r\n/**\r\n * 주기 업데이트 체크 인터벌 (1시간).\r\n *\r\n * 근거 (autoplan v2 §\"분석 — 적정 체크 주기\"): 익명 GitHub API rate limit 의 40%\r\n * 만 사용, 사용자 인지 지연 평균 30분. 1시간보다 짧으면 GitHub rate limit 위험,\r\n * 길면 새 릴리즈 발견 지연 체감 가능.\r\n *\r\n * dev 모드 (`ENVIRONMENT.IS_DEV === true`) 에서는 autoUpdater 자체가 비활성이라 본\r\n * 상수도 미사용.\r\n */\r\nexport const UPDATE_CHECK_INTERVAL_MS = 60 * 60 * 1000\r\n\r\n/**\r\n * Auto-update install 시 mobile-safe graceful shutdown 의 grace 시간 (5s).\r\n *\r\n * autoplan E1 — 사용자가 \"지금 설치 및 재시작\" 을 confirm 한 직후 모든 mobile client\r\n * 에 `app:update-installing` broadcast 후 본 시간 만큼 대기, 그 후 `wsManager.cleanup()`\r\n * + `autoUpdater.quitAndInstall()`. 모바일이 disconnect 토스트 표시 + scrollback 보존\r\n * 할 시간 마진.\r\n */\r\nexport const UPDATE_INSTALL_GRACE_MS = 5000\r\n\r\n/**\r\n * Auto-update 수동 체크 (`update:check-now` IPC) 의 최대 대기 시간 (10s).\r\n *\r\n * autoplan E6 — `autoUpdater.checkForUpdates()` 가 네트워크 단절 등으로 무한 hang\r\n * 가능. Promise.race timeout 으로 보호하고 caller 에는 `{ ok: false, reason: 'timeout' }`\r\n * 반환. polling 의 schedulePoll 은 별도 try/catch 만 — interval 자체가 1시간이라\r\n * timeout 불요.\r\n */\r\nexport const UPDATE_CHECK_NOW_TIMEOUT_MS = 10_000\r\n\r\n// ─── Log Cleanup ────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * 로그 청소 주기 (24h). `auto-update.ts` 의 schedulePoll 방식을 재사용하되 주기는 하루 1회.\r\n *\r\n * 근거 (autoplan 2026-06-07 결정1): 로그 보존이 14일이라 시간당 granularity 는 startup-only/\r\n * daily 대비 이득이 거의 없다(스캔 대상 파일 셋이 1시간 안엔 거의 안 변함). 스케줄러 '방식'은\r\n * 유지하되 주기만 하루로 둬 타이머 churn 을 줄인다.\r\n */\r\nexport const LOG_CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000\r\n\r\n/**\r\n * 로그 파일 보존 한도 (14일). mtime 이 이보다 오래된 `*.log` / `*.old.log` 만 삭제 대상.\r\n *\r\n * 근거: `PTY_IDLE_TIMEOUT_MS`(7일, 1주 휴가 cycle)보다 길게 둬 1주 부재 후 복귀 시에도\r\n * 디버깅용 로그가 남도록 마진을 둔다. `.old` 아카이브에도 동일 age-gate 가 적용되어 갓\r\n * 로테이션된 신선한 백업은 보존된다.\r\n */\r\nexport const LOG_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000\r\n","/**\r\n * vt100/ANSI emulator wrap — A3 server-truth grid 의 단일 소스.\r\n *\r\n * 책임:\r\n *   - PTY stdout chunk → grid cell 변환 (`@xterm/headless` 위임)\r\n *   - GRID_SNAPSHOT (0x04) 직렬화 (Buffer)\r\n *   - 마지막 broadcast 이후 변경된 cell 만 GRID_DIFF (0x05) 직렬화\r\n *   - resize cap 검사 (max 500 cols × 200 rows — plan §13-3 / spec §3-β)\r\n *   - seq / base_seq 단조증가 관리 (client 가 gap 감지로 snapshot.req 발사 가능)\r\n *\r\n * Owner: PtyLifecycleManager. PTY 1:1.\r\n *\r\n * single-path 정책 (plan §13-2 / spec §6.5): mobile/grid.diff 클라이언트는 client-side\r\n * Terminal.resize() 직접 호출 안 함. 모든 grid 변경은 본 클래스가 권위 — server-truth.\r\n *\r\n * spike note: dirty range 정밀 추적은 v2 — 현재는 보수적 markFullDirty() 로 정확성 우선.\r\n * GRID_DIFF 가 사실상 GRID_SNAPSHOT 크기와 비슷할 수 있으나 wire 호환은 정확.\r\n *\r\n * spec source of truth: claude_code_mobile/docs/specs/binary-protocol-v3.md §3, §3-α, §3-β.\r\n */\r\nimport headless from '@xterm/headless'\r\nconst { Terminal } = headless;\r\nimport { SerializeAddon } from '@xterm/addon-serialize'\r\nimport { encodeGridSnapshot, encodeGridDiff } from '../protocol/binary-protocol'\r\n\r\nconst MAX_COLS = 500\r\nconst MAX_ROWS = 200\r\n\r\n/**\r\n * Resize 결과 — capped 인 경우 실제 적용된 cols/rows 가 인자와 다름. ws-manager 가\r\n * client 에 `grid_resize_capped` APP_JSON broadcast (실제값 회신) 시 사용.\r\n */\r\nexport interface ResizeResult {\r\n  cols: number\r\n  rows: number\r\n  capped: boolean\r\n}\r\n\r\n/**\r\n * 단일 PTY 의 grid 권위 emulator. PtyLifecycleManager 가 PTY 1:1 로 owner.\r\n */\r\nexport class GridEmulator {\r\n  // `Terminal` 은 `headless` default export 에서 destructure 한 값.\r\n  // type annotation 에는 InstanceType<typeof Terminal> 로 instance 타입 추출.\r\n  private term: InstanceType<typeof Terminal>\r\n  private serializer: SerializeAddon\r\n  private seq = 0\r\n  private lastBroadcastSeq = 0\r\n  private dirty = new Set<string>() // \"r,c\" key\r\n\r\n  constructor(initialCols: number, initialRows: number) {\r\n    const cols = Math.min(Math.max(1, initialCols), MAX_COLS)\r\n    const rows = Math.min(Math.max(1, initialRows), MAX_ROWS)\r\n    this.term = new Terminal({ cols, rows, allowProposedApi: true })\r\n    // `@xterm/addon-serialize` 의 typings 는 `@xterm/xterm` 의 Terminal 을 expect 하지만\r\n    // `@xterm/headless` 의 Terminal 도 동일 IBuffer/loadAddon 인터페이스를 구현해 runtime 에서\r\n    // 호환 (relay frame-snapshot.service.ts PoC 검증). `tsconfig.skipLibCheck=true` 로 .d.ts\r\n    // 안 type mismatch 는 무시되고, `loadAddon(serializer as any)` 으로 호출부 type 충돌만 우회.\r\n    this.serializer = new SerializeAddon()\r\n    this.term.loadAddon(this.serializer as any)\r\n    // dirty tracking: 보수적 — 매 chunk parse 후 전체 cell 을 dirty 표시.\r\n    // 정확한 dirty range 추출은 xterm/headless internal API 조사 후 v2 정밀화.\r\n    this.term.onWriteParsed(() => this.markFullDirty())\r\n  }\r\n\r\n  /** 현재 grid 크기. */\r\n  get cols(): number {\r\n    return this.term.cols\r\n  }\r\n\r\n  get rows(): number {\r\n    return this.term.rows\r\n  }\r\n\r\n  /**\r\n   * PTY stdout chunk append — ANSI parse + grid 갱신.\r\n   * write callback 안에서 onWriteParsed 가 자동 trigger 되어 dirty 표시.\r\n   *\r\n   * [onParsed] xterm/headless 의 write 는 비동기 파싱이라, 파싱 완료(buffer 반영) 시점에\r\n   * 콜백된다. 운영 경로(PTY 흐름)는 콜백 불필요하지만, 테스트가 파싱 완료를 동기화하는 데 사용.\r\n   */\r\n  write(chunk: string | Buffer, onParsed?: () => void): void {\r\n    this.term.write(\r\n      typeof chunk === 'string' ? chunk : chunk.toString('utf8'),\r\n      onParsed,\r\n    )\r\n  }\r\n\r\n  /**\r\n   * RESIZE 수신 — cap 검사 + 적용.\r\n   *\r\n   * cap (`MAX_COLS=500`, `MAX_ROWS=200`) 초과 시 truncate. ws-manager 가 결과의\r\n   * `capped=true` 를 보고 client 에 `grid_resize_capped` APP_JSON broadcast 해야 함.\r\n   *\r\n   * resize 직후 모든 cell 을 dirty 표시 — 다음 diff broadcast 가 사실상 full snapshot\r\n   * 크기. spike v2 에서 resize-aware diff (이전 grid 와 비교) 로 정밀화 가능.\r\n   */\r\n  resize(cols: number, rows: number): ResizeResult {\r\n    const c = Math.min(Math.max(1, cols), MAX_COLS)\r\n    const r = Math.min(Math.max(1, rows), MAX_ROWS)\r\n    const capped = c !== cols || r !== rows\r\n    this.term.resize(c, r)\r\n    this.markFullDirty()\r\n    return { cols: c, rows: r, capped }\r\n  }\r\n\r\n  /**\r\n   * Cold attach (mobile 가 cap.ack 'grid.diff' 받은 직후) / recover (snapshot.req 응답).\r\n   * Full snapshot 송신 — seq 증가, dirty clear, lastBroadcastSeq 갱신.\r\n   *\r\n   * 반환은 GRID_SNAPSHOT (0x04) frame Buffer. ws-manager 가 ws.send() 로 전송.\r\n   */\r\n  snapshot(): Buffer {\r\n    this.seq++\r\n    this.lastBroadcastSeq = this.seq\r\n    this.dirty.clear()\r\n    const cells = this.serializeCells()\r\n    const buf = this.term.buffer.active\r\n    return encodeGridSnapshot(\r\n      this.seq,\r\n      this.term.cols,\r\n      this.term.rows,\r\n      cells,\r\n      buf.cursorY,\r\n      buf.cursorX,\r\n    )\r\n  }\r\n\r\n  /**\r\n   * Incremental — 마지막 broadcast 이후 dirty cell 만 GRID_DIFF (0x05) 직렬화.\r\n   * dirty 가 비어있으면 null 반환 (broadcast skip — keep-alive 는 ws-manager 가 별도 정책).\r\n   */\r\n  diffSinceLast(): Buffer | null {\r\n    if (this.dirty.size === 0) return null\r\n    this.seq++\r\n    const ops = this.serializeDirtyOps()\r\n    const baseSeq = this.lastBroadcastSeq\r\n    this.lastBroadcastSeq = this.seq\r\n    this.dirty.clear()\r\n    return encodeGridDiff(this.seq, baseSeq, ops)\r\n  }\r\n\r\n  /**\r\n   * 현재 화면 state 를 ANSI 문자열로 직렬화 (cursor 위치 + SGR 보존).\r\n   *\r\n   * `addon-serialize` 의 `serialize()` 는 mobile 이 `\\x1b[2J\\x1b[H` reset 후 그대로 write 하면\r\n   * 동일 화면을 재현할 수 있는 ANSI 시퀀스를 반환한다. ws-manager 가 session_attach 시 이 결과를\r\n   * FRAME_SNAPSHOT (0x03) 으로 감싸 attach 하는 mobile 에 송신 — busy alt-screen TUI(claude 등)가\r\n   * raw scrollback tail 만으로는 복원 불가한 화면을 본 emulator(정확한 PTY size)로 정확히 복원한다.\r\n   *\r\n   * 직렬화 실패 시 빈 문자열 반환 — 호출부(ws-manager)는 빈 문자열이면 frame 송신을 skip 한다.\r\n   */\r\n  serializeAnsi(): string {\r\n    try {\r\n      // scrollback:0 — 보이는 화면만 직렬화(review I1). 기본값은 전체 scrollback(headless 1000줄)\r\n      // 까지 포함해 frame 이 수백 KB~1MB 로 비대해질 수 있고(relay maxPayload drop 위험), 모바일은\r\n      // `\\x1b[2J\\x1b[H` 후 현재 화면만 overlay 하므로 scrollback 은 불필요하다.\r\n      return this.serializer.serialize({ scrollback: 0 })\r\n    } catch {\r\n      return ''\r\n    }\r\n  }\r\n\r\n  /**\r\n   * [non-sync diag 20260614] 빈 스냅샷의 원인이 a1(claude 가 그 순간 화면을 실제로 비움) 인지\r\n   * a2(`@xterm/addon-serialize` 가 alt-screen buffer 를 못 잡는 버그) 인지를 캡처 없이 ws.log 로\r\n   * 가르기 위한 read-only 진단.\r\n   *\r\n   * `serializeAnsi()` 결과(FRAME_SNAPSHOT bytes)가 작을 때, **동일한 active buffer 를 직접 순회**해\r\n   * 실제 내용 유무를 잰다. buffer 에 내용이 있는데(nonBlankLines 큼) 직렬화만 비면 = a2(버그),\r\n   * buffer 도 비면 = a1(claude). busy 인데 `bufType==='normal'` 이면 alt-screen 이탈 보조 단서.\r\n   * docs/non-sync.md §4 (claude_code_mobile repo). grid 상태/seq/dirty 불변.\r\n   */\r\n  snapshotDiag(): {\r\n    bufType: string\r\n    nonBlankLines: number\r\n    rows: number\r\n    glyphs: number\r\n  } {\r\n    const buf = this.term.buffer.active\r\n    let nonBlankLines = 0\r\n    let glyphs = 0\r\n    for (let r = 0; r < this.term.rows; r++) {\r\n      const line = buf.getLine(r)\r\n      if (!line) continue\r\n      const trimmed = line.translateToString(true).trim()\r\n      if (trimmed.length === 0) continue\r\n      nonBlankLines++\r\n      for (const ch of trimmed) if (ch !== ' ') glyphs++\r\n    }\r\n    return { bufType: buf.type, nonBlankLines, rows: this.term.rows, glyphs }\r\n  }\r\n\r\n  /** xterm/headless 인스턴스 폐기. */\r\n  dispose(): void {\r\n    this.term.dispose()\r\n    this.dirty.clear()\r\n  }\r\n\r\n  // ── private helpers ──────────────────────────────────────────────────\r\n\r\n  /** 모든 cell 을 dirty 표시 — write/resize 후 호출. */\r\n  private markFullDirty(): void {\r\n    for (let r = 0; r < this.term.rows; r++) {\r\n      for (let c = 0; c < this.term.cols; c++) {\r\n        this.dirty.add(`${r},${c}`)\r\n      }\r\n    }\r\n  }\r\n\r\n  /**\r\n   * xterm buffer → cell array Buffer (full grid).\r\n   *\r\n   * Encoding (spec §3-α palette mode default):\r\n   *   per cell: [ch UTF-8 1-4 bytes][fg u8 palette][bg u8 palette][attrs u8]\r\n   *   평균 6 bytes/cell — 500×200 = ~600KB (WS frame 1MB 안).\r\n   *\r\n   * RGB mode (cell.getFgColor() 의 high bit 검사 필요) 는 spike v2.\r\n   */\r\n  private serializeCells(): Buffer {\r\n    const buf = this.term.buffer.active\r\n    const out: number[] = []\r\n    for (let r = 0; r < this.term.rows; r++) {\r\n      const line = buf.getLine(r)\r\n      if (!line) continue\r\n      for (let c = 0; c < this.term.cols; c++) {\r\n        const cell = line.getCell(c)\r\n        if (!cell) continue\r\n        const ch = cell.getChars() || ' '\r\n        const chBytes = Buffer.from(ch, 'utf8')\r\n        out.push(...chBytes)\r\n        out.push(this.normalizeColor(cell.getFgColor(), 7))\r\n        out.push(this.normalizeColor(cell.getBgColor(), 0))\r\n        out.push(this.encodeAttrs(cell))\r\n      }\r\n    }\r\n    return Buffer.from(out)\r\n  }\r\n\r\n  /**\r\n   * Dirty cell 만 ops Buffer 로 직렬화. opCount (BE u16) prefix 포함.\r\n   *\r\n   * Per op: [r BE u16][c BE u16][ch UTF-8][fg u8][bg u8][attrs u8].\r\n   */\r\n  private serializeDirtyOps(): Buffer {\r\n    const buf = this.term.buffer.active\r\n    const opCount = this.dirty.size\r\n    const head = Buffer.alloc(2)\r\n    head.writeUInt16BE(opCount & 0xffff, 0)\r\n    const opBytes: number[] = []\r\n    for (const key of this.dirty) {\r\n      const [rs, cs] = key.split(',')\r\n      const r = parseInt(rs, 10)\r\n      const c = parseInt(cs, 10)\r\n      const line = buf.getLine(r)\r\n      if (!line) continue\r\n      const cell = line.getCell(c)\r\n      if (!cell) continue\r\n      const ch = cell.getChars() || ' '\r\n      const chBytes = Buffer.from(ch, 'utf8')\r\n      const rcBuf = Buffer.alloc(4)\r\n      rcBuf.writeUInt16BE(r & 0xffff, 0)\r\n      rcBuf.writeUInt16BE(c & 0xffff, 2)\r\n      opBytes.push(...rcBuf)\r\n      opBytes.push(...chBytes)\r\n      opBytes.push(this.normalizeColor(cell.getFgColor(), 7))\r\n      opBytes.push(this.normalizeColor(cell.getBgColor(), 0))\r\n      opBytes.push(this.encodeAttrs(cell))\r\n    }\r\n    return Buffer.concat([head, Buffer.from(opBytes)])\r\n  }\r\n\r\n  /**\r\n   * Cell color → palette byte. xterm 의 default (-1) 는 fallback (fg=7 white, bg=0 black).\r\n   * RGB mode (24-bit color) 는 spike v2 — 현재는 palette mode 만.\r\n   */\r\n  private normalizeColor(color: number, fallback: number): number {\r\n    if (color < 0) return fallback\r\n    return color & 0xff\r\n  }\r\n\r\n  /**\r\n   * xterm cell → attrs byte (spec §3-α bit field).\r\n   *\r\n   * cell 객체는 xterm/headless 내부 `IBufferCell` — `isBold()` / `isItalic()` 등 method 보유.\r\n   * 일부 method 가 미구현이면 try/catch 로 0 fallback.\r\n   */\r\n  private encodeAttrs(cell: import('@xterm/headless').IBufferCell): number {\r\n    let a = 0\r\n    try {\r\n      if (cell.isBold()) a |= 0x01\r\n      if (cell.isItalic()) a |= 0x02\r\n      if (cell.isUnderline()) a |= 0x04\r\n      if (cell.isInverse()) a |= 0x08\r\n    } catch {\r\n      // headless 의 IBufferCell 일부 method 미구현 — 0 fallback\r\n    }\r\n    return a\r\n  }\r\n}\r\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IColor, IColorRGB } from 'common/Types';\n\nlet $r = 0;\nlet $g = 0;\nlet $b = 0;\nlet $a = 0;\n\nexport const NULL_COLOR: IColor = {\n  css: '#00000000',\n  rgba: 0\n};\n\n/**\n * Helper functions where the source type is \"channels\" (individual color channels as numbers).\n */\nexport namespace channels {\n  export function toCss(r: number, g: number, b: number, a?: number): string {\n    if (a !== undefined) {\n      return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}${toPaddedHex(a)}`;\n    }\n    return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}`;\n  }\n\n  export function toRgba(r: number, g: number, b: number, a: number = 0xFF): number {\n    // Note: The aggregated number is RGBA32 (BE), thus needs to be converted to ABGR32\n    // on LE systems, before it can be used for direct 32-bit buffer writes.\n    // >>> 0 forces an unsigned int\n    return (r << 24 | g << 16 | b << 8 | a) >>> 0;\n  }\n\n  export function toColor(r: number, g: number, b: number, a?: number): IColor {\n    return {\n      css: channels.toCss(r, g, b, a),\n      rgba: channels.toRgba(r, g, b, a)\n    };\n  }\n}\n\n/**\n * Helper functions where the source type is `IColor`.\n */\nexport namespace color {\n  export function blend(bg: IColor, fg: IColor): IColor {\n    $a = (fg.rgba & 0xFF) / 255;\n    if ($a === 1) {\n      return {\n        css: fg.css,\n        rgba: fg.rgba\n      };\n    }\n    const fgR = (fg.rgba >> 24) & 0xFF;\n    const fgG = (fg.rgba >> 16) & 0xFF;\n    const fgB = (fg.rgba >> 8) & 0xFF;\n    const bgR = (bg.rgba >> 24) & 0xFF;\n    const bgG = (bg.rgba >> 16) & 0xFF;\n    const bgB = (bg.rgba >> 8) & 0xFF;\n    $r = bgR + Math.round((fgR - bgR) * $a);\n    $g = bgG + Math.round((fgG - bgG) * $a);\n    $b = bgB + Math.round((fgB - bgB) * $a);\n    const css = channels.toCss($r, $g, $b);\n    const rgba = channels.toRgba($r, $g, $b);\n    return { css, rgba };\n  }\n\n  export function isOpaque(color: IColor): boolean {\n    return (color.rgba & 0xFF) === 0xFF;\n  }\n\n  export function ensureContrastRatio(bg: IColor, fg: IColor, ratio: number): IColor | undefined {\n    const result = rgba.ensureContrastRatio(bg.rgba, fg.rgba, ratio);\n    if (!result) {\n      return undefined;\n    }\n    return channels.toColor(\n      (result >> 24 & 0xFF),\n      (result >> 16 & 0xFF),\n      (result >> 8  & 0xFF)\n    );\n  }\n\n  export function opaque(color: IColor): IColor {\n    const rgbaColor = (color.rgba | 0xFF) >>> 0;\n    [$r, $g, $b] = rgba.toChannels(rgbaColor);\n    return {\n      css: channels.toCss($r, $g, $b),\n      rgba: rgbaColor\n    };\n  }\n\n  export function opacity(color: IColor, opacity: number): IColor {\n    $a = Math.round(opacity * 0xFF);\n    [$r, $g, $b] = rgba.toChannels(color.rgba);\n    return {\n      css: channels.toCss($r, $g, $b, $a),\n      rgba: channels.toRgba($r, $g, $b, $a)\n    };\n  }\n\n  export function multiplyOpacity(color: IColor, factor: number): IColor {\n    $a = color.rgba & 0xFF;\n    return opacity(color, ($a * factor) / 0xFF);\n  }\n\n  export function toColorRGB(color: IColor): IColorRGB {\n    return [(color.rgba >> 24) & 0xFF, (color.rgba >> 16) & 0xFF, (color.rgba >> 8) & 0xFF];\n  }\n}\n\n/**\n * Helper functions where the source type is \"css\" (string: '#rgb', '#rgba', '#rrggbb',\n * '#rrggbbaa').\n */\nexport namespace css {\n  // Attempt to set get the shared canvas context\n  let $ctx: CanvasRenderingContext2D | undefined;\n  let $litmusColor: CanvasGradient | undefined;\n  try {\n    // This is guaranteed to run in the first window, so document should be correct\n    const canvas = document.createElement('canvas');\n    canvas.width = 1;\n    canvas.height = 1;\n    const ctx = canvas.getContext('2d', {\n      willReadFrequently: true\n    });\n    if (ctx) {\n      $ctx = ctx;\n      $ctx.globalCompositeOperation = 'copy';\n      $litmusColor = $ctx.createLinearGradient(0, 0, 1, 1);\n    }\n  }\n  catch {\n    // noop\n  }\n\n  /**\n   * Converts a css string to an IColor, this should handle all valid CSS color strings and will\n   * throw if it's invalid. The ideal format to use is `#rrggbb[aa]` as it's the fastest to parse.\n   *\n   * Only `#rgb[a]`, `#rrggbb[aa]`, `rgb()` and `rgba()` formats are supported when run in a Node\n   * environment.\n   */\n  export function toColor(css: string): IColor {\n    // Formats: #rgb[a] and #rrggbb[aa]\n    if (css.match(/#[\\da-f]{3,8}/i)) {\n      switch (css.length) {\n        case 4: { // #rgb\n          $r = parseInt(css.slice(1, 2).repeat(2), 16);\n          $g = parseInt(css.slice(2, 3).repeat(2), 16);\n          $b = parseInt(css.slice(3, 4).repeat(2), 16);\n          return channels.toColor($r, $g, $b);\n        }\n        case 5: { // #rgba\n          $r = parseInt(css.slice(1, 2).repeat(2), 16);\n          $g = parseInt(css.slice(2, 3).repeat(2), 16);\n          $b = parseInt(css.slice(3, 4).repeat(2), 16);\n          $a = parseInt(css.slice(4, 5).repeat(2), 16);\n          return channels.toColor($r, $g, $b, $a);\n        }\n        case 7: // #rrggbb\n          return {\n            css,\n            rgba: (parseInt(css.slice(1), 16) << 8 | 0xFF) >>> 0\n          };\n        case 9: // #rrggbbaa\n          return {\n            css,\n            rgba: parseInt(css.slice(1), 16) >>> 0\n          };\n      }\n    }\n\n    // Formats: rgb() or rgba()\n    const rgbaMatch = css.match(/rgba?\\(\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*(,\\s*(0|1|\\d?\\.(\\d+))\\s*)?\\)/);\n    if (rgbaMatch) {\n      $r = parseInt(rgbaMatch[1]);\n      $g = parseInt(rgbaMatch[2]);\n      $b = parseInt(rgbaMatch[3]);\n      $a = Math.round((rgbaMatch[5] === undefined ? 1 : parseFloat(rgbaMatch[5])) * 0xFF);\n      return channels.toColor($r, $g, $b, $a);\n    }\n\n    // Validate the context is available for canvas-based color parsing\n    if (!$ctx || !$litmusColor) {\n      throw new Error('css.toColor: Unsupported css format');\n    }\n\n    // Validate the color using canvas fillStyle\n    // See https://html.spec.whatwg.org/multipage/canvas.html#fill-and-stroke-styles\n    $ctx.fillStyle = $litmusColor;\n    $ctx.fillStyle = css;\n    if (typeof $ctx.fillStyle !== 'string') {\n      throw new Error('css.toColor: Unsupported css format');\n    }\n\n    $ctx.fillRect(0, 0, 1, 1);\n    [$r, $g, $b, $a] = $ctx.getImageData(0, 0, 1, 1).data;\n\n    // Validate the color is non-transparent as color hue gets lost when drawn to the canvas\n    if ($a !== 0xFF) {\n      throw new Error('css.toColor: Unsupported css format');\n    }\n\n    // Extract the color from the canvas' fillStyle property which exposes the color value in rgba()\n    // format\n    // See https://html.spec.whatwg.org/multipage/canvas.html#serialisation-of-a-color\n    return {\n      rgba: channels.toRgba($r, $g, $b, $a),\n      css\n    };\n  }\n}\n\n/**\n * Helper functions where the source type is \"rgb\" (number: 0xrrggbb).\n */\nexport namespace rgb {\n  /**\n   * Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio\n   * between two colors.\n   * @param rgb The color to use.\n   * @see https://www.w3.org/TR/WCAG20/#relativeluminancedef\n   */\n  export function relativeLuminance(rgb: number): number {\n    return relativeLuminance2(\n      (rgb >> 16) & 0xFF,\n      (rgb >> 8 ) & 0xFF,\n      (rgb      ) & 0xFF);\n  }\n\n  /**\n   * Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio\n   * between two colors.\n   * @param r The red channel (0x00 to 0xFF).\n   * @param g The green channel (0x00 to 0xFF).\n   * @param b The blue channel (0x00 to 0xFF).\n   * @see https://www.w3.org/TR/WCAG20/#relativeluminancedef\n   */\n  export function relativeLuminance2(r: number, g: number, b: number): number {\n    const rs = r / 255;\n    const gs = g / 255;\n    const bs = b / 255;\n    const rr = rs <= 0.03928 ? rs / 12.92 : Math.pow((rs + 0.055) / 1.055, 2.4);\n    const rg = gs <= 0.03928 ? gs / 12.92 : Math.pow((gs + 0.055) / 1.055, 2.4);\n    const rb = bs <= 0.03928 ? bs / 12.92 : Math.pow((bs + 0.055) / 1.055, 2.4);\n    return rr * 0.2126 + rg * 0.7152 + rb * 0.0722;\n  }\n}\n\n/**\n * Helper functions where the source type is \"rgba\" (number: 0xrrggbbaa).\n */\nexport namespace rgba {\n  export function blend(bg: number, fg: number): number {\n    $a = (fg & 0xFF) / 0xFF;\n    if ($a === 1) {\n      return fg;\n    }\n    const fgR = (fg >> 24) & 0xFF;\n    const fgG = (fg >> 16) & 0xFF;\n    const fgB = (fg >> 8) & 0xFF;\n    const bgR = (bg >> 24) & 0xFF;\n    const bgG = (bg >> 16) & 0xFF;\n    const bgB = (bg >> 8) & 0xFF;\n    $r = bgR + Math.round((fgR - bgR) * $a);\n    $g = bgG + Math.round((fgG - bgG) * $a);\n    $b = bgB + Math.round((fgB - bgB) * $a);\n    return channels.toRgba($r, $g, $b);\n  }\n\n  /**\n   * Given a foreground color and a background color, either increase or reduce the luminance of the\n   * foreground color until the specified contrast ratio is met. If pure white or black is hit\n   * without the contrast ratio being met, go the other direction using the background color as the\n   * foreground color and take either the first or second result depending on which has the higher\n   * contrast ratio.\n   *\n   * `undefined` will be returned if the contrast ratio is already met.\n   *\n   * @param bgRgba The background color in rgba format.\n   * @param fgRgba The foreground color in rgba format.\n   * @param ratio The contrast ratio to achieve.\n   */\n  export function ensureContrastRatio(bgRgba: number, fgRgba: number, ratio: number): number | undefined {\n    const bgL = rgb.relativeLuminance(bgRgba >> 8);\n    const fgL = rgb.relativeLuminance(fgRgba >> 8);\n    const cr = contrastRatio(bgL, fgL);\n    if (cr < ratio) {\n      if (fgL < bgL) {\n        const resultA = reduceLuminance(bgRgba, fgRgba, ratio);\n        const resultARatio = contrastRatio(bgL, rgb.relativeLuminance(resultA >> 8));\n        if (resultARatio < ratio) {\n          const resultB = increaseLuminance(bgRgba, fgRgba, ratio);\n          const resultBRatio = contrastRatio(bgL, rgb.relativeLuminance(resultB >> 8));\n          return resultARatio > resultBRatio ? resultA : resultB;\n        }\n        return resultA;\n      }\n      const resultA = increaseLuminance(bgRgba, fgRgba, ratio);\n      const resultARatio = contrastRatio(bgL, rgb.relativeLuminance(resultA >> 8));\n      if (resultARatio < ratio) {\n        const resultB = reduceLuminance(bgRgba, fgRgba, ratio);\n        const resultBRatio = contrastRatio(bgL, rgb.relativeLuminance(resultB >> 8));\n        return resultARatio > resultBRatio ? resultA : resultB;\n      }\n      return resultA;\n    }\n    return undefined;\n  }\n\n  export function reduceLuminance(bgRgba: number, fgRgba: number, ratio: number): number {\n    // This is a naive but fast approach to reducing luminance as converting to\n    // HSL and back is expensive\n    const bgR = (bgRgba >> 24) & 0xFF;\n    const bgG = (bgRgba >> 16) & 0xFF;\n    const bgB = (bgRgba >>  8) & 0xFF;\n    let fgR = (fgRgba >> 24) & 0xFF;\n    let fgG = (fgRgba >> 16) & 0xFF;\n    let fgB = (fgRgba >>  8) & 0xFF;\n    let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgG, fgB), rgb.relativeLuminance2(bgR, bgG, bgB));\n    while (cr < ratio && (fgR > 0 || fgG > 0 || fgB > 0)) {\n      // Reduce by 10% until the ratio is hit\n      fgR -= Math.max(0, Math.ceil(fgR * 0.1));\n      fgG -= Math.max(0, Math.ceil(fgG * 0.1));\n      fgB -= Math.max(0, Math.ceil(fgB * 0.1));\n      cr = contrastRatio(rgb.relativeLuminance2(fgR, fgG, fgB), rgb.relativeLuminance2(bgR, bgG, bgB));\n    }\n    return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;\n  }\n\n  export function increaseLuminance(bgRgba: number, fgRgba: number, ratio: number): number {\n    // This is a naive but fast approach to increasing luminance as converting to\n    // HSL and back is expensive\n    const bgR = (bgRgba >> 24) & 0xFF;\n    const bgG = (bgRgba >> 16) & 0xFF;\n    const bgB = (bgRgba >>  8) & 0xFF;\n    let fgR = (fgRgba >> 24) & 0xFF;\n    let fgG = (fgRgba >> 16) & 0xFF;\n    let fgB = (fgRgba >>  8) & 0xFF;\n    let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgG, fgB), rgb.relativeLuminance2(bgR, bgG, bgB));\n    while (cr < ratio && (fgR < 0xFF || fgG < 0xFF || fgB < 0xFF)) {\n      // Increase by 10% until the ratio is hit\n      fgR = Math.min(0xFF, fgR + Math.ceil((255 - fgR) * 0.1));\n      fgG = Math.min(0xFF, fgG + Math.ceil((255 - fgG) * 0.1));\n      fgB = Math.min(0xFF, fgB + Math.ceil((255 - fgB) * 0.1));\n      cr = contrastRatio(rgb.relativeLuminance2(fgR, fgG, fgB), rgb.relativeLuminance2(bgR, bgG, bgB));\n    }\n    return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;\n  }\n\n  export function toChannels(value: number): [number, number, number, number] {\n    return [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF];\n  }\n}\n\nexport function toPaddedHex(c: number): string {\n  const s = c.toString(16);\n  return s.length < 2 ? '0' + s : s;\n}\n\n/**\n * Gets the contrast ratio between two relative luminance values.\n * @param l1 The first relative luminance.\n * @param l2 The first relative luminance.\n * @see https://www.w3.org/TR/WCAG20/#contrast-ratiodef\n */\nexport function contrastRatio(l1: number, l2: number): number {\n  if (l1 < l2) {\n    return (l2 + 0.05) / (l1 + 0.05);\n  }\n  return (l1 + 0.05) / (l2 + 0.05);\n}\n","/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { CharData, IColor, ICoreTerminal, ITerminalOptions } from 'common/Types';\nimport { IBuffer } from 'common/buffer/Types';\nimport { IDisposable, Terminal as ITerminalApi } from '@xterm/xterm';\nimport { channels, css } from 'common/Color';\nimport type { Event } from 'vs/base/common/event';\n\n/**\n * A portion of the public API that are implemented identially internally and simply passed through.\n */\ntype InternalPassthroughApis = Omit<ITerminalApi, 'buffer' | 'parser' | 'unicode' | 'modes' | 'writeln' | 'loadAddon'>;\n\nexport interface ITerminal extends InternalPassthroughApis, ICoreTerminal {\n  screenElement: HTMLElement | undefined;\n  browser: IBrowser;\n  buffer: IBuffer;\n  linkifier: ILinkifier2 | undefined;\n  options: Required<ITerminalOptions>;\n\n  onBlur: Event<void>;\n  onFocus: Event<void>;\n  onA11yChar: Event<string>;\n  onA11yTab: Event<number>;\n  onWillOpen: Event<HTMLElement>;\n\n  cancel(ev: MouseEvent | WheelEvent | KeyboardEvent | InputEvent, force?: boolean): boolean | void;\n}\n\nexport type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;\nexport type CustomWheelEventHandler = (event: WheelEvent) => boolean;\n\nexport type LineData = CharData[];\n\nexport interface ICompositionHelper {\n  readonly isComposing: boolean;\n  compositionstart(): void;\n  compositionupdate(ev: CompositionEvent): void;\n  compositionend(): void;\n  updateCompositionElements(dontRecurse?: boolean): void;\n  keydown(ev: KeyboardEvent): boolean;\n}\n\nexport interface IBrowser {\n  isNode: boolean;\n  userAgent: string;\n  platform: string;\n  isFirefox: boolean;\n  isMac: boolean;\n  isIpad: boolean;\n  isIphone: boolean;\n  isWindows: boolean;\n}\n\nexport interface IColorSet {\n  foreground: IColor;\n  background: IColor;\n  cursor: IColor;\n  cursorAccent: IColor;\n  selectionForeground: IColor | undefined;\n  selectionBackgroundTransparent: IColor;\n  /** The selection blended on top of background. */\n  selectionBackgroundOpaque: IColor;\n  selectionInactiveBackgroundTransparent: IColor;\n  selectionInactiveBackgroundOpaque: IColor;\n  scrollbarSliderBackground: IColor;\n  scrollbarSliderHoverBackground: IColor;\n  scrollbarSliderActiveBackground: IColor;\n  overviewRulerBorder: IColor;\n  ansi: IColor[];\n  /** Maps original colors to colors that respect minimum contrast ratio. */\n  contrastCache: IColorContrastCache;\n  /** Maps original colors to colors that respect _half_ of the minimum contrast ratio. */\n  halfContrastCache: IColorContrastCache;\n}\n\nexport type ReadonlyColorSet = Readonly<Omit<IColorSet, 'ansi'>> & { ansi: Readonly<Pick<IColorSet, 'ansi'>['ansi']> };\n\nexport interface IColorContrastCache {\n  clear(): void;\n  setCss(bg: number, fg: number, value: string | null): void;\n  getCss(bg: number, fg: number): string | null | undefined;\n  setColor(bg: number, fg: number, value: IColor | null): void;\n  getColor(bg: number, fg: number): IColor | null | undefined;\n}\n\nexport interface IPartialColorSet {\n  foreground: IColor;\n  background: IColor;\n  cursor?: IColor;\n  cursorAccent?: IColor;\n  selectionBackground?: IColor;\n  ansi: IColor[];\n}\n\nexport interface IViewport extends IDisposable {\n  scrollBarWidth: number;\n  readonly onRequestScrollLines: Event<{ amount: number, suppressScrollEvent: boolean }>;\n  syncScrollArea(immediate?: boolean, force?: boolean): void;\n  getLinesScrolled(ev: WheelEvent): number;\n  getBufferElements(startLine: number, endLine?: number): { bufferElements: HTMLElement[], cursorElement?: HTMLElement };\n  handleWheel(ev: WheelEvent): boolean;\n  handleTouchStart(ev: TouchEvent): void;\n  handleTouchMove(ev: TouchEvent): boolean;\n  scrollLines(disp: number): void;  // todo api name?\n  reset(): void;\n}\n\nexport interface ILinkifierEvent {\n  x1: number;\n  y1: number;\n  x2: number;\n  y2: number;\n  cols: number;\n  fg: number | undefined;\n}\n\ninterface ILinkState {\n  decorations: ILinkDecorations;\n  isHovered: boolean;\n}\nexport interface ILinkWithState {\n  link: ILink;\n  state?: ILinkState;\n}\n\nexport interface ILinkifier2 extends IDisposable {\n  onShowLinkUnderline: Event<ILinkifierEvent>;\n  onHideLinkUnderline: Event<ILinkifierEvent>;\n  readonly currentLink: ILinkWithState | undefined;\n}\n\nexport interface ILink {\n  range: IBufferRange;\n  text: string;\n  decorations?: ILinkDecorations;\n  activate(event: MouseEvent, text: string): void;\n  hover?(event: MouseEvent, text: string): void;\n  leave?(event: MouseEvent, text: string): void;\n  dispose?(): void;\n}\n\nexport interface ILinkDecorations {\n  pointerCursor: boolean;\n  underline: boolean;\n}\n\nexport interface IBufferRange {\n  start: IBufferCellPosition;\n  end: IBufferCellPosition;\n}\n\nexport interface IBufferCellPosition {\n  x: number;\n  y: number;\n}\n\nexport type CharacterJoinerHandler = (text: string) => [number, number][];\n\nexport interface ICharacterJoiner {\n  id: number;\n  handler: CharacterJoinerHandler;\n}\n\nexport interface IRenderDebouncer extends IDisposable {\n  refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void;\n}\n\nexport interface IRenderDebouncerWithCallback extends IRenderDebouncer {\n  addRefreshCallback(callback: FrameRequestCallback): number;\n}\n\nexport interface IBufferElementProvider {\n  provideBufferElements(): DocumentFragment | HTMLElement;\n}\n\n// An IIFE to generate DEFAULT_ANSI_COLORS.\nexport const DEFAULT_ANSI_COLORS = Object.freeze((() => {\n  const colors = [\n    // dark:\n    css.toColor('#2e3436'),\n    css.toColor('#cc0000'),\n    css.toColor('#4e9a06'),\n    css.toColor('#c4a000'),\n    css.toColor('#3465a4'),\n    css.toColor('#75507b'),\n    css.toColor('#06989a'),\n    css.toColor('#d3d7cf'),\n    // bright:\n    css.toColor('#555753'),\n    css.toColor('#ef2929'),\n    css.toColor('#8ae234'),\n    css.toColor('#fce94f'),\n    css.toColor('#729fcf'),\n    css.toColor('#ad7fa8'),\n    css.toColor('#34e2e2'),\n    css.toColor('#eeeeec')\n  ];\n\n  // Fill in the remaining 240 ANSI colors.\n  // Generate colors (16-231)\n  const v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff];\n  for (let i = 0; i < 216; i++) {\n    const r = v[(i / 36) % 6 | 0];\n    const g = v[(i / 6) % 6 | 0];\n    const b = v[i % 6];\n    colors.push({\n      css: channels.toCss(r, g, b),\n      rgba: channels.toRgba(r, g, b)\n    });\n  }\n\n  // Generate greys (232-255)\n  for (let i = 0; i < 24; i++) {\n    const c = 8 + i * 10;\n    colors.push({\n      css: channels.toCss(c, c, c),\n      rgba: channels.toRgba(c, c, c)\n    });\n  }\n\n  return colors;\n})());\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * (EXPERIMENTAL) This Addon is still under development\n */\n\nimport type { IBuffer, IBufferCell, IBufferRange, ITerminalAddon, Terminal } from '@xterm/xterm';\nimport type { IHTMLSerializeOptions, SerializeAddon as ISerializeApi, ISerializeOptions, ISerializeRange } from '@xterm/addon-serialize';\nimport { IAttributeData, IColor } from 'common/Types';\nimport { DEFAULT_ANSI_COLORS } from 'browser/Types';\n\nfunction constrain(value: number, low: number, high: number): number {\n  return Math.max(low, Math.min(value, high));\n}\n\nfunction escapeHTMLChar(c: string): string {\n  switch (c) {\n    case '&': return '&amp;';\n    case '<': return '&lt;';\n  }\n  return c;\n}\n\n// TODO: Refine this template class later\nabstract class BaseSerializeHandler {\n  constructor(\n    protected readonly _buffer: IBuffer\n  ) {\n  }\n\n  public serialize(range: IBufferRange, excludeFinalCursorPosition?: boolean): string {\n    // we need two of them to flip between old and new cell\n    const cell1 = this._buffer.getNullCell();\n    const cell2 = this._buffer.getNullCell();\n    let oldCell = cell1;\n\n    const startRow = range.start.y;\n    const endRow = range.end.y;\n    const startColumn = range.start.x;\n    const endColumn = range.end.x;\n\n    this._beforeSerialize(endRow - startRow, startRow, endRow);\n\n    for (let row = startRow; row <= endRow; row++) {\n      const line = this._buffer.getLine(row);\n      if (line) {\n        const startLineColumn = row === range.start.y ? startColumn : 0;\n        const endLineColumn = row === range.end.y ? endColumn: line.length;\n        for (let col = startLineColumn; col < endLineColumn; col++) {\n          const c = line.getCell(col, oldCell === cell1 ? cell2 : cell1);\n          if (!c) {\n            console.warn(`Can't get cell at row=${row}, col=${col}`);\n            continue;\n          }\n          this._nextCell(c, oldCell, row, col);\n          oldCell = c;\n        }\n      }\n      this._rowEnd(row, row === endRow);\n    }\n\n    this._afterSerialize();\n\n    return this._serializeString(excludeFinalCursorPosition);\n  }\n\n  protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { }\n  protected _rowEnd(row: number, isLastRow: boolean): void { }\n  protected _beforeSerialize(rows: number, startRow: number, endRow: number): void { }\n  protected _afterSerialize(): void { }\n  protected _serializeString(excludeFinalCursorPosition?: boolean): string { return ''; }\n}\n\nfunction equalFg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {\n  return cell1.getFgColorMode() === cell2.getFgColorMode()\n    && cell1.getFgColor() === cell2.getFgColor();\n}\n\nfunction equalBg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {\n  return cell1.getBgColorMode() === cell2.getBgColorMode()\n    && cell1.getBgColor() === cell2.getBgColor();\n}\n\nfunction equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {\n  return cell1.isInverse() === cell2.isInverse()\n    && cell1.isBold() === cell2.isBold()\n    && cell1.isUnderline() === cell2.isUnderline()\n    && cell1.isOverline() === cell2.isOverline()\n    && cell1.isBlink() === cell2.isBlink()\n    && cell1.isInvisible() === cell2.isInvisible()\n    && cell1.isItalic() === cell2.isItalic()\n    && cell1.isDim() === cell2.isDim()\n    && cell1.isStrikethrough() === cell2.isStrikethrough();\n}\n\nclass StringSerializeHandler extends BaseSerializeHandler {\n  private _rowIndex: number = 0;\n  private _allRows: string[] = new Array<string>();\n  private _allRowSeparators: string[] = new Array<string>();\n  private _currentRow: string = '';\n  private _nullCellCount: number = 0;\n\n  // we can see a full colored cell and a null cell that only have background the same style\n  // but the information isn't preserved by null cell itself\n  // so wee need to record it when required.\n  private _cursorStyle: IBufferCell = this._buffer.getNullCell();\n\n  // where exact the cursor styles comes from\n  // because we can't copy the cell directly\n  // so we remember where the content comes from instead\n  private _cursorStyleRow: number = 0;\n  private _cursorStyleCol: number = 0;\n\n  // this is a null cell for reference for checking whether background is empty or not\n  private _backgroundCell: IBufferCell = this._buffer.getNullCell();\n\n  private _firstRow: number = 0;\n  private _lastCursorRow: number = 0;\n  private _lastCursorCol: number = 0;\n  private _lastContentCursorRow: number = 0;\n  private _lastContentCursorCol: number = 0;\n\n  constructor(\n    buffer: IBuffer,\n    private readonly _terminal: Terminal\n  ) {\n    super(buffer);\n  }\n\n  protected _beforeSerialize(rows: number, start: number, end: number): void {\n    this._allRows = new Array<string>(rows);\n    this._lastContentCursorRow = start;\n    this._lastCursorRow = start;\n    this._firstRow = start;\n  }\n\n  private _thisRowLastChar: IBufferCell = this._buffer.getNullCell();\n  private _thisRowLastSecondChar: IBufferCell = this._buffer.getNullCell();\n  private _nextRowFirstChar: IBufferCell = this._buffer.getNullCell();\n  protected _rowEnd(row: number, isLastRow: boolean): void {\n    // if there is colorful empty cell at line end, whe must pad it back, or the the color block\n    // will missing\n    if (this._nullCellCount > 0 && !equalBg(this._cursorStyle, this._backgroundCell)) {\n      // use clear right to set background.\n      this._currentRow += `\\u001b[${this._nullCellCount}X`;\n    }\n\n    let rowSeparator = '';\n\n    // handle row separator\n    if (!isLastRow) {\n      // Enable BCE\n      if (row - this._firstRow >= this._terminal.rows) {\n        this._buffer.getLine(this._cursorStyleRow)?.getCell(this._cursorStyleCol, this._backgroundCell);\n      }\n\n      // Fetch current line\n      const currentLine = this._buffer.getLine(row)!;\n      // Fetch next line\n      const nextLine = this._buffer.getLine(row + 1)!;\n\n      if (!nextLine.isWrapped) {\n        // just insert the line break\n        rowSeparator = '\\r\\n';\n        // we sended the enter\n        this._lastCursorRow = row + 1;\n        this._lastCursorCol = 0;\n      } else {\n        rowSeparator = '';\n        const thisRowLastChar = currentLine.getCell(currentLine.length - 1, this._thisRowLastChar)!;\n        const thisRowLastSecondChar = currentLine.getCell(currentLine.length - 2, this._thisRowLastSecondChar)!;\n        const nextRowFirstChar = nextLine.getCell(0, this._nextRowFirstChar)!;\n        const isNextRowFirstCharDoubleWidth = nextRowFirstChar.getWidth() > 1;\n\n        // validate whether this line wrap is ever possible\n        // which mean whether cursor can placed at a overflow position (x === row) naturally\n        let isValid = false;\n\n        if (\n          // you must output character to cause overflow, control sequence can't do this\n          nextRowFirstChar.getChars() &&\n            isNextRowFirstCharDoubleWidth ? this._nullCellCount <= 1 : this._nullCellCount <= 0\n        ) {\n          if (\n            // the last character can't be null,\n            // you can't use control sequence to move cursor to (x === row)\n            (thisRowLastChar.getChars() || thisRowLastChar.getWidth() === 0) &&\n            // change background of the first wrapped cell also affects BCE\n            // so we mark it as invalid to simply the process to determine line separator\n            equalBg(thisRowLastChar, nextRowFirstChar)\n          ) {\n            isValid = true;\n          }\n\n          if (\n            // the second to last character can't be null if the next line starts with CJK,\n            // you can't use control sequence to move cursor to (x === row)\n            isNextRowFirstCharDoubleWidth &&\n            (thisRowLastSecondChar.getChars() || thisRowLastSecondChar.getWidth() === 0) &&\n            // change background of the first wrapped cell also affects BCE\n            // so we mark it as invalid to simply the process to determine line separator\n            equalBg(thisRowLastChar, nextRowFirstChar) &&\n            equalBg(thisRowLastSecondChar, nextRowFirstChar)\n          ) {\n            isValid = true;\n          }\n        }\n\n        if (!isValid) {\n          // force the wrap with magic\n          // insert enough character to force the wrap\n          rowSeparator = '-'.repeat(this._nullCellCount + 1);\n          // move back and erase next line head\n          rowSeparator += '\\u001b[1D\\u001b[1X';\n\n          if (this._nullCellCount > 0) {\n            // do these because we filled the last several null slot, which we shouldn't\n            rowSeparator += '\\u001b[A';\n            rowSeparator += `\\u001b[${currentLine.length - this._nullCellCount}C`;\n            rowSeparator += `\\u001b[${this._nullCellCount}X`;\n            rowSeparator += `\\u001b[${currentLine.length - this._nullCellCount}D`;\n            rowSeparator += '\\u001b[B';\n          }\n\n          // This is content and need the be serialized even it is invisible.\n          // without this, wrap will be missing from outputs.\n          this._lastContentCursorRow = row + 1;\n          this._lastContentCursorCol = 0;\n\n          // force commit the cursor position\n          this._lastCursorRow = row + 1;\n          this._lastCursorCol = 0;\n        }\n      }\n    }\n\n    this._allRows[this._rowIndex] = this._currentRow;\n    this._allRowSeparators[this._rowIndex++] = rowSeparator;\n    this._currentRow = '';\n    this._nullCellCount = 0;\n  }\n\n  private _diffStyle(cell: IBufferCell | IAttributeData, oldCell: IBufferCell): number[] {\n    const sgrSeq: number[] = [];\n    const fgChanged = !equalFg(cell, oldCell);\n    const bgChanged = !equalBg(cell, oldCell);\n    const flagsChanged = !equalFlags(cell, oldCell);\n\n    if (fgChanged || bgChanged || flagsChanged) {\n      if (cell.isAttributeDefault()) {\n        if (!oldCell.isAttributeDefault()) {\n          sgrSeq.push(0);\n        }\n      } else {\n        if (fgChanged) {\n          const color = cell.getFgColor();\n          if (cell.isFgRGB()) { sgrSeq.push(38, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); }\n          else if (cell.isFgPalette()) {\n            if (color >= 16) { sgrSeq.push(38, 5, color); }\n            else { sgrSeq.push(color & 8 ? 90 + (color & 7) : 30 + (color & 7)); }\n          }\n          else { sgrSeq.push(39); }\n        }\n        if (bgChanged) {\n          const color = cell.getBgColor();\n          if (cell.isBgRGB()) { sgrSeq.push(48, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); }\n          else if (cell.isBgPalette()) {\n            if (color >= 16) { sgrSeq.push(48, 5, color); }\n            else { sgrSeq.push(color & 8 ? 100 + (color & 7) : 40 + (color & 7)); }\n          }\n          else { sgrSeq.push(49); }\n        }\n        if (flagsChanged) {\n          if (cell.isInverse() !== oldCell.isInverse()) { sgrSeq.push(cell.isInverse() ? 7 : 27); }\n          if (cell.isBold() !== oldCell.isBold()) { sgrSeq.push(cell.isBold() ? 1 : 22); }\n          if (cell.isUnderline() !== oldCell.isUnderline()) { sgrSeq.push(cell.isUnderline() ? 4 : 24); }\n          if (cell.isOverline() !== oldCell.isOverline()) { sgrSeq.push(cell.isOverline() ? 53 : 55); }\n          if (cell.isBlink() !== oldCell.isBlink()) { sgrSeq.push(cell.isBlink() ? 5 : 25); }\n          if (cell.isInvisible() !== oldCell.isInvisible()) { sgrSeq.push(cell.isInvisible() ? 8 : 28); }\n          if (cell.isItalic() !== oldCell.isItalic()) { sgrSeq.push(cell.isItalic() ? 3 : 23); }\n          if (cell.isDim() !== oldCell.isDim()) { sgrSeq.push(cell.isDim() ? 2 : 22); }\n          if (cell.isStrikethrough() !== oldCell.isStrikethrough()) { sgrSeq.push(cell.isStrikethrough() ? 9 : 29); }\n        }\n      }\n    }\n\n    return sgrSeq;\n  }\n\n  protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void {\n    // a width 0 cell don't need to be count because it is just a placeholder after a CJK character;\n    const isPlaceHolderCell = cell.getWidth() === 0;\n\n    if (isPlaceHolderCell) {\n      return;\n    }\n\n    // this cell don't have content\n    const isEmptyCell = cell.getChars() === '';\n\n    const sgrSeq = this._diffStyle(cell, this._cursorStyle);\n\n    // the empty cell style is only assumed to be changed when background changed, because\n    // foreground is always 0.\n    const styleChanged = isEmptyCell ? !equalBg(this._cursorStyle, cell) : sgrSeq.length > 0;\n\n    /**\n     *  handles style change\n     */\n    if (styleChanged) {\n      // before update the style, we need to fill empty cell back\n      if (this._nullCellCount > 0) {\n        // use clear right to set background.\n        if (!equalBg(this._cursorStyle, this._backgroundCell)) {\n          this._currentRow += `\\u001b[${this._nullCellCount}X`;\n        }\n        // use move right to move cursor.\n        this._currentRow += `\\u001b[${this._nullCellCount}C`;\n        this._nullCellCount = 0;\n      }\n\n      this._lastContentCursorRow = this._lastCursorRow = row;\n      this._lastContentCursorCol = this._lastCursorCol = col;\n\n      this._currentRow += `\\u001b[${sgrSeq.join(';')}m`;\n\n      // update the last cursor style\n      const line = this._buffer.getLine(row);\n      if (line !== undefined) {\n        line.getCell(col, this._cursorStyle);\n        this._cursorStyleRow = row;\n        this._cursorStyleCol = col;\n      }\n    }\n\n    /**\n     *  handles actual content\n     */\n    if (isEmptyCell) {\n      this._nullCellCount += cell.getWidth();\n    } else {\n      if (this._nullCellCount > 0) {\n        // we can just assume we have same style with previous one here\n        // because style change is handled by previous stage\n        // use move right when background is empty, use clear right when there is background.\n        if (equalBg(this._cursorStyle, this._backgroundCell)) {\n          this._currentRow += `\\u001b[${this._nullCellCount}C`;\n        } else {\n          this._currentRow += `\\u001b[${this._nullCellCount}X`;\n          this._currentRow += `\\u001b[${this._nullCellCount}C`;\n        }\n        this._nullCellCount = 0;\n      }\n\n      this._currentRow += cell.getChars();\n\n      // update cursor\n      this._lastContentCursorRow = this._lastCursorRow = row;\n      this._lastContentCursorCol = this._lastCursorCol = col + cell.getWidth();\n    }\n  }\n\n  protected _serializeString(excludeFinalCursorPosition: boolean): string {\n    let rowEnd = this._allRows.length;\n\n    // the fixup is only required for data without scrollback\n    // because it will always be placed at last line otherwise\n    if (this._buffer.length - this._firstRow <= this._terminal.rows) {\n      rowEnd = this._lastContentCursorRow + 1 - this._firstRow;\n      this._lastCursorCol = this._lastContentCursorCol;\n      this._lastCursorRow = this._lastContentCursorRow;\n    }\n\n    let content = '';\n\n    for (let i = 0; i < rowEnd; i++) {\n      content += this._allRows[i];\n      if (i + 1 < rowEnd) {\n        content += this._allRowSeparators[i];\n      }\n    }\n\n    // restore the cursor\n    if (!excludeFinalCursorPosition) {\n      const realCursorRow = this._buffer.baseY + this._buffer.cursorY;\n      const realCursorCol = this._buffer.cursorX;\n\n      const cursorMoved = (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol);\n\n      const moveRight = (offset: number): void => {\n        if (offset > 0) {\n          content += `\\u001b[${offset}C`;\n        } else if (offset < 0) {\n          content += `\\u001b[${-offset}D`;\n        }\n      };\n      const moveDown = (offset: number): void => {\n        if (offset > 0) {\n          content += `\\u001b[${offset}B`;\n        } else if (offset < 0) {\n          content += `\\u001b[${-offset}A`;\n        }\n      };\n\n      if (cursorMoved) {\n        moveDown(realCursorRow - this._lastCursorRow);\n        moveRight(realCursorCol - this._lastCursorCol);\n      }\n    }\n\n    // Restore the cursor's current style, see https://github.com/xtermjs/xterm.js/issues/3677\n    // HACK: Internal API access since it's awkward to expose this in the API and serialize will\n    // likely be the only consumer\n    const curAttrData: IAttributeData = (this._terminal as any)._core._inputHandler._curAttrData;\n    const sgrSeq = this._diffStyle(curAttrData, this._cursorStyle);\n    if (sgrSeq.length > 0) {\n      content += `\\u001b[${sgrSeq.join(';')}m`;\n    }\n\n    return content;\n  }\n}\n\nexport class SerializeAddon implements ITerminalAddon , ISerializeApi {\n  private _terminal: Terminal | undefined;\n\n  public activate(terminal: Terminal): void {\n    this._terminal = terminal;\n  }\n\n  private _serializeBufferByScrollback(terminal: Terminal, buffer: IBuffer, scrollback?: number): string {\n    const maxRows = buffer.length;\n    const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows);\n    return this._serializeBufferByRange(terminal, buffer, {\n      start: maxRows - correctRows,\n      end: maxRows - 1\n    }, false);\n  }\n\n  private _serializeBufferByRange(terminal: Terminal, buffer: IBuffer, range: ISerializeRange, excludeFinalCursorPosition: boolean): string {\n    const handler = new StringSerializeHandler(buffer, terminal);\n    return handler.serialize({\n      start: { x: 0,             y: typeof range.start === 'number' ? range.start : range.start.line },\n      end:   { x: terminal.cols, y: typeof range.end   === 'number' ? range.end   : range.end.line   }\n    }, excludeFinalCursorPosition);\n  }\n\n  private _serializeBufferAsHTML(terminal: Terminal, options: Partial<IHTMLSerializeOptions>): string {\n    const buffer = terminal.buffer.active;\n    const handler = new HTMLSerializeHandler(buffer, terminal, options);\n    const onlySelection = options.onlySelection ?? false;\n    const range = options.range;\n    if (range) {\n      return handler.serialize({\n        start: { x: range.startCol,             y: typeof range.startLine === 'number' ? range.startLine : range.startLine },\n        end:   { x: terminal.cols, y: typeof range.endLine   === 'number' ? range.endLine   : range.endLine   }\n      });\n    }\n    if (!onlySelection) {\n      const maxRows = buffer.length;\n      const scrollback = options.scrollback;\n      const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows);\n      return handler.serialize({\n        start: { x: 0,             y: maxRows - correctRows },\n        end:   { x: terminal.cols, y: maxRows - 1           }\n      });\n    }\n\n    const selection = this._terminal?.getSelectionPosition();\n    if (selection !== undefined) {\n      return handler.serialize({\n        start: { x: selection.start.x, y: selection.start.y },\n        end:   { x: selection.end.x,   y: selection.end.y   }\n      });\n    }\n\n    return '';\n  }\n\n  private _serializeModes(terminal: Terminal): string {\n    let content = '';\n    const modes = terminal.modes;\n\n    // Default: false\n    if (modes.applicationCursorKeysMode) content += '\\x1b[?1h';\n    if (modes.applicationKeypadMode) content += '\\x1b[?66h';\n    if (modes.bracketedPasteMode) content += '\\x1b[?2004h';\n    if (modes.insertMode) content += '\\x1b[4h';\n    if (modes.originMode) content += '\\x1b[?6h';\n    if (modes.reverseWraparoundMode) content += '\\x1b[?45h';\n    if (modes.sendFocusMode) content += '\\x1b[?1004h';\n    // synchronizedOutputMode doesn't need to be serialized as it's a temporary mode\n\n    // Default: true\n    if (modes.wraparoundMode === false) content += '\\x1b[?7l';\n\n    // Default: 'none'\n    if (modes.mouseTrackingMode !== 'none') {\n      switch (modes.mouseTrackingMode) {\n        case 'x10': content += '\\x1b[?9h'; break;\n        case 'vt200': content += '\\x1b[?1000h'; break;\n        case 'drag': content += '\\x1b[?1002h'; break;\n        case 'any': content += '\\x1b[?1003h'; break;\n      }\n    }\n\n    return content;\n  }\n\n  public serialize(options?: ISerializeOptions): string {\n    // TODO: Add combinedData support\n    if (!this._terminal) {\n      throw new Error('Cannot use addon until it has been loaded');\n    }\n\n    // Normal buffer\n    let content = options?.range\n      ? this._serializeBufferByRange(this._terminal, this._terminal.buffer.normal, options.range, true)\n      : this._serializeBufferByScrollback(this._terminal, this._terminal.buffer.normal, options?.scrollback);\n\n    // Alternate buffer\n    if (!options?.excludeAltBuffer) {\n      if (this._terminal.buffer.active.type === 'alternate') {\n        const alternativeScreenContent = this._serializeBufferByScrollback(this._terminal, this._terminal.buffer.alternate, undefined);\n        content += `\\u001b[?1049h\\u001b[H${alternativeScreenContent}`;\n      }\n    }\n\n    // Modes\n    if (!options?.excludeModes) {\n      content += this._serializeModes(this._terminal);\n    }\n\n    return content;\n  }\n\n  public serializeAsHTML(options?: Partial<IHTMLSerializeOptions>): string {\n    if (!this._terminal) {\n      throw new Error('Cannot use addon until it has been loaded');\n    }\n\n    return this._serializeBufferAsHTML(this._terminal, options || {});\n  }\n\n  public dispose(): void { }\n}\n\nexport class HTMLSerializeHandler extends BaseSerializeHandler {\n  private _currentRow: string = '';\n\n  private _htmlContent = '';\n\n  private _ansiColors: Readonly<IColor[]>;\n\n  constructor(\n    buffer: IBuffer,\n    private readonly _terminal: Terminal,\n    private readonly _options: Partial<IHTMLSerializeOptions>\n  ) {\n    super(buffer);\n\n    // For xterm headless: fallback to ansi colors\n    if ((_terminal as any)._core._themeService) {\n      this._ansiColors = (_terminal as any)._core._themeService.colors.ansi;\n    }\n    else {\n      this._ansiColors = DEFAULT_ANSI_COLORS;\n    }\n  }\n\n  private _padStart(target: string, targetLength: number, padString: string): string {\n    targetLength = targetLength >> 0;\n    padString = padString ?? ' ';\n    if (target.length > targetLength) {\n      return target;\n    }\n\n    targetLength -= target.length;\n    if (targetLength > padString.length) {\n      padString += padString.repeat(targetLength / padString.length);\n    }\n    return padString.slice(0, targetLength) + target;\n  }\n\n  protected _beforeSerialize(rows: number, start: number, end: number): void {\n    this._htmlContent += '<html><body><!--StartFragment--><pre>';\n\n    let foreground = '#000000';\n    let background = '#ffffff';\n    if (this._options.includeGlobalBackground ?? false) {\n      foreground = this._terminal.options.theme?.foreground ?? '#ffffff';\n      background = this._terminal.options.theme?.background ?? '#000000';\n    }\n\n    const globalStyleDefinitions = [];\n    globalStyleDefinitions.push('color: ' + foreground + ';');\n    globalStyleDefinitions.push('background-color: ' + background + ';');\n    globalStyleDefinitions.push('font-family: ' + this._terminal.options.fontFamily + ';');\n    globalStyleDefinitions.push('font-size: ' + this._terminal.options.fontSize + 'px;');\n    this._htmlContent += '<div style=\\'' + globalStyleDefinitions.join(' ') + '\\'>';\n  }\n\n  protected _afterSerialize(): void {\n    this._htmlContent += '</div>';\n    this._htmlContent += '</pre><!--EndFragment--></body></html>';\n  }\n\n  protected _rowEnd(row: number, isLastRow: boolean): void {\n    this._htmlContent += '<div><span>' + this._currentRow + '</span></div>';\n    this._currentRow = '';\n  }\n\n  private _getHexColor(cell: IBufferCell, isFg: boolean): string | undefined {\n    const color = isFg ? cell.getFgColor() : cell.getBgColor();\n    if (isFg ? cell.isFgRGB() : cell.isBgRGB()) {\n      const rgb = [\n        (color >> 16) & 255,\n        (color >>  8) & 255,\n        (color      ) & 255\n      ];\n      return '#' + rgb.map(x => this._padStart(x.toString(16), 2, '0')).join('');\n    }\n    if (isFg ? cell.isFgPalette() : cell.isBgPalette()) {\n      return this._ansiColors[color].css;\n    }\n    return undefined;\n  }\n\n  private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): string[] | undefined {\n    const content: string[] = [];\n\n    const fgChanged = !equalFg(cell, oldCell);\n    const bgChanged = !equalBg(cell, oldCell);\n    const flagsChanged = !equalFlags(cell, oldCell);\n\n    if (fgChanged || bgChanged || flagsChanged) {\n      const fgHexColor = this._getHexColor(cell, true);\n      if (fgHexColor) {\n        content.push('color: ' + fgHexColor + ';');\n      }\n\n      const bgHexColor = this._getHexColor(cell, false);\n      if (bgHexColor) {\n        content.push('background-color: ' + bgHexColor + ';');\n      }\n\n      if (cell.isInverse()) { content.push('color: #000000; background-color: #BFBFBF;'); }\n      if (cell.isBold()) { content.push('font-weight: bold;'); }\n      if (cell.isUnderline() && cell.isOverline()) { content.push('text-decoration: overline underline;'); }\n      else if (cell.isUnderline()) { content.push('text-decoration: underline;'); }\n      else if (cell.isOverline()) { content.push('text-decoration: overline;'); }\n      if (cell.isBlink()) { content.push('text-decoration: blink;'); }\n      if (cell.isInvisible()) { content.push('visibility: hidden;'); }\n      if (cell.isItalic()) { content.push('font-style: italic;'); }\n      if (cell.isDim()) { content.push('opacity: 0.5;'); }\n      if (cell.isStrikethrough()) { content.push('text-decoration: line-through;'); }\n\n      return content;\n    }\n\n    return undefined;\n  }\n\n  protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void {\n    // a width 0 cell don't need to be count because it is just a placeholder after a CJK character;\n    const isPlaceHolderCell = cell.getWidth() === 0;\n    if (isPlaceHolderCell) {\n      return;\n    }\n\n    // this cell don't have content\n    const isEmptyCell = cell.getChars() === '';\n\n    const styleDefinitions = this._diffStyle(cell, oldCell);\n\n    // handles style change\n    if (styleDefinitions) {\n      this._currentRow += styleDefinitions.length === 0 ?\n        '</span><span>' :\n        '</span><span style=\\'' + styleDefinitions.join(' ') + '\\'>';\n    }\n\n    // handles actual content\n    if (isEmptyCell) {\n      this._currentRow += ' ';\n    } else {\n      this._currentRow += escapeHTMLChar(cell.getChars());\n    }\n  }\n\n  protected _serializeString(): string {\n    return this._htmlContent;\n  }\n}\n","/**\r\n * Binary protocol v3 — opcode 상수 + parser / encoder helpers (desktop).\r\n *\r\n * Spec single source of truth: claude_code_mobile/docs/specs/binary-protocol-v3.md.\r\n * relay (nest-nexus/src/relay/binary-protocol.ts) + mobile (claude_code_mobile/\r\n * lib/services/relay_binary_dispatcher.dart) 와 fixture-driven contract test 로\r\n * wire 호환을 영구 검증.\r\n *\r\n * Desktop 의 역할:\r\n *  - 송신: PTY stdout → PTY_DATA / 응용 메시지 → APP_JSON. relay 가 그대로 forward.\r\n *  - 수신: mobile → desktop binary frame.\r\n *    - APP_JSON → 기존 message handler (terminal_input, session_attach 등)\r\n *    - RESIZE → pty.resize fire-and-forget (RFC 4254 §6.7)\r\n *    - PROTOCOL_ERROR → close + log\r\n */\r\n\r\nimport { PROTOCOL_VERSION } from '@arva/shared/constants'\r\n\r\nexport const OPCODE = {\r\n  PTY_DATA: 0x00,\r\n  APP_JSON: 0x01,\r\n  RESIZE: 0x02,\r\n  FRAME_SNAPSHOT: 0x03,\r\n  GRID_SNAPSHOT: 0x04,\r\n  GRID_DIFF: 0x05,\r\n  PROTOCOL_ERROR: 0xff,\r\n} as const\r\n\r\nexport const PROTOCOL_ERROR_REASON = {\r\n  UNKNOWN_OPCODE: 0x01,\r\n  MALFORMED_PAYLOAD: 0x02,\r\n  VERSION_MISMATCH: 0x03,\r\n  UNAUTHORIZED: 0x04,\r\n} as const\r\n\r\nexport type ParsedFrame =\r\n  | { type: 'pty_data'; text: string }\r\n  | { type: 'app_json'; json: Record<string, unknown> }\r\n  | { type: 'resize'; cols: number; rows: number }\r\n  | { type: 'frame_snapshot'; machineId: string; ansi: string }\r\n  | {\r\n      type: 'grid_snapshot'\r\n      seq: number\r\n      cols: number\r\n      rows: number\r\n      cells: Buffer\r\n      cursorR: number\r\n      cursorC: number\r\n    }\r\n  | { type: 'grid_diff'; seq: number; baseSeq: number; ops: Buffer }\r\n  | { type: 'protocol_error'; reason: number }\r\n\r\nexport class FrameParseError extends Error {\r\n  /** PROTOCOL_ERROR 송신 시 reason byte. */\r\n  readonly reason: number\r\n  constructor(message: string, reason: number) {\r\n    super(message)\r\n    this.reason = reason\r\n  }\r\n}\r\n\r\n/**\r\n * binary frame 1개를 [ParsedFrame] 으로 변환. malformed / unknown opcode → throw.\r\n */\r\nexport function parseFrame(buffer: Buffer): ParsedFrame {\r\n  if (buffer.length === 0) {\r\n    throw new FrameParseError(\r\n      'empty frame',\r\n      PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD,\r\n    )\r\n  }\r\n  const opcode = buffer.readUInt8(0)\r\n  switch (opcode) {\r\n    case OPCODE.PTY_DATA: {\r\n      // desktop 측은 PTY_DATA 수신 안 함 (host → mobile 단방향). 단 fixture-driven\r\n      // contract test 에서 동일 parse 가능해야 함.\r\n      const text = buffer.toString('utf-8', 1)\r\n      return { type: 'pty_data', text }\r\n    }\r\n    case OPCODE.APP_JSON: {\r\n      const text = buffer.toString('utf-8', 1)\r\n      let parsed: unknown\r\n      try {\r\n        parsed = JSON.parse(text)\r\n      } catch (err) {\r\n        throw new FrameParseError(\r\n          `APP_JSON parse failed: ${(err as Error).message}`,\r\n          PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD,\r\n        )\r\n      }\r\n      if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\r\n        throw new FrameParseError(\r\n          'APP_JSON not a JSON object',\r\n          PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD,\r\n        )\r\n      }\r\n      return { type: 'app_json', json: parsed as Record<string, unknown> }\r\n    }\r\n    case OPCODE.RESIZE: {\r\n      if (buffer.length < 9) {\r\n        throw new FrameParseError(\r\n          'RESIZE payload < 8 bytes',\r\n          PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD,\r\n        )\r\n      }\r\n      const cols = buffer.readUInt32BE(1)\r\n      const rows = buffer.readUInt32BE(5)\r\n      return { type: 'resize', cols, rows }\r\n    }\r\n    case OPCODE.FRAME_SNAPSHOT: {\r\n      // desktop 측은 FRAME_SNAPSHOT 수신 안 함 (relay → mobile 만). contract 호환.\r\n      if (buffer.length < 5) {\r\n        throw new FrameParseError(\r\n          'FRAME_SNAPSHOT machineId len header missing',\r\n          PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD,\r\n        )\r\n      }\r\n      const idLen = buffer.readUInt32BE(1)\r\n      const idEnd = 5 + idLen\r\n      if (idEnd > buffer.length) {\r\n        throw new FrameParseError(\r\n          'FRAME_SNAPSHOT machineId len exceeds buffer',\r\n          PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD,\r\n        )\r\n      }\r\n      const machineId = buffer.toString('utf-8', 5, idEnd)\r\n      if (idEnd + 4 > buffer.length) {\r\n        throw new FrameParseError(\r\n          'FRAME_SNAPSHOT ansi len header missing',\r\n          PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD,\r\n        )\r\n      }\r\n      const ansiLen = buffer.readUInt32BE(idEnd)\r\n      const ansiEnd = idEnd + 4 + ansiLen\r\n      if (ansiEnd > buffer.length) {\r\n        throw new FrameParseError(\r\n          'FRAME_SNAPSHOT ansi len exceeds buffer',\r\n          PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD,\r\n        )\r\n      }\r\n      const ansi = buffer.toString('utf-8', idEnd + 4, ansiEnd)\r\n      return { type: 'frame_snapshot', machineId, ansi }\r\n    }\r\n    case OPCODE.GRID_SNAPSHOT: {\r\n      // A3 server-truth full snapshot. desktop 측은 송신만 — 수신 거의 없음 (relay echo X 가정),\r\n      // 단 fixture-driven contract test 는 동일 parse 가능해야 contract 보장.\r\n      // header: opcode 1 + seq 4 + cols 2 + rows 2 + (가변 cells) + cursorR 2 + cursorC 2 ≥ 11 bytes\r\n      if (buffer.length < 11) {\r\n        throw new FrameParseError(\r\n          'GRID_SNAPSHOT header < 11 bytes',\r\n          PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD,\r\n        )\r\n      }\r\n      const seq = buffer.readUInt32BE(1)\r\n      const cols = buffer.readUInt16BE(5)\r\n      const rows = buffer.readUInt16BE(7)\r\n      // cells 는 가변 길이 — cursor 4 byte 가 buffer 끝에서 역계산\r\n      const cursorR = buffer.readUInt16BE(buffer.length - 4)\r\n      const cursorC = buffer.readUInt16BE(buffer.length - 2)\r\n      const cells = buffer.subarray(9, buffer.length - 4)\r\n      return { type: 'grid_snapshot', seq, cols, rows, cells, cursorR, cursorC }\r\n    }\r\n    case OPCODE.GRID_DIFF: {\r\n      // A3 incremental cell ops. 마찬가지로 desktop 측은 송신만 — 본 case 는 contract 호환 용.\r\n      // header: opcode 1 + seq 4 + base_seq 4 + opCount 2 + (가변 ops) ≥ 11 bytes\r\n      if (buffer.length < 11) {\r\n        throw new FrameParseError(\r\n          'GRID_DIFF header < 11 bytes',\r\n          PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD,\r\n        )\r\n      }\r\n      const seq = buffer.readUInt32BE(1)\r\n      const baseSeq = buffer.readUInt32BE(5)\r\n      // opCount 는 ops 안에 prefix 로 있음 — dispatcher 가 cell-by-cell 해석. parser 는 raw ops buffer 만 반환.\r\n      const ops = buffer.subarray(9)\r\n      return { type: 'grid_diff', seq, baseSeq, ops }\r\n    }\r\n    case OPCODE.PROTOCOL_ERROR: {\r\n      if (buffer.length < 2) {\r\n        throw new FrameParseError(\r\n          'PROTOCOL_ERROR payload empty',\r\n          PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD,\r\n        )\r\n      }\r\n      return { type: 'protocol_error', reason: buffer.readUInt8(1) }\r\n    }\r\n    default:\r\n      throw new FrameParseError(\r\n        `unknown opcode 0x${opcode.toString(16)}`,\r\n        PROTOCOL_ERROR_REASON.UNKNOWN_OPCODE,\r\n      )\r\n  }\r\n}\r\n\r\n/**\r\n * PTY_DATA frame (`[0x00] [PTY raw bytes]`) — desktop 의 PTY stdout 을 mobile 로 송신.\r\n */\r\nexport function encodePtyData(bytes: Buffer): Buffer {\r\n  const frame = Buffer.alloc(1 + bytes.length)\r\n  frame.writeUInt8(OPCODE.PTY_DATA, 0)\r\n  bytes.copy(frame, 1)\r\n  return frame\r\n}\r\n\r\n/**\r\n * APP_JSON frame — application 메시지 (machine_list, hello_ack, terminal_resize_ack 등).\r\n */\r\nexport function encodeAppJson(data: object): Buffer {\r\n  const body = Buffer.from(JSON.stringify(data), 'utf-8')\r\n  const frame = Buffer.alloc(1 + body.length)\r\n  frame.writeUInt8(OPCODE.APP_JSON, 0)\r\n  body.copy(frame, 1)\r\n  return frame\r\n}\r\n\r\n/**\r\n * PROTOCOL_ERROR frame.\r\n */\r\nexport function encodeProtocolError(reason: number): Buffer {\r\n  const frame = Buffer.alloc(2)\r\n  frame.writeUInt8(OPCODE.PROTOCOL_ERROR, 0)\r\n  frame.writeUInt8(reason & 0xff, 1)\r\n  return frame\r\n}\r\n\r\n/**\r\n * FRAME_SNAPSHOT (0x03) frame — desktop 이 session_attach 시 자신의 GridEmulator(정확한 PTY\r\n * size)를 ANSI 로 직렬화해 attach 하는 mobile 에 송신. raw scrollback tail 만으로는 복원 불가한\r\n * busy alt-screen TUI(claude 등) 화면을 정확히 재현한다.\r\n *\r\n * Wire format (mobile `relay_binary_dispatcher.dart` `_parseFrameSnapshot` 와 정확히 일치해야 함):\r\n *   `[0x03] [machineId_len: BE u32] [machineId: utf8] [ansi_len: BE u32] [ansi: utf8]`.\r\n * relay 는 host→mobile binary frame 을 opaque 하게 forward 하므로 relay 변경 불필요.\r\n *\r\n * @param machineId 대상 머신 식별자 (mobile 이 어느 머신의 frame 인지 라우팅)\r\n * @param ansi GridEmulator.serializeAnsi() 결과 — cursor + SGR 보존 ANSI 시퀀스\r\n */\r\nexport function encodeFrameSnapshot(machineId: string, ansi: string): Buffer {\r\n  const idBytes = Buffer.from(machineId, 'utf-8')\r\n  const ansiBytes = Buffer.from(ansi, 'utf-8')\r\n  const head = Buffer.alloc(5)\r\n  head.writeUInt8(OPCODE.FRAME_SNAPSHOT, 0)\r\n  head.writeUInt32BE(idBytes.length >>> 0, 1)\r\n  const ansiLen = Buffer.alloc(4)\r\n  ansiLen.writeUInt32BE(ansiBytes.length >>> 0, 0)\r\n  return Buffer.concat([head, idBytes, ansiLen, ansiBytes])\r\n}\r\n\r\n/**\r\n * hello_ack message wrapped in APP_JSON. desktop 이 mobile 의 첫 hello 받으면 응답.\r\n */\r\nexport function encodeHelloAck(): Buffer {\r\n  return encodeAppJson({ type: 'hello_ack', serverVersion: PROTOCOL_VERSION })\r\n}\r\n\r\n/**\r\n * GRID_SNAPSHOT (0x04) frame — A3 server-truth full snapshot.\r\n *\r\n * Wire format: `[0x04] [seq: BE u32] [cols: BE u16] [rows: BE u16] [cellArray] [cursorR: BE u16] [cursorC: BE u16]`.\r\n * cellArray 는 GridEmulator.serializeCells() 가 만든 Buffer — 가변 길이 (palette mode 평균 6 bytes/cell).\r\n * 본 함수는 그 Buffer 를 envelope 만 두름.\r\n *\r\n * @param seq 단조증가 sequence — recover 시 client 가 last_seq 로 reference\r\n * @param cols / rows server-side cap (max_cols/rows) 적용 후 실제 grid 크기\r\n * @param cells GridEmulator.serializeCells() 결과 Buffer\r\n * @param cursorR / cursorC 0-based row/col index\r\n */\r\nexport function encodeGridSnapshot(\r\n  seq: number,\r\n  cols: number,\r\n  rows: number,\r\n  cells: Buffer,\r\n  cursorR: number,\r\n  cursorC: number,\r\n): Buffer {\r\n  const head = Buffer.alloc(9)\r\n  head.writeUInt8(OPCODE.GRID_SNAPSHOT, 0)\r\n  head.writeUInt32BE(seq >>> 0, 1)\r\n  head.writeUInt16BE(cols & 0xffff, 5)\r\n  head.writeUInt16BE(rows & 0xffff, 7)\r\n  const tail = Buffer.alloc(4)\r\n  tail.writeUInt16BE(cursorR & 0xffff, 0)\r\n  tail.writeUInt16BE(cursorC & 0xffff, 2)\r\n  return Buffer.concat([head, cells, tail])\r\n}\r\n\r\n/**\r\n * GRID_DIFF (0x05) frame — A3 incremental cell ops.\r\n *\r\n * Wire format: `[0x05] [seq: BE u32] [base_seq: BE u32] [opsBuffer]`.\r\n * opsBuffer 는 GridEmulator.serializeDirtyOps() 가 만든 Buffer — `[opCount: BE u16] [ops...]` 형태.\r\n * 본 함수는 envelope 만 두름.\r\n *\r\n * @param seq 단조증가 sequence — base_seq + 1 이 되어야 정상 (client 가 gap 감지로 snapshot.req 발사)\r\n * @param baseSeq 직전 broadcast 의 seq — client 가 seq 연속성 검증용\r\n * @param opsBuffer GridEmulator.serializeDirtyOps() 결과 (opCount 포함)\r\n */\r\nexport function encodeGridDiff(\r\n  seq: number,\r\n  baseSeq: number,\r\n  opsBuffer: Buffer,\r\n): Buffer {\r\n  const head = Buffer.alloc(9)\r\n  head.writeUInt8(OPCODE.GRID_DIFF, 0)\r\n  head.writeUInt32BE(seq >>> 0, 1)\r\n  head.writeUInt32BE(baseSeq >>> 0, 5)\r\n  return Buffer.concat([head, opsBuffer])\r\n}\r\n","// PTY lifecycle policy: per-machine metadata, LRU eviction, idle timer,\n// pty_status broadcast (throttled).\n//\n// 책임: ssh-manager 가 PTY spawn/exit/data 이벤트를 알릴 때마다 metadata 갱신,\n// pty_status broadcast, count cap / idle timer 검사. 추출 이유: ssh-manager 가\n// 1117 LOC + 30 메서드로 비대화 — CLAUDE.md 10-메서드 규칙 위반. 분리하면 미래\n// E1 daemon 분리 시 이 클래스가 daemon 측 자연 boundary.\n//\n// Lifecycle hooks 매트릭스 (정확한 갱신 순서):\n//\n//   Trigger              | ptyMetadata     | pty_status broadcast\n//   ---------------------|-----------------|---------------------------\n//   onConnect            | (count cap chk) | n/a (spawn 후 onAttach)\n//                        | + create entry  |\n//   onDataActivity       | touch (1s deb)  | running (throttled)\n//   onUserInput          | touch (즉시)     | running (throttled)\n//   onAttach             | touchAttach     | bypass throttle, recompute\n//   onDetach             | n/a             | recompute (idle/running)\n//   onRestartPty         | reset           | dead → 새 spawn 후 running\n//   onDisconnect         | delete          | dead, reason='natural' FIRST\n//   onPtyExit            | (delete)        | dead, reason='natural'\n//   onRemoveMachine      | delete          | (chains via onDisconnect)\n//   tickIdle (60s)       | scan            | for each: status; if 7d→evict\n//\n// Eviction 시 송신 순서: pty_status('dead', reason='lru'|'idle') FIRST →\n// disconnect() (그 안에서 connection_status('disconnected')). 모바일이 dead\n// 먼저 받아 UI 전환, 그 후 disconnected — flicker 없음.\n\nimport { createLogger } from '../lib/logger'\n\n// Per-component scoped logger — writes to logs/pty.log.\nconst log = createLogger('pty')\nimport type { ServerMessage } from '@arva/shared/types'\nimport {\n  PTY_COUNT_MAX,\n  PTY_IDLE_TIMEOUT_MS,\n  PTY_ACTIVITY_THRESHOLD_MS,\n  PTY_STATUS_BROADCAST_THROTTLE_MS,\n} from '@arva/shared/constants'\nimport { GridEmulator, type ResizeResult } from './grid-emulator'\n\n/** 머신별 lifecycle 추적 데이터. */\nexport interface PtyMetadata {\n  lastAttach: number\n  lastActivity: number\n  attachCount: number\n  /** 다음 idle tick 검사 예정 시각. activity/attach 마다 now + IDLE_TIMEOUT 으로 갱신.\n   *  timestamp-driven tick: tick 시 nextIdleCheckAt > now 면 즉시 skip → broadcast storm 0. */\n  nextIdleCheckAt: number\n}\n\nexport type PtyStatus = 'idle' | 'running' | 'dead'\nexport type DeadReason = 'idle' | 'lru' | 'natural'\n\n/** ssh-manager 가 lifecycle manager 에게 제공하는 hook. */\nexport interface PtyLifecycleHost {\n  /** 현재 활성 PTY 수 (sessions + localSessions + pendingFirstResize). pre-await sync. */\n  currentPtyCount(): number\n  /** Eviction 대상 머신을 강제 disconnect. evict 결정 후 호출. */\n  evictMachine(machineId: string): void\n  /** restartPty 진행 중인 머신은 idle tick 에서 evict skip. */\n  isRestarting(machineId: string): boolean\n  /** Renderer + mobile 에 메시지 broadcast (ssh-manager 의 broadcast 콜백 위임). */\n  broadcast(msg: ServerMessage): void\n}\n\n/**\n * PTY lifecycle 정책 + pty_status broadcast 의 단일 진입점.\n *\n * ssh-manager 는 PTY 라이프사이클 이벤트마다 이 클래스의 hook 메서드를 호출하면\n * 됨. Manager 가 metadata, throttling, idle timer, eviction 결정을 모두 담당.\n *\n * 시작/종료: constructor() 가 ptyIdleTimer 시작, dispose() 가 clear.\n */\nexport class PtyLifecycleManager {\n  private readonly host: PtyLifecycleHost\n  private readonly metadata = new Map<string, PtyMetadata>()\n  private readonly lastBroadcastAt = new Map<string, number>()\n  private readonly lastStatus = new Map<string, PtyStatus>()\n  private ptyIdleTimer: ReturnType<typeof setInterval> | null = null\n  /**\n   * A3 server-truth grid emulator owner. PTY 1:1.\n   * onConnect 시 생성, onPtyExit 시 dispose. capability=grid.diff client 의 단일 권위.\n   * spec: claude_code_mobile/docs/specs/binary-protocol-v3.md §3, §6.5.\n   */\n  private readonly emulators = new Map<string, GridEmulator>()\n\n  /** 테스트용 clock injection. 기본 Date.now. */\n  private readonly nowFn: () => number\n\n  constructor(host: PtyLifecycleHost, opts: { now?: () => number; idleTickMs?: number } = {}) {\n    this.host = host\n    this.nowFn = opts.now ?? Date.now\n    const tickMs = opts.idleTickMs ?? 60_000\n    this.ptyIdleTimer = setInterval(() => this.tickIdle(), tickMs)\n  }\n\n  /** 모든 timer 해제 + Map clear. ssh-manager.cleanup() 에서 호출. */\n  dispose(): void {\n    if (this.ptyIdleTimer) {\n      clearInterval(this.ptyIdleTimer)\n      this.ptyIdleTimer = null\n    }\n    this.metadata.clear()\n    this.lastBroadcastAt.clear()\n    this.lastStatus.clear()\n    for (const em of this.emulators.values()) em.dispose()\n    this.emulators.clear()\n  }\n\n  // ── Lifecycle hooks (ssh-manager 호출) ─────────────────────────────────────\n\n  /**\n   * connect() / connectLocal() 진입 시 호출. **첫 await 전 동기적으로** count cap 검사.\n   * cap 도달이면 LRU 1개 evict 후 새 metadata create. 같은 tick 동시 호출은\n   * pendingFirstResize.set 이 첫 await 전에 일어나므로 host.currentPtyCount() 가 보호.\n   *\n   * 주의: 이 메서드 자체는 동기. evictMachine 호출은 host 가 동기 처리 (disconnect\n   * 가 sync mutate). 따라서 cap 검사 → evict → 새 spawn 가 race 없이 진행.\n   */\n  onConnect(machineId: string, initialCols = 80, initialRows = 24): void {\n    const count = this.host.currentPtyCount()\n    if (count >= PTY_COUNT_MAX) {\n      const victim = this.pickLruVictim(machineId)\n      if (victim) {\n        log.info(`[pty-lifecycle] count=${count} cap=${PTY_COUNT_MAX} → evict LRU ${victim}`)\n        // 송신 순서: dead FIRST → disconnect (host.evictMachine 안에서 connection_status).\n        this.broadcastStatus(victim, 'dead', { reason: 'lru', bypassThrottle: true })\n        this.host.evictMachine(victim)\n      }\n    }\n    this.cleanupMachineState(machineId)\n    const now = this.nowFn()\n    this.metadata.set(machineId, {\n      lastAttach: now,\n      lastActivity: now,\n      attachCount: 0,\n      nextIdleCheckAt: now + PTY_IDLE_TIMEOUT_MS,\n    })\n    // A3: emulator 생성 (PTY 1:1). initialCols/Rows 는 SSH spawn 의 hint 와 일치해야\n    // 첫 GRID_SNAPSHOT 이 정확. 인자 default (80×24) 는 SSH RFC 4254 default.\n    if (!this.emulators.has(machineId)) {\n      this.emulators.set(machineId, new GridEmulator(initialCols, initialRows))\n    }\n  }\n\n  /** PTY 출력 도착 시 호출. 1초 debounce — 핫패스 부담 최소화. */\n  onDataActivity(machineId: string): void {\n    const meta = this.metadata.get(machineId)\n    if (!meta) return\n    const now = this.nowFn()\n    if (now - meta.lastActivity < 1000) return\n    this.touchActivity(meta, now)\n    this.broadcastStatus(machineId, this.computeStatus(meta))\n  }\n\n  /** sendInput() 호출 시. debounce 없음 — 사용자 액션은 항상 기록. */\n  onUserInput(machineId: string): void {\n    const meta = this.metadata.get(machineId)\n    if (!meta) return\n    this.touchActivity(meta, this.nowFn())\n    this.broadcastStatus(machineId, this.computeStatus(meta))\n  }\n\n  /** session_attach 처리 시. throttle bypass — 첫 화면 정확성. */\n  onAttach(machineId: string): void {\n    let meta = this.metadata.get(machineId)\n    const now = this.nowFn()\n    if (!meta) {\n      // PTY 가 아직 spawn 안 됐는데 attach 가 먼저 옴. ssh-manager 가\n      // 곧 onConnect 호출할 것이므로 임시 entry. running 으로 broadcast.\n      meta = {\n        lastAttach: now,\n        lastActivity: now,\n        attachCount: 1,\n        nextIdleCheckAt: now + PTY_IDLE_TIMEOUT_MS,\n      }\n      this.metadata.set(machineId, meta)\n    } else {\n      meta.lastAttach = now\n      meta.attachCount += 1\n      meta.nextIdleCheckAt = now + PTY_IDLE_TIMEOUT_MS\n    }\n    this.broadcastStatus(machineId, this.computeStatus(meta), { bypassThrottle: true })\n  }\n\n  /**\n   * 활동 timestamp 갱신: lastActivity + nextIdleCheckAt 동시 set.\n   * onDataActivity / onUserInput 의 공통 패턴. 미래 lifecycle 필드 추가 시\n   * 모든 activity 지점에 일관 적용 보장.\n   */\n  private touchActivity(meta: PtyMetadata, now: number): void {\n    meta.lastActivity = now\n    meta.nextIdleCheckAt = now + PTY_IDLE_TIMEOUT_MS\n  }\n\n  /** session_detach 처리 시. lastActivity 기준 idle/running 재계산해서 송신. */\n  onDetach(machineId: string): void {\n    const meta = this.metadata.get(machineId)\n    if (!meta) return\n    this.broadcastStatus(machineId, this.computeStatus(meta), { bypassThrottle: true })\n  }\n\n  /**\n   * restartPty() 호출 시. metadata reset (lastAttach/lastActivity=now,\n   * attachCount+1). dead → 새 spawn 후 running (ssh-manager 가 onConnect 재호출).\n   */\n  onRestartPty(machineId: string): void {\n    const now = this.nowFn()\n    const prev = this.metadata.get(machineId)\n    const newMeta: PtyMetadata = {\n      lastAttach: now,\n      lastActivity: now,\n      attachCount: (prev?.attachCount ?? 0) + 1,\n      nextIdleCheckAt: now + PTY_IDLE_TIMEOUT_MS,\n    }\n    this.broadcastStatus(machineId, 'dead', { reason: 'natural', bypassThrottle: true })\n    this.metadata.set(machineId, newMeta)\n  }\n\n  /**\n   * 사용자 명시 disconnect() 시. dead broadcast FIRST → host 가 sessions delete.\n   * cleanup 은 ssh-manager 의 disconnect() 안에서 cleanupMachineState 호출.\n   */\n  onDisconnect(machineId: string, reason: DeadReason = 'natural'): void {\n    if (this.metadata.has(machineId) || this.lastStatus.has(machineId)) {\n      this.broadcastStatus(machineId, 'dead', { reason, bypassThrottle: true })\n    }\n    this.cleanupMachineState(machineId)\n  }\n\n  /** PTY 자연 종료 (proc.onExit / stream.on('close')). disconnect 와 동일 처리. */\n  onPtyExit(machineId: string): void {\n    this.onDisconnect(machineId, 'natural')\n    this.disposeEmulator(machineId)\n  }\n\n  // ── A3 server-truth (grid emulator) hooks ──────────────────────────────\n\n  /**\n   * PTY stdout chunk 도착 시 호출 — emulator 의 grid 갱신.\n   *\n   * ssh-manager 의 `proc.onData` 안에서 기존 `onDataActivity(machineId)` 옆에 호출.\n   * SRP — emulator append 만 책임 (broadcast 는 [flushDiff] 또는 ws-manager 에서 별도).\n   *\n   * cap=`pty.data` (legacy) client 만 있는 경우에도 emulator 는 운영 — capability 가 동적\n   * 변경 (재연결 등) 시 즉시 GRID_SNAPSHOT 보낼 수 있도록.\n   */\n  onPtyData(machineId: string, chunk: string | Buffer): void {\n    const em = this.emulators.get(machineId)\n    if (em) em.write(chunk)\n  }\n\n  /**\n   * mobile client 가 cap.ack 'grid.diff' 받은 직후 호출 — GRID_SNAPSHOT 1회 송신.\n   *\n   * `send` 는 ws-manager 가 주입한 단일 client 송신 함수 (`ws.send.bind(ws)` 형태).\n   * 모든 client broadcast 가 아닌 신규 attach client 에만 — cold attach 는 1:1.\n   */\n  onAttachGridDiff(machineId: string, send: (frame: Buffer) => void): void {\n    const em = this.emulators.get(machineId)\n    if (em) send(em.snapshot())\n  }\n\n  /**\n   * 머신의 현재 grid 를 ANSI 문자열로 직렬화 (cursor + SGR 보존). emulator 미존재 시 빈 문자열.\n   *\n   * ws-manager 가 session_attach 시 호출 → FRAME_SNAPSHOT (0x03) 로 감싸 attach mobile 에 송신.\n   * cap 무관(legacy pty.data client 포함 모두 대상) — raw scrollback tail 만으로 복원 불가한 busy\n   * alt-screen TUI 화면을 정확한 PTY size 의 server-truth emulator 로 복원하기 위함.\n   */\n  serializeGridAnsi(machineId: string): string {\n    const em = this.emulators.get(machineId)\n    return em ? em.serializeAnsi() : ''\n  }\n\n  /**\n   * [non-sync diag 20260614] 머신 grid 의 \"빈 화면\" 진단 — buffer 직접 순회 결과.\n   * ws-manager 가 frame_snapshot 로그에 append 해 a1(claude 비움) vs a2(serialize 버그) 를\n   * 가른다. emulator 미존재 시 null. docs/non-sync.md §4 (claude_code_mobile repo).\n   */\n  gridSnapshotDiag(machineId: string): {\n    bufType: string\n    nonBlankLines: number\n    rows: number\n    glyphs: number\n  } | null {\n    const em = this.emulators.get(machineId)\n    return em ? em.snapshotDiag() : null\n  }\n\n  /**\n   * RESIZE 수신 시 — emulator resize + cap 검사.\n   *\n   * 결과의 `capped=true` 이면 ws-manager 가 client 에 `grid_resize_capped` APP_JSON\n   * broadcast (실제 적용된 cols/rows 회신, plan §13-11 trim 알림).\n   *\n   * resize 직후 emulator 의 markFullDirty 가 동작 — 다음 [flushDiff] 가 사실상 full\n   * snapshot 크기. spike v2 에서 resize-aware diff 로 정밀화 가능.\n   */\n  onResize(machineId: string, cols: number, rows: number): ResizeResult | null {\n    const em = this.emulators.get(machineId)\n    if (!em) return null\n    return em.resize(cols, rows)\n  }\n\n  /**\n   * snapshot.req 수신 시 — manual / recover / reattach 모두 동일 처리.\n   * GRID_SNAPSHOT 1회 응답. throttle (3/min/session) 은 ws-manager 가 본 호출 전에 검사.\n   */\n  onSnapshotReq(machineId: string, send: (frame: Buffer) => void): void {\n    const em = this.emulators.get(machineId)\n    if (em) send(em.snapshot())\n  }\n\n  /**\n   * 마지막 broadcast 이후 dirty cell 의 GRID_DIFF 송신.\n   *\n   * `broadcast` 는 ws-manager 가 주입 — cap=`grid.diff` attached client 에만 fan-out.\n   * dirty 가 비어있으면 broadcast 발사 X (em.diffSinceLast() 가 null 반환).\n   *\n   * 호출 시점: ws-manager 가 onPtyData 직후 또는 N ms 디바운스 후 (spike 측정 후 결정).\n   */\n  flushDiff(machineId: string, broadcast: (frame: Buffer) => void): void {\n    const em = this.emulators.get(machineId)\n    if (!em) return\n    const diff = em.diffSinceLast()\n    if (diff) broadcast(diff)\n  }\n\n  /**\n   * 머신별 emulator dispose. onPtyExit / onRemoveMachine / cleanup 에서 호출.\n   * disposeEmulator 단독 호출 가능 — emulator 만 정리하고 metadata 보존하는 시나리오 대비.\n   */\n  private disposeEmulator(machineId: string): void {\n    const em = this.emulators.get(machineId)\n    if (em) {\n      em.dispose()\n      this.emulators.delete(machineId)\n    }\n  }\n\n  /** removeMachine 시 (disconnect 가 chain 으로 처리하는 경우 외 explicit cleanup). */\n  onRemoveMachine(machineId: string): void {\n    this.cleanupMachineState(machineId)\n  }\n\n  // ── Helpers ─────────────────────────────────────────────────────────────────\n\n  /**\n   * 3 lifecycle Map 에서 해당 머신 ID 제거. disconnect/restartPty/removeMachine/\n   * onConnect (re-create 전 정리) 4지점에서 호출. DRY 보장 — 새 lifecycle Map\n   * (예: future RSS oom 도입 시 dropTimestamps) 도입 시 한 곳에서 증설.\n   */\n  private cleanupMachineState(machineId: string): void {\n    this.metadata.delete(machineId)\n    this.lastBroadcastAt.delete(machineId)\n    this.lastStatus.delete(machineId)\n    this.disposeEmulator(machineId)\n  }\n\n  /**\n   * [Reclaim guard 20260606] 머신의 마지막 PTY 활동 시각(ms) 또는 undefined.\n   *\n   * PTY 출력(`onDataActivity`)/입력(`onUserInput`)으로 갱신되는 `lastActivity` 를 그대로\n   * 노출 — AuthorityResolver 의 handed_off watchdog 가 \"응답 스트리밍 중(=연속 출력)\" 을\n   * 감지해 회수를 보류하는 데 쓴다. 머신 메타가 없으면(eviction/PTY exit/disconnect) undefined.\n   */\n  getLastActivity(machineId: string): number | undefined {\n    return this.metadata.get(machineId)?.lastActivity\n  }\n\n  /** lastActivity 가 최근 ACTIVITY_THRESHOLD 안이면 'running', 아니면 'idle'. */\n  private computeStatus(meta: PtyMetadata): PtyStatus {\n    const now = this.nowFn()\n    return now - meta.lastActivity < PTY_ACTIVITY_THRESHOLD_MS ? 'running' : 'idle'\n  }\n\n  /** LRU key = max(lastAttach, lastActivity). 가장 작은 (가장 오래된) 머신 반환.\n   *  excludeMachineId 는 새로 connect 시도하는 머신 — eviction 후보 X. */\n  private pickLruVictim(excludeMachineId: string): string | null {\n    let victim: string | null = null\n    let oldest = Number.POSITIVE_INFINITY\n    for (const [id, meta] of this.metadata) {\n      if (id === excludeMachineId) continue\n      const lruKey = Math.max(meta.lastAttach, meta.lastActivity)\n      if (lruKey < oldest) {\n        oldest = lruKey\n        victim = id\n      }\n    }\n    return victim\n  }\n\n  /**\n   * pty_status broadcast 의 단일 진입점.\n   * - throttle: now - lastBroadcastAt < THROTTLE && status === lastStatus → skip\n   * - bypassThrottle: session_attach/eviction 등 즉시 송신해야 할 때\n   * - try/catch + electron-log.warn: BrowserWindow closed race 흡수 (silent)\n   */\n  private broadcastStatus(\n    machineId: string,\n    status: PtyStatus,\n    opts: { reason?: DeadReason; bypassThrottle?: boolean } = {},\n  ): void {\n    const now = this.nowFn()\n    const lastAt = this.lastBroadcastAt.get(machineId) ?? 0\n    const lastStat = this.lastStatus.get(machineId)\n    const sameStatus = lastStat === status\n    if (!opts.bypassThrottle && sameStatus && now - lastAt < PTY_STATUS_BROADCAST_THROTTLE_MS) {\n      return\n    }\n    this.lastBroadcastAt.set(machineId, now)\n    this.lastStatus.set(machineId, status)\n    try {\n      const msg: ServerMessage =\n        status === 'dead' && opts.reason\n          ? { type: 'pty_status', machineId, status, reason: opts.reason }\n          : { type: 'pty_status', machineId, status }\n      this.host.broadcast(msg)\n    } catch (err) {\n      log.warn('[pty-lifecycle] broadcast failed', { machineId, status, err: (err as Error).message })\n    }\n  }\n\n  /**\n   * Idle timer tick (60s). Timestamp-driven: nextIdleCheckAt > now 면 즉시 skip\n   * → vi.advanceTimersByTime(7d) 시 broadcast storm 0.\n   */\n  private tickIdle(): void {\n    const now = this.nowFn()\n    // host evict 호출이 metadata 를 mutate 하므로 entries snapshot.\n    const snapshot = [...this.metadata.entries()]\n    for (const [id, meta] of snapshot) {\n      if (meta.nextIdleCheckAt > now) continue\n      // restarting 중이면 skip — 다음 tick 에 재검사.\n      if (this.host.isRestarting(id)) continue\n      const lruKey = Math.max(meta.lastAttach, meta.lastActivity)\n      if (now - lruKey > PTY_IDLE_TIMEOUT_MS) {\n        log.info(`[pty-lifecycle] idle evict machine=${id} idle=${(now - lruKey) / 1000}s`)\n        this.broadcastStatus(id, 'dead', { reason: 'idle', bypassThrottle: true })\n        this.host.evictMachine(id)\n      } else {\n        // 활동 있어서 idle 아님 — broadcast 만 하고 nextIdleCheckAt 갱신.\n        meta.nextIdleCheckAt = now + PTY_IDLE_TIMEOUT_MS\n        this.broadcastStatus(id, this.computeStatus(meta))\n      }\n    }\n  }\n\n  // ── Test-only inspectors ──────────────────────────────────────────────────\n\n  /** @internal 테스트용 — production 호출 금지. */\n  metadataForTest(machineId: string): PtyMetadata | undefined {\n    return this.metadata.get(machineId)\n  }\n\n  /** @internal 테스트용. */\n  lastStatusForTest(machineId: string): PtyStatus | undefined {\n    return this.lastStatus.get(machineId)\n  }\n}\n","import type { BroadcastFn, ServerMessage } from '@arva/shared/types'\nimport { createLogger } from '../lib/logger'\n\n// Per-component scoped logger — writes to logs/authority.log.\n// A2 spike dogfood (2026-05-26~): viewer_mode broadcast / takeback / handover\n// 흐름 가시화 위해 신설.\nconst log = createLogger('authority')\n\n/**\n * 권위 leader. 'desktop' 이면 데스크탑 renderer 가 PTY 사이즈 invariant 결정,\n * 'mobile' 이면 특정 모바일 client (`AuthorityState.mobileClientId`) 가 결정.\n */\nexport type AuthorityLeader = 'desktop' | 'mobile'\n\n/**\n * 머신 1개당 권위 상태. 외부에서는 `AuthorityResolver.getState` 로 readonly 조회.\n *\n * **A2 spike (20260523)**: discriminated union 으로 변환 — 'mobile' 변형이 항상-존재하는\n * `attachedAt`/`lastActivityAt` 를 안전히 보유. 이전 단일 interface 형태에서는 `recordAppliedSize`\n * 의 `{...prev, cols, rows}` spread 가 `attachedAt` 를 erase 할 수 있던 type bug 가 있었음.\n *\n * - `'desktop'` 변형 — `lastDesktopSize` 는 grace expire 시 복귀 size 로 사용.\n * - `'mobile'` 변형 — `cols/rows` 는 mobile 권위 viewer 가 마지막 적용 사이즈.\n *   `attachedAt` 는 viewer_mode broadcast 의 mobileMeta 기준 시각.\n *   `lastActivityAt` 는 mobile input 마다 갱신 (heartbeat 용, 1Hz throttle 로 재broadcast).\n */\nexport type AuthorityState =\n  | {\n      leader: 'desktop'\n      lastDesktopSize?: { cols: number; rows: number }\n    }\n  | {\n      leader: 'mobile'\n      mobileClientId: string\n      cols?: number\n      rows?: number\n      attachedAt: number\n      lastActivityAt: number\n    }\n\n/**\n * AuthorityResolver 의 외부 의존성. 모든 부수효과는 deps 함수 호출로만 발생 →\n * vitest 에서 SSHManager 전체 mock 없이 순수 state machine 으로 검증 가능.\n */\nexport interface AuthorityResolverDeps {\n  /** ServerMessage broadcast — main/index.ts 의 broadcast 클로저. */\n  broadcast: BroadcastFn\n  /**\n   * ssh-manager.resize 위임. grace expire 시 desktop-restore 트리거 등 외부 효과.\n   * source 가 'desktop-restore' 면 resolver 의 allowsResize 가드를 무조건 통과.\n   */\n  applyResize: (\n    machineId: string,\n    cols: number,\n    rows: number,\n    source: 'desktop-restore'\n  ) => void\n  /** 마지막 desktop source resize 사이즈 — grace expire 시 복귀 사이즈로 사용. */\n  getLastDesktopSize: (\n    machineId: string\n  ) => { cols: number; rows: number } | undefined\n  /**\n   * [Reclaim guard 20260606] 머신의 마지막 PTY 활동 시각(ms). production 은\n   * `PtyLifecycleManager.getLastActivity` — PTY 출력(`onDataActivity`)/입력으로 갱신.\n   *\n   * handed_off watchdog 가 \"모바일 무입력\" 만이 아니라 \"PTY 출력도 정지\" 를 함께 봐야\n   * 응답 스트리밍 중(=연속 PTY 출력) 세션을 뺏지 않는다. 미주입(undefined) 시 watchdog 는\n   * `lastActivityAt` 단일 기준으로 폴백 — 구 동작과 동일(테스트 W1~W5 회귀 0).\n   */\n  getLastPtyActivity?: (machineId: string) => number | undefined\n  /**\n   * [Reclaim guard 20260606] true 면 에이전트 턴 진행 중(예: claude_state working/\n   * awaiting-approval) — 출력·입력이 잠시 없어도 watchdog 회수를 절대 상한 전까지 보류한다.\n   *\n   * resolver 는 신호 출처(Claude 등)를 모른다 — opaque busy 만 받는다. 미주입 시 false 폴백.\n   */\n  isSessionBusy?: (machineId: string) => boolean\n  /** 테스트용 — production 은 Date.now 그대로. */\n  now?: () => number\n  /** 테스트용 — production 은 setTimeout. handle 타입 미고정 (NodeJS.Timeout / number). */\n  setTimer?: (fn: () => void, ms: number) => unknown\n  /** 테스트용 — production 은 clearTimeout. */\n  clearTimer?: (handle: unknown) => void\n  /**\n   * [A2 spike 20260523] true 면 single-viewer 동작 (handover 모달 skip + viewer_mode broadcast).\n   * false 면 Phase 1 동작 (requestHandover + 모달, viewer_mode broadcast 0).\n   *\n   * Production 기본값: env `VIEWER_MODE_SPIKE !== 'false'` (즉 default on). CEO Fix-D 의\n   * rollback 절차 = env flip + agent restart → 본 옵션이 false 로 evaluate → Phase 1 복원.\n   *\n   * 테스트는 명시적으로 boolean 전달해 두 모드 모두 검증. 미지정 시 env 평가 (Production 일치).\n   */\n  a2SpikeEnabled?: boolean\n}\n\n/**\n * Phase 1 (20260519) — PTY 권위 상태기계.\n *\n * 머신 1개당 1개의 상태기계 인스턴스를 관리한다. 60s TTL 휴리스틱(mobileAuthorityUntil)\n * 을 대체해 명시적 handover 모달 + grace timer 로 invariant 를 server-side 에서 보장.\n *\n * State: no-mobile / awaiting-handover / mobile-leading / mobile-rejected / grace-pending\n * (실제 저장은 `states` Map 에 leader + mobileClientId 조합으로만 표현 — pending/grace 는\n *  별도 Map 으로 추적해 합성 상태를 식별)\n *\n * 의존성은 모두 deps 로 주입 — 본 클래스는 SSHManager / Electron / setTimeout 직접 참조 X.\n *\n * BroadcastFn 시그니처의 `perRecipient` 콜백을 사용해 `terminal_authority_changed` 의\n * receiver 별 `isAuthority` 를 계산한다 (Phase 3 review back-port A2 finding).\n */\nexport class AuthorityResolver {\n  /** GRACE_PERIOD_MS — 마지막 모바일 detach 후 권위 유지 시간. 5s default (확정 #1). */\n  private static readonly GRACE_PERIOD_MS = 5_000\n  /** HANDOVER_TIMEOUT_MS — handover 모달 사용자 응답 대기 시간. 10s default (확정 #3). */\n  private static readonly HANDOVER_TIMEOUT_MS = 10_000\n  /** HANDOVER_DEFAULT_ACCEPT — handover timeout 시 default 결정. true=accept (확정 #8). */\n  private static readonly HANDOVER_DEFAULT_ACCEPT = true\n  /**\n   * [A2 spike 20260523] TAKEBACK_COOLDOWN_MS — desktop 의 Take Back 직후 mobile 재attach\n   * 거부 시간. 5s. clientId 와 무관하게 per-machineId 적용 (다른 디바이스로 bypass 방지).\n   */\n  private static readonly TAKEBACK_COOLDOWN_MS = 5_000\n  /**\n   * [Bug fix 20260604] HANDOFF_WATCHDOG_MS — handed_off 상태에서 모바일 무활동 한계 (60s).\n   *\n   * 이 시간 동안 모바일 입력(`lastActivityAt` 갱신)이 없으면 모바일이 사라진 것으로 보고\n   * desktop 회수. relay 경로는 per-mobile detach 신호가 없어(`relayWs` 는 agent↔relay 단일\n   * 소켓 + relay 가 per-mobile disconnect 를 forward 안 함 + 모바일이 주기적 heartbeat 미전송)\n   * `onMobileDetach` 가 호출되지 않는다 → 본 watchdog 가 1Hz heartbeat ticker 의 무한 실행을\n   * 막는 유일한 desktop-side 안전장치다.\n   *\n   * Tradeoff: 입력 없이 출력만 읽는 idle 모바일은 60s 후 회수됨 (재입력 시 cooldown-free 라\n   * 깔끔히 재attach). idle 포함 즉시 감지는 모바일 heartbeat/session_detach 전송 필요 (follow-up).\n   */\n  private static readonly HANDOFF_WATCHDOG_MS = 60_000\n  /**\n   * [Reclaim guard 20260606] HANDOFF_HARD_CEILING_MS — 모바일 무활동 절대 상한 (10min).\n   *\n   * 왜 필요한가: `getLastPtyActivity` 게이트는 응답 스트리밍을 보호하지만, **끝없이 출력하는\n   * PTY**(`tail -f`/dev server/`watch`/hung build)는 `lastPtyActivity` 가 영원히 신선해\n   * ptyIdleMs 가 WATCHDOG 를 못 넘긴다 → 모바일이 relay 로 조용히 사라져도 회수 불능\n   * (= commit 3ad27a2 stuck-handed_off 재현). 모바일 무활동(`idleMs`)이 본 상한을 넘으면\n   * PTY 출력/busy 와 무관하게 회수해 silent-vanish 복구를 **보장**한다 (noisy PTY 한정 60s→10min).\n   *\n   * 단 상한은 `idleMs`(모바일 keepalive/입력 침묵) 기준 — keepalive 를 계속 보내는 '존재하는\n   * 폰'은 의도적으로 유지된다(복구 보장은 keepalive 가 멎은 뒤 기준). 좀비/relay 가 keepalive 를\n   * 대신 보내는 경우만 예외이며 이는 relay 측 신뢰 문제로 본 범위 밖(수동 Take Back 이 최종 escape).\n   */\n  private static readonly HANDOFF_HARD_CEILING_MS = 600_000\n  /**\n   * [H6 fix 20260607] HANDOFF_BUSY_HARD_CEILING_MS — busy(에이전트 턴 진행) 중일 때 적용하는\n   * 모바일 무활동 절대 상한 (30min). 일반 `HANDOFF_HARD_CEILING_MS`(10min)보다 크다.\n   *\n   * 왜: 라이브 authority.log 판독(가이드 §0.3 \"H6\")에서 `busy=true` + PTY 활발히 출력 중\n   * (`ptyIdleMs<1s`)인데 10min 상한으로 모바일이 회수되는(=작업 중 세션 강탈) 사례 3건 확인.\n   * 정상 claude 턴은 분 단위라 30min 안에 끝나므로 전부 보호된다. 그래도 **finite** 인 이유:\n   * claude 가 Stop 없이 죽으면 'working' 이 잔류해 busy=true 가 영구화될 수 있어\n   * (hook-receiver `isAgentBusy` 주석) 무한 면제 시 stuck-handed_off 가 재발 — 30min backstop\n   * 으로 복구를 보장한다. 진짜 foreground 시청 중이면 모바일 keepalive 가 idleMs 를 낮게\n   * 유지해 어떤 상한에도 안 닿는 게 정상(상한은 폰이 침묵할 때만 의미).\n   */\n  private static readonly HANDOFF_BUSY_HARD_CEILING_MS = 1_800_000\n\n  private states = new Map<string, AuthorityState>()\n  private graceTimers = new Map<string, unknown>()\n  private pendingDecisions = new Map<\n    string,\n    {\n      timer: unknown\n      requesterClientId: string\n      requestedCols: number\n      requestedRows: number\n    }\n  >()\n  /**\n   * [A2 spike] machineId → cooldown 만료 timestamp (ms). `forceDesktopLeader` 가 now+5000\n   * 으로 설정. `onMobileAttach` 가 진입 즉시 체크 → reject + viewer_mode='rejected' broadcast.\n   * 인-메모리 only (agent restart 시 손실). spike 단계에서는 acceptable (R-8).\n   */\n  private takebackCooldownUntil = new Map<string, number>()\n\n  /**\n   * [Bug #1 fix 20260526 — M12 dogfood] machineId → 1Hz heartbeat ticker handle.\n   *\n   * 문제: `touchMobileActivity` 가 state 의 `lastActivityAt` 만 갱신하고 broadcast 안 함.\n   * desktop renderer 의 HandoffPlaceholder 가 표시하는 \"Last activity: Xs ago\" 가 *오래된*\n   * lastActivityAt 으로 계속 increment → reset 안 됨.\n   *\n   * 해결: handed_off 모드 진입 시 본 ticker 시작. 매 1초 mobileMeta 를 state 에서 새로 읽어\n   * viewer_mode='handed_off' 재방송 → renderer 가 갱신된 lastActivityAt 으로 \"Xs ago\" 정상 표시.\n   * handed_off 이탈 (interactive/attaching/error/...) 시 즉시 stop.\n   *\n   * 인-메모리 only. `destroy()` 가 모두 정리.\n   */\n  private _heartbeatTimers = new Map<string, unknown>()\n\n  private readonly deps: AuthorityResolverDeps\n  private readonly _setTimer: (fn: () => void, ms: number) => unknown\n  private readonly _clearTimer: (handle: unknown) => void\n  private readonly _a2SpikeEnabled: boolean\n\n  /**\n   * @param deps  broadcast / applyResize / getLastDesktopSize 외부 효과.\n   *              now / setTimer / clearTimer 는 production 미지정 시 setTimeout/clearTimeout 사용.\n   *              a2SpikeEnabled 미지정 시 env `VIEWER_MODE_SPIKE !== 'false'` 평가.\n   */\n  constructor(deps: AuthorityResolverDeps) {\n    this.deps = deps\n    this._setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms))\n    this._clearTimer =\n      deps.clearTimer ?? (h => clearTimeout(h as ReturnType<typeof setTimeout>))\n    this._a2SpikeEnabled =\n      deps.a2SpikeEnabled ?? process.env.VIEWER_MODE_SPIKE !== 'false'\n  }\n\n  /**\n   * resize() 호출 직전 invariant 가드. true 면 진행, false 면 ssh-manager 가 drop.\n   *\n   * - 'desktop-restore' 는 무조건 통과 (resolver 가 자기 expireGrace 에서 트리거하는 경로).\n   * - handover 협상 중이면 모든 source drop — 사이즈 깜빡임 차단.\n   * - 그 외는 leader 와 source 일치 시에만 통과 (양방향 invariant).\n   */\n  allowsResize(\n    machineId: string,\n    source: 'desktop' | 'mobile' | 'desktop-restore'\n  ): boolean {\n    if (source === 'desktop-restore') return true\n    if (this.pendingDecisions.has(machineId)) return false\n    const state = this.states.get(machineId) ?? { leader: 'desktop' as const }\n    if (state.leader === 'desktop') return source === 'desktop'\n    return source === 'mobile'\n  }\n\n  /**\n   * resize() 가 PTY 에 적용된 직후 호출. broadcast 의 cols/rows fallback 으로 사용된다.\n   * state 자체는 만들지 않고 (no-state 머신은 노출 안 함), 이미 state 가 있는 머신만 갱신.\n   * 첫 resize 시 ssh-manager 가 호출하기 전 state 가 없을 수도 있으니 ensure-create.\n   */\n  recordAppliedSize(machineId: string, cols: number, rows: number): void {\n    const prev = this.states.get(machineId) ?? { leader: 'desktop' as const }\n    if (prev.leader === 'desktop') {\n      // desktop 변형 — lastDesktopSize 갱신 (grace expire 시 복귀 size).\n      this.states.set(machineId, {\n        leader: 'desktop',\n        lastDesktopSize: { cols, rows },\n      })\n    } else {\n      // mobile 변형 — cols/rows 갱신, mobileClientId/attachedAt/lastActivityAt 보존\n      // (이전 단일 interface 의 spread bug 회피, Eng Fix-K).\n      this.states.set(machineId, { ...prev, cols, rows })\n    }\n  }\n\n  /**\n   * mobile WS attach 시 ws-manager 가 호출.\n   *\n   * 분기:\n   *   (1) grace-pending + 같은 clientId 재연결 → 즉시 복귀, broadcast(reconnected).\n   *   (2) 그 외 (leader=desktop 또는 leader=mobile + 다른 clientId) → 즉시 leader=mobile.\n   *\n   * 모바일 attach = 항상 모바일 권위. (R-3 정합 — 2026-06-07 desktop-priority 정책 제거,\n   * 모바일은 이미 PtyAuthorityMode 를 무시하므로 데스크탑만 따라간 것.)\n   */\n  onMobileAttach(\n    machineId: string,\n    clientId: string,\n    hintCols?: number,\n    hintRows?: number\n  ): void {\n    // [A2 spike 20260523] (0) Take Back cooldown 체크 — 5s 거부 + viewer_mode='rejected'.\n    // 해당 모바일 ws 는 ws-manager 에서 close code 4008 (TAKEBACK_COOLDOWN) 로 종료.\n    // spike-off 일 때는 cooldown 자체가 없으므로 skip.\n    if (this._a2SpikeEnabled) {\n      const cooldownUntil = this.takebackCooldownUntil.get(machineId) ?? 0\n      if (this.now() < cooldownUntil) {\n        this._broadcastViewerMode(machineId, 'rejected', {\n          cooldownUntil,\n          reason: 'takeback_cooldown',\n        })\n        return\n      }\n    }\n\n    const state = this.states.get(machineId) ?? { leader: 'desktop' as const }\n\n    // (1) grace-pending + 같은 clientId 재연결\n    const grace = this.graceTimers.get(machineId)\n    if (\n      grace !== undefined &&\n      state.leader === 'mobile' &&\n      state.mobileClientId === clientId\n    ) {\n      this._clearTimer(grace)\n      this.graceTimers.delete(machineId)\n      this.deps.broadcast(\n        { type: 'terminal_mobile_unstable', machineId, state: 'reconnected' },\n        { skipMobile: true }\n      )\n      if (this._a2SpikeEnabled) {\n        // viewer_mode 재방송 + lastActivityAt 갱신. spike-off 면 Phase 1 의 reconnect\n        // broadcast (위) 만 송신 — state 갱신은 ssh-manager 의 다음 RESIZE 에 위임.\n        this.states.set(machineId, {\n          ...state,\n          cols: hintCols ?? state.cols,\n          rows: hintRows ?? state.rows,\n          lastActivityAt: this.now(),\n        })\n        this._broadcastViewerMode(machineId, 'handed_off', {\n          cols: hintCols ?? state.cols,\n          rows: hintRows ?? state.rows,\n        })\n      }\n      return\n    }\n\n    // (2) 모바일 attach → 즉시 leader=mobile (정책 게이트 없음).\n    if (this._a2SpikeEnabled) {\n      // [A2 spike] handover 모달 skip — 즉시 leader 전환. viewer_mode='handed_off' broadcast\n      // 가 desktop UI 사용자 피드백 역할 (모달 대체). pendingDecisions cleanup 필수\n      // (Eng Fix-M): 직전 다른 mobile B 의 orphan timer 가 5s 후 leader 를 강제 전환할\n      // 수 있는 cooldown bypass 시나리오 close.\n      const stalePending = this.pendingDecisions.get(machineId)\n      if (stalePending) {\n        this._clearTimer(stalePending.timer)\n        this.pendingDecisions.delete(machineId)\n      }\n\n      // [handoff diag 20260607 — H5] attach 시점에 아직 grace timer 가 무장돼 있으면 경고.\n      // 이 분기는 graceTimers 를 지우지 않으므로(Branch(1) 의 same-clientId 경로만 지움),\n      // 무장된 grace 는 ~5s 뒤 expireGrace 를 발화해 방금 leader=mobile 로 만든 이 attach 를\n      // 강등한다(= H5 orphan grace). 이 줄이 뜬 뒤 곧 'grace_expired reclaim prevLeader=mobile'\n      // 이 따라오면 H5 확정. (fix 후엔 이 줄이 떠도 강등이 안 따라와야 정상.)\n      if (this.graceTimers.has(machineId)) {\n        log.warn(\n          `handoff_diag attach_handoff orphan_grace_pending machine=${machineId} ` +\n            `newClient=${clientId} prevStateClient=${state.leader === 'mobile' ? state.mobileClientId : 'n/a'} ` +\n            `— grace NOT cleared, will reclaim in ~${AuthorityResolver.GRACE_PERIOD_MS}ms`\n        )\n      }\n\n      // [verify 20260607] desktop-priority 제거 후 \"모바일 attach = 항상 leader=mobile\"\n      // 를 배포 빌드 authority.log 에서 확인 가능하게 1줄. 제거된 no_handoff(policy=\n      // desktop-priority) 자리를 대체하는 positive 신호 — 이 줄이 보이면 정책 게이트 없이\n      // handoff 됐다는 뜻. (per-attach 1회라 노이즈 낮음.)\n      log.warn(\n        `handoff_diag attach_handoff (always mobile-priority) machine=${machineId} ` +\n          `client=${clientId} prevLeader=${state.leader}`\n      )\n\n      const prevCols =\n        hintCols ?? (state.leader === 'mobile' ? state.cols : undefined)\n      const prevRows =\n        hintRows ?? (state.leader === 'mobile' ? state.rows : undefined)\n\n      const next: AuthorityState = {\n        leader: 'mobile',\n        mobileClientId: clientId,\n        cols: prevCols,\n        rows: prevRows,\n        attachedAt: this.now(),\n        lastActivityAt: this.now(),\n      }\n      this.states.set(machineId, next)\n      this._broadcastViewerMode(machineId, 'handed_off', {\n        cols: prevCols,\n        rows: prevRows,\n      })\n      this.emitAuthorityChanged(\n        machineId,\n        'mobile',\n        prevCols ?? 80,\n        prevRows ?? 24,\n        clientId\n      )\n    } else {\n      // [Phase 1 fallback] handover 모달 요청 — CEO Fix-D rollback 시 사용.\n      const fallbackCols =\n        state.leader === 'mobile' ? state.cols : state.lastDesktopSize?.cols\n      const fallbackRows =\n        state.leader === 'mobile' ? state.rows : state.lastDesktopSize?.rows\n      this.requestHandover(\n        machineId,\n        clientId,\n        hintCols ?? fallbackCols ?? 80,\n        hintRows ?? fallbackRows ?? 24\n      )\n    }\n  }\n\n  /**\n   * mobile WS detach 시 ws-manager 가 호출.\n   *\n   * leader=mobile + 같은 clientId 면 grace timer 시작 + broadcast(disconnected).\n   * 그 외 (leader=desktop, 다른 clientId) 는 noop — 비권위 모바일 detach 는 무관.\n   */\n  onMobileDetach(machineId: string, clientId: string): void {\n    const state = this.states.get(machineId)\n    if (\n      !state ||\n      state.leader !== 'mobile' ||\n      state.mobileClientId !== clientId\n    ) {\n      return\n    }\n\n    this.deps.broadcast(\n      { type: 'terminal_mobile_unstable', machineId, state: 'disconnected' },\n      { skipMobile: true }\n    )\n    // [A2 spike] viewer_mode='detaching' + graceUntil — desktop placeholder 가 1Hz tick 으로\n    // \"Phone reconnecting… {n}s\" countdown 표시 (Design Fix-G).\n    const graceUntil = this.now() + AuthorityResolver.GRACE_PERIOD_MS\n    this._broadcastViewerMode(machineId, 'detaching', { graceUntil })\n    const timer = this._setTimer(\n      () => this.expireGrace(machineId),\n      AuthorityResolver.GRACE_PERIOD_MS\n    )\n    this.graceTimers.set(machineId, timer)\n  }\n\n  /**\n   * desktop renderer 의 양보 모달 응답 처리. 또는 handover timer 자동 발화 (default accept).\n   *\n   * 늦은 응답 (이미 timer 가 발화되어 pendingDecisions 가 비어있는 경우) 은 무시 —\n   * already-resolved guard. renderer 응답이 10s 후 도착하면 모달이 의미 없으므로.\n   */\n  onAuthorityDecision(machineId: string, accept: boolean): void {\n    const pending = this.pendingDecisions.get(machineId)\n    if (!pending) return\n    this._clearTimer(pending.timer)\n    this.pendingDecisions.delete(machineId)\n\n    // [/review fix P1] 권위 결정 시 stale grace timer 정리. 시나리오:\n    // M1 권위 → M1 detach (grace 5s 시작) → M2 attach (handover 요청) → user accept.\n    // grace timer 가 정리 안 되면 5s 후 expireGrace 가 leader 를 강제 desktop 으로\n    // 전이시켜 M2 가 권위 잃는 race. accept/reject 양쪽 모두 정리해야 안전.\n    const stale = this.graceTimers.get(machineId)\n    if (stale !== undefined) {\n      this._clearTimer(stale)\n      this.graceTimers.delete(machineId)\n    }\n\n    if (accept) {\n      // [A2 spike] mobile 변형은 attachedAt/lastActivityAt 필수 (discriminated union).\n      const now = this.now()\n      const next: AuthorityState = {\n        leader: 'mobile',\n        mobileClientId: pending.requesterClientId,\n        cols: pending.requestedCols,\n        rows: pending.requestedRows,\n        attachedAt: now,\n        lastActivityAt: now,\n      }\n      this.states.set(machineId, next)\n      this.emitAuthorityChanged(\n        machineId,\n        'mobile',\n        next.cols ?? 80,\n        next.rows ?? 24,\n        next.mobileClientId\n      )\n      // [A2 spike] viewer_mode='handed_off' broadcast — desktop UI 가 placeholder 로 swap.\n      this._broadcastViewerMode(machineId, 'handed_off', {\n        cols: next.cols,\n        rows: next.rows,\n      })\n    } else {\n      // mobile-rejected — leader=desktop 유지, 모바일은 비권위.\n      const cur = this.states.get(machineId) ?? { leader: 'desktop' as const }\n      // [A2 spike discriminated union] desktop 변형은 cols/rows 직접 갖지 않음. cur.cols/rows 는\n      // 이전이 mobile 이었다면 거기서 borrow, 아니면 lastDesktopSize 에서 borrow.\n      const fallbackCols =\n        cur.leader === 'mobile' ? cur.cols : cur.lastDesktopSize?.cols\n      const fallbackRows =\n        cur.leader === 'mobile' ? cur.rows : cur.lastDesktopSize?.rows\n      this.states.set(machineId, {\n        leader: 'desktop',\n        lastDesktopSize:\n          fallbackCols !== undefined && fallbackRows !== undefined\n            ? { cols: fallbackCols, rows: fallbackRows }\n            : undefined,\n      })\n      this.emitAuthorityChanged(\n        machineId,\n        'desktop',\n        fallbackCols ?? 80,\n        fallbackRows ?? 24,\n        undefined\n      )\n    }\n  }\n\n  /**\n   * mobile 측 takeover 요청 — desktop 에 handover request 재발행.\n   *\n   * 분기:\n   *   - leader=desktop → 새 handover 모달.\n   *   - leader=mobile + 같은 clientId → noop (이미 권위).\n   *   - leader=mobile + 다른 clientId → 새 handover 모달.\n   */\n  onMobileTakeoverRequest(machineId: string, clientId: string): void {\n    const state = this.states.get(machineId) ?? { leader: 'desktop' as const }\n    if (state.leader === 'mobile' && state.mobileClientId === clientId) return\n    const fallbackCols =\n      state.leader === 'mobile' ? state.cols : state.lastDesktopSize?.cols\n    const fallbackRows =\n      state.leader === 'mobile' ? state.rows : state.lastDesktopSize?.rows\n    this.requestHandover(\n      machineId,\n      clientId,\n      fallbackCols ?? 80,\n      fallbackRows ?? 24\n    )\n  }\n\n  /**\n   * 머신 권위 상태 조회 — 테스트/디버깅용. 외부 변경 금지 (readonly).\n   * state 가 없으면 initial (leader=desktop) 을 반환 — 호출자가 분기할 필요 없음.\n   */\n  getState(machineId: string): Readonly<AuthorityState> {\n    return this.states.get(machineId) ?? { leader: 'desktop' as const }\n  }\n\n  /**\n   * 머신 삭제 시 cleanup — states / graceTimers / pendingDecisions 모두 정리.\n   *\n   * [Review P1] ssh.removeMachine 호출 시 동반 호출되어야 함. 다른 머신은 영향 없음.\n   */\n  removeMachine(machineId: string): void {\n    const grace = this.graceTimers.get(machineId)\n    if (grace !== undefined) {\n      this._clearTimer(grace)\n      this.graceTimers.delete(machineId)\n    }\n    const pending = this.pendingDecisions.get(machineId)\n    if (pending !== undefined) {\n      this._clearTimer(pending.timer)\n      this.pendingDecisions.delete(machineId)\n    }\n    this.states.delete(machineId)\n    // [A2 spike] 머신 삭제 시 cooldown 도 정리.\n    this.takebackCooldownUntil.delete(machineId)\n  }\n\n  /**\n   * 앱 종료 시 cleanup — 모든 grace / handover timer 취소. states 도 비움.\n   * SSHManager.cleanup 에서 호출하면 누수 0.\n   */\n  destroy(): void {\n    for (const handle of this.graceTimers.values()) {\n      this._clearTimer(handle)\n    }\n    this.graceTimers.clear()\n    for (const entry of this.pendingDecisions.values()) {\n      this._clearTimer(entry.timer)\n    }\n    this.pendingDecisions.clear()\n    this.states.clear()\n    // [A2 spike] cooldown Map 도 정리.\n    this.takebackCooldownUntil.clear()\n    // [Bug #1 fix 20260526] heartbeat ticker Map 도 정리 — handle 누수 방지.\n    for (const handle of this._heartbeatTimers.values()) {\n      this._clearTimer(handle)\n    }\n    this._heartbeatTimers.clear()\n  }\n\n  // ─── A2 spike public methods (20260523) ────────────────────────────────────\n\n  /**\n   * Desktop Take Back — leader 강제 desktop 전환 + 5s cooldown + viewer_mode='interactive'.\n   *\n   * 호출 경로: `ws-manager.handleDesktopTakeback` 가 mobile WS 를 close code 4007 으로 종료한\n   * 직후 본 메서드를 호출. mobile 측에서는 close code → DisconnectReason.takenByDesktop →\n   * `KickedByDesktopBanner` 풀스크린 표시.\n   *\n   * [Eng Fix-M] grace timer + pendingDecisions 모두 정리. orphan handover 모달 race 가 5s 후\n   * cooldown bypass 로 leader 를 다른 mobile 에 줘버리는 시나리오 close.\n   */\n  forceDesktopLeader(machineId: string): void {\n    const grace = this.graceTimers.get(machineId)\n    if (grace !== undefined) {\n      this._clearTimer(grace)\n      this.graceTimers.delete(machineId)\n    }\n    const pending = this.pendingDecisions.get(machineId)\n    if (pending !== undefined) {\n      this._clearTimer(pending.timer)\n      this.pendingDecisions.delete(machineId)\n    }\n    // 이전 mobile state 의 cols/rows 를 lastDesktopSize 로 borrow 할 수도 있지만,\n    // takeback 은 desktop 사용자가 자기 화면 size 로 새로 시작하므로 ssh-manager 의 다음\n    // RESIZE (TerminalView 의 force-fit useEffect, C4) 가 정확한 size 를 결정한다. 여기서는\n    // lastDesktopSize 만 보존.\n    const prev = this.states.get(machineId)\n    const lastDesktopSize =\n      prev?.leader === 'desktop' ? prev.lastDesktopSize : undefined\n    this.states.set(machineId, {\n      leader: 'desktop',\n      lastDesktopSize,\n    })\n    this.takebackCooldownUntil.set(\n      machineId,\n      this.now() + AuthorityResolver.TAKEBACK_COOLDOWN_MS\n    )\n    this._broadcastViewerMode(machineId, 'interactive')\n    // 권위 변경 broadcast — 기존 invariant 유지.\n    this.emitAuthorityChanged(\n      machineId,\n      'desktop',\n      lastDesktopSize?.cols ?? 80,\n      lastDesktopSize?.rows ?? 24,\n      undefined\n    )\n  }\n\n  /**\n   * Mobile input 마다 lastActivityAt 갱신 — heartbeat 용.\n   *\n   * [Eng Fix-K] in-place mutation 금지 — `this.states.set(mid, {...})` 으로 새 객체 할당.\n   * `Readonly<AuthorityState>` 계약 유지 (`getState()` 의 caller 는 변경 0 가정).\n   *\n   * 호출처: ssh-manager 의 mobile input 처리 path. 1Hz throttle 로 viewer_mode 재방송은\n   * 별도 ticker (C2 의 pty-lifecycle 또는 ws-manager) 가 담당.\n   */\n  touchMobileActivity(machineId: string, clientId: string): void {\n    const s = this.states.get(machineId)\n    if (!s || s.leader !== 'mobile' || s.mobileClientId !== clientId) {\n      // [Option D 20260606 — adopt-on-input] leader 아닌 모바일의 input/keepalive 는\n      // \"복귀 신호\"로 해석한다. 실기기 RCA(authority.log 18:08): 모바일이 백그라운드 5s\n      // 초과로 grace 회수당한 뒤(leader=desktop) 복귀했는데 session_attach 재송신을\n      // 놓친 경우, 입력은 PTY 에 도달하지만(ws-manager 가 leader 무관하게 forward) 출력\n      // 흐름(viewer)이 모바일로 안 돌아와 \"먹통\"으로 보였다. drop 직전 attach 로그가 0\n      // 이었던 게 직접 증거. 여기서 onMobileAttach 로 위임해 leader→mobile 을 복구하면\n      // handed_off 재방송 + authority_changed 로 출력 흐름이 재개된다. mobile-only 재attach\n      // (PR #94)로는 못 막던 구조 — 폰은 grace 회수를 알 수단이 없으므로(viewer_mode 는\n      // desktop-internal) 데스크탑이 활동을 보고 흡수하는 게 구조적 정답(Option D).\n      //\n      // 가드: spike-off / takeback cooldown 은 adopt 하지 않고 기존처럼 drop. cooldown\n      // 이후엔 onMobileAttach 의 \"모바일 attach=즉시 leader\" 의미대로 모바일이 다시 leader.\n      const inCooldown =\n        this.now() < (this.takebackCooldownUntil.get(machineId) ?? 0)\n      if (this._a2SpikeEnabled && !inCooldown) {\n        // 마지막으로 알려진 viewport 를 hint 로 — desktop reclaim 후 state 는 mobile\n        // cols/rows 를 잃으므로 lastDesktopSize 로 폴백(80x24 강제 snap 방지). 실제 PTY\n        // resize 는 모바일이 재adopt 직후 보내는 terminal_resize 가 처리(여기선 강제 X).\n        const hintCols =\n          s === undefined\n            ? undefined\n            : s.leader === 'mobile'\n              ? s.cols\n              : s.lastDesktopSize?.cols\n        const hintRows =\n          s === undefined\n            ? undefined\n            : s.leader === 'mobile'\n              ? s.rows\n              : s.lastDesktopSize?.rows\n        log.warn(\n          `handoff_diag adopt_on_input (mobile-priority re-attach) machine=${machineId} ` +\n            `fromClient=${clientId} prevLeader=${s?.leader ?? 'none'} ` +\n            `prevClient=${s && s.leader === 'mobile' ? s.mobileClientId : 'n/a'}`\n        )\n        this.onMobileAttach(machineId, clientId, hintCols, hintRows)\n        return\n      }\n\n      // [handoff diag 20260606] adopt 불가(spike-off / takeback cooldown) 한 비권위 활동만\n      // 여기서 drop. (RC1 desync 케이스는 위 adopt 가 흡수.) relay 재연결로 데스크탑이\n      // clientId 를 새로 생성한 경우 등 진단용.\n      log.warn(\n        `handoff_diag activity_dropped (clientId mismatch) machine=${machineId} ` +\n          `fromClient=${clientId} leader=${s?.leader ?? 'none'} ` +\n          `stateClient=${s && s.leader === 'mobile' ? s.mobileClientId : 'n/a'}`\n      )\n      return\n    }\n    // dogfood 디버깅: lastActivityAt 갱신 매번 기록. M12 heartbeat broadcast bug\n    // (별도 ticker 미구현) 진단 위해 추가. 1초당 N회 (input 빈도) — log 부담 있음.\n    log.info(\n      `touchMobileActivity machine=${machineId} client=${clientId} t=${s.lastActivityAt}`\n    )\n    this.states.set(machineId, { ...s, lastActivityAt: this.now() })\n  }\n\n  /**\n   * Agent 측 PTY 가 mobile attach 중 죽었을 때 호출 — viewer_mode='error' broadcast.\n   *\n   * [Design Fix-E F1] desktop UI 가 HandoffPlaceholder 의 rose-tone error branch 로 swap →\n   * \"Connection lost — phone offline too\". 사용자가 takeback 또는 reconnect 결정 가능.\n   *\n   * 호출처: `pty-lifecycle.onPtyExit` 가 leader='mobile' 확인 후 본 메서드 호출 (C2).\n   */\n  handleAgentCrash(machineId: string): void {\n    const s = this.states.get(machineId)\n    if (!s || s.leader !== 'mobile') return\n    this._broadcastViewerMode(machineId, 'error', { reason: 'agent_crash' })\n  }\n\n  /**\n   * `ws-manager.session_attach` 핸들러가 cooldown 거부 분기에서 사용 — close code 4008\n   * (TAKEBACK_COOLDOWN) 으로 mobile WS 종료 결정.\n   */\n  isInTakebackCooldown(machineId: string): boolean {\n    return this.now() < (this.takebackCooldownUntil.get(machineId) ?? 0)\n  }\n\n  /**\n   * [Bug #3 fix 20260526] Cooldown 종료 epoch ms. `ws-manager.handleSessionAttach`\n   * 의 cooldown reject 분기가 `desktop.kick` frame 에 `cooldownUntil` 채울 때 사용.\n   * 모바일 `RejectedBanner` countdown 의 기준값.\n   *\n   * 반환값 `undefined` = cooldown 없음 (정상 attach 가능 상태).\n   */\n  getTakebackCooldownUntil(machineId: string): number | undefined {\n    return this.takebackCooldownUntil.get(machineId)\n  }\n\n  /**\n   * `ws-manager.session_attach` 가 진입 직후 호출 — viewer_mode='attaching' broadcast.\n   * Design D2-δ row 1 의 \"Phone connecting…\" overlay 트리거. resolver.onMobileAttach 호출\n   * 전에 발화되어야 핸드오프 직전 race window UX 가 자연스러움.\n   */\n  broadcastAttaching(machineId: string, cols?: number, rows?: number): void {\n    this._broadcastViewerMode(machineId, 'attaching', { cols, rows })\n  }\n\n  // ─── private ────────────────────────────────────────────────────────────────\n\n  /**\n   * handover 요청 발행 + pending 등록.\n   *\n   * [Review Q1] 이전 pending 이 있으면 cleanup + 첫째 클라이언트에 명시 통지 (현재 leader 재발행).\n   * 두 모바일이 5ms 차이로 attach 시 첫째 handover 가 둘째에 의해 덮어쓰이는 경우, 첫째\n   * 클라이언트는 \"모달 응답 대기 중\" 상태에서 영원히 대기하지 않도록.\n   */\n  private requestHandover(\n    machineId: string,\n    clientId: string,\n    cols: number,\n    rows: number\n  ): void {\n    const prev = this.pendingDecisions.get(machineId)\n    if (prev) {\n      this._clearTimer(prev.timer)\n      // 첫째 클라이언트에 cancel 시그널 — 현재 leader 의 authority_changed 재발행.\n      const cur = this.states.get(machineId) ?? { leader: 'desktop' as const }\n      const curCols =\n        cur.leader === 'mobile' ? cur.cols : cur.lastDesktopSize?.cols\n      const curRows =\n        cur.leader === 'mobile' ? cur.rows : cur.lastDesktopSize?.rows\n      this.emitAuthorityChanged(\n        machineId,\n        cur.leader,\n        curCols ?? 80,\n        curRows ?? 24,\n        cur.leader === 'mobile' ? cur.mobileClientId : undefined\n      )\n    }\n\n    const timer = this._setTimer(\n      () =>\n        this.onAuthorityDecision(\n          machineId,\n          AuthorityResolver.HANDOVER_DEFAULT_ACCEPT\n        ),\n      AuthorityResolver.HANDOVER_TIMEOUT_MS\n    )\n    this.pendingDecisions.set(machineId, {\n      timer,\n      requesterClientId: clientId,\n      requestedCols: cols,\n      requestedRows: rows,\n    })\n\n    this.deps.broadcast(\n      {\n        type: 'terminal_authority_request',\n        machineId,\n        requestedCols: cols,\n        requestedRows: rows,\n        timeoutMs: AuthorityResolver.HANDOVER_TIMEOUT_MS,\n      },\n      { skipMobile: true }\n    )\n  }\n\n  /**\n   * grace timer 만료 — 마지막 권위 모바일이 5s 안에 재연결하지 않은 경우.\n   *\n   * [Review A1] broadcast 순서: `terminal_mobile_unstable:expired` → state 전이 →\n   * `terminal_authority_changed` → applyResize (ssh.resize 가 `terminal_resize_ack`\n   * broadcast 함). 권위 변경이 사이즈 변경 *전에* renderer 에 도달해야 자연스러운 UX.\n   *\n   * [Review F1] applyResize 가 throw 해도 state 는 이미 desktop 으로 전이. try/catch\n   * 로 감싸 console.error 만 남기고 진행. 사용자가 다음 ResizeObserver fire 시 정상 복귀.\n   *\n   * [Bug fix 20260604] handed_off watchdog 의 desktop 회수 경로로도 호출된다 (grace timer\n   * 없이 직접). 위 `graceTimers.delete` + leader=desktop 가드가 grace-pending 아닌 호출도\n   * 안전하게 처리하므로 공유 reclaim 루틴으로 재사용한다.\n   */\n  private expireGrace(machineId: string): void {\n    this.graceTimers.delete(machineId)\n\n    // [/review fix P1 defense-in-depth] grace timer 가 onAuthorityDecision 에서\n    // 정리되지 않은 race window 에 발화하더라도, state 가 이미 desktop 으로 전이됐거나\n    // 다른 mobile leader 가 권위 잡은 경우라면 noop. 일반 expire (leader=mobile +\n    // mobileClientId 살아있는 경우) 만 정상 처리.\n    const cur = this.states.get(machineId)\n    // [handoff diag 20260607 — H5] grace-timer 회수를 watchdog 회수(handed_off watchdog\n    // expired, :993)·Take Back(forceDesktopLeader) 과 분리해 greppable 하게 1줄. 핵심 진단:\n    // prevLeader=mobile 이고 guardEarlyReturn=false 면 = \"백그라운드 detach 로 무장된 grace 가,\n    // 그 사이 재attach 된 모바일(같은/다른 clientId)을 5s 뒤 강등\" = H5(orphan grace) 발현이다.\n    // 원인은 onMobileAttach Branch(2) 가 pendingDecisions 만 지우고 graceTimers 는 안 지우는 것\n    // (:309-313 부근). watchdog 와 달리 이 raw grace 경로는 busy-UNgated(#29 는 watchdog 만 gate).\n    // 이 줄이 mobile prevLeader 로 자주 뜨면 fix = Branch(2)에서 graceTimers.delete 또는\n    // expireGrace 가드에 mobileClientId 비교 추가(현 가드는 leader==='desktop' 만 봄).\n    log.warn(\n      `grace_expired reclaim machine=${machineId} prevLeader=${cur?.leader ?? 'none'} ` +\n        `prevClient=${cur && cur.leader === 'mobile' ? cur.mobileClientId : 'n/a'} ` +\n        `guardEarlyReturn=${cur?.leader === 'desktop'}`\n    )\n    if (cur && cur.leader === 'desktop') {\n      return\n    }\n\n    this.deps.broadcast(\n      { type: 'terminal_mobile_unstable', machineId, state: 'expired' },\n      { skipMobile: true }\n    )\n\n    const last = this.deps.getLastDesktopSize(machineId)\n    const nextCols = last?.cols ?? 80\n    const nextRows = last?.rows ?? 24\n    // [A2 spike discriminated union] desktop 변형은 cols/rows 직접 갖지 않음.\n    this.states.set(machineId, {\n      leader: 'desktop',\n      lastDesktopSize: last ? { cols: nextCols, rows: nextRows } : undefined,\n    })\n    this.emitAuthorityChanged(\n      machineId,\n      'desktop',\n      nextCols,\n      nextRows,\n      undefined\n    )\n    // [A2 spike] viewer_mode='interactive' — desktop UI 가 placeholder unmount + xterm 복귀.\n    this._broadcastViewerMode(machineId, 'interactive')\n\n    if (last) {\n      try {\n        this.deps.applyResize(\n          machineId,\n          last.cols,\n          last.rows,\n          'desktop-restore'\n        )\n      } catch (err) {\n        // state 는 이미 desktop 으로 전이됨 — 다음 사용자 액션 시 정상 복귀.\n        log.error(\n          `applyResize on grace expire failed machine=${machineId}`,\n          err\n        )\n      }\n    }\n  }\n\n  /**\n   * `terminal_authority_changed` broadcast — receiver 별 isAuthority 계산.\n   *\n   * - renderer 수신: leader='desktop' 이면 true.\n   * - mobile (clientId 있는) 수신: leader='mobile' && clientId === mobileClientId.\n   * - relay (clientId 미상) 수신: 보수적으로 false fallback.\n   */\n  private emitAuthorityChanged(\n    machineId: string,\n    leader: AuthorityLeader,\n    cols: number,\n    rows: number,\n    mobileClientId: string | undefined\n  ): void {\n    const baseMsg: ServerMessage = {\n      type: 'terminal_authority_changed',\n      machineId,\n      leader,\n      isAuthority: false, // fan-out 시 receiver 별로 overwrite\n      cols,\n      rows,\n    }\n    this.deps.broadcast(baseMsg, {\n      perRecipient: recipient => {\n        if (recipient.kind === 'renderer') {\n          return { isAuthority: leader === 'desktop' }\n        }\n        // mobile — clientId 가 있고 mobile leader 와 일치하면 true\n        if (\n          leader === 'mobile' &&\n          mobileClientId !== undefined &&\n          recipient.clientId === mobileClientId\n        ) {\n          return { isAuthority: true }\n        }\n        return { isAuthority: false }\n      },\n    })\n  }\n\n  /**\n   * [A2 spike 20260523] `desktop.viewer_mode` 메시지 broadcast helper.\n   *\n   * env `VIEWER_MODE_SPIKE=false` 시 즉시 no-op — agent 재시작 + flag flip 으로 rollback.\n   * (CEO Fix-D: sunk cost mitigation 의 핵심 안전장치)\n   *\n   * desktop renderer 만 수신 — `skipMobile: true`. mobile 측은 본 message type 을 무시.\n   * 예외: `'rejected'` mode 는 거부된 모바일 자기 자신에게도 전달되어야 cooldown UI 가 떠야\n   * 하지만, 본 spike 에서는 모바일 close code 4008 (TAKEBACK_COOLDOWN) 만으로 동일 시각 효과\n   * 달성 (Phase 2 의 RejectedBanner 가 close code 기반). 따라서 항상 skipMobile.\n   *\n   * `mobileMeta` 는 `state.leader === 'mobile'` 일 때만 채움 — `'handed_off'` placeholder 의\n   * heartbeat / \"cols×rows\" 표시 데이터.\n   */\n  private _broadcastViewerMode(\n    machineId: string,\n    mode:\n      | 'interactive'\n      | 'attaching'\n      | 'handed_off'\n      | 'detaching'\n      | 'error'\n      | 'rejected',\n    meta?: {\n      cols?: number\n      rows?: number\n      graceUntil?: number\n      cooldownUntil?: number\n      reason?: string\n    }\n  ): void {\n    if (!this._a2SpikeEnabled) return\n    // [Bug #1 fix] mode 별 heartbeat ticker 관리. handed_off 진입 시 1Hz ticker 시작,\n    // 그 외 모든 mode 전환 시 즉시 stop. ticker 의 callback 이 본 메서드를 재호출하지만\n    // `_ensureHeartbeatTicker` 가 .has() 체크로 중복 set 방지 — 재진입 안전.\n    if (mode === 'handed_off') {\n      this._ensureHeartbeatTicker(machineId)\n    } else {\n      this._stopHeartbeatTicker(machineId)\n    }\n    const s = this.states.get(machineId)\n    const mobileMeta =\n      s?.leader === 'mobile'\n        ? {\n            cols: s.cols,\n            rows: s.rows,\n            attachedAt: s.attachedAt,\n            lastActivityAt: s.lastActivityAt,\n          }\n        : undefined\n    // dogfood 디버깅: 모든 viewer_mode broadcast 가시화 — A2 spike 의 핵심 이벤트.\n    log.info(\n      `broadcast viewer_mode machine=${machineId} mode=${mode}` +\n        (mobileMeta\n          ? ` lastActivityAt=${mobileMeta.lastActivityAt} cols=${mobileMeta.cols} rows=${mobileMeta.rows}`\n          : '') +\n        (meta?.graceUntil ? ` graceUntil=${meta.graceUntil}` : '') +\n        (meta?.cooldownUntil ? ` cooldownUntil=${meta.cooldownUntil}` : '') +\n        (meta?.reason ? ` reason=${meta.reason}` : '')\n    )\n    this.deps.broadcast(\n      {\n        type: 'desktop.viewer_mode',\n        machineId,\n        mode,\n        mobileMeta,\n        graceUntil: meta?.graceUntil,\n        cooldownUntil: meta?.cooldownUntil,\n        reason: meta?.reason,\n      },\n      { skipMobile: true }\n    )\n  }\n\n  /**\n   * Date.now 대체 — `AuthorityResolverDeps.now` 가 있으면 사용 (테스트 fake 시계).\n   */\n  private now(): number {\n    return this.deps.now?.() ?? Date.now()\n  }\n\n  /**\n   * [Bug #1 fix 20260526] handed_off heartbeat ticker — 매 1초 viewer_mode 재방송.\n   *\n   * 이미 timer 있으면 no-op (재진입 안전). `_broadcastViewerMode` 의 callback path 가 본\n   * 메서드를 다시 호출하지만 `_heartbeatTimers.has()` 체크로 중복 set 차단.\n   *\n   * tick 동작:\n   *   1. state 가 사라졌거나 leader != mobile → ticker stop + return (cleanup).\n   *   2. `_broadcastViewerMode('handed_off')` 호출 — state 의 최신 `lastActivityAt` 가 broadcast.\n   *   3. 다음 tick scheduling (1s 후).\n   *\n   * `_setTimer` 사용 — test fake clock 호환.\n   */\n  private _ensureHeartbeatTicker(machineId: string): void {\n    if (this._heartbeatTimers.has(machineId)) return\n    const tick = (): void => {\n      const s = this.states.get(machineId)\n      if (!s || s.leader !== 'mobile') {\n        this._heartbeatTimers.delete(machineId)\n        return\n      }\n      // [Bug fix 20260604 + Reclaim guard 20260606] watchdog — relay 경로엔 per-mobile\n      // detach 신호가 없어(HANDOFF_WATCHDOG_MS 주석) 모바일이 조용히 사라지면 본 ticker 가\n      // 영구 실행된다. 회수 조건을 3중으로 본다:\n      //   idleMs       = 모바일 무입력 시간 (lastActivityAt 기준).\n      //   ptyIdleMs    = PTY 무활동(출력/입력) 시간 (getLastPtyActivity 기준 — 응답 스트리밍\n      //                  /입력 중이면 ~0). 미주입 시 lastActivityAt 폴백 → 구 mobile-input-only(회귀 0).\n      //   busy         = 에이전트 턴 진행 중(claude_state) — 출력이 잠시 없어도 보호.\n      // protectedNow(=PTY 최근 출력 OR busy) 인 동안엔 회수하지 않아 \"응답 끝나기 전엔 안 뺏김\".\n      // 단 idleMs 가 절대 상한을 넘으면(연속출력 PTY + 폰 silent-vanish) 보호를 깨고 회수해\n      // desktop 복구를 보장한다. 회수 시 viewer_mode='interactive' 가 ticker 를 stop +\n      // cooldown 미설정이라 재attach kick 도 없다. 재스케줄 없이 return.\n      const now = this.now()\n      const idleMs = now - s.lastActivityAt\n      const ptyIdleMs =\n        now - (this.deps.getLastPtyActivity?.(machineId) ?? s.lastActivityAt)\n      const busy = this.deps.isSessionBusy?.(machineId) ?? false\n      const protectedNow =\n        ptyIdleMs <= AuthorityResolver.HANDOFF_WATCHDOG_MS || busy\n      // [H6 fix 20260607] busy(claude_state working/awaiting-approval)는 noisy-PTY(ptyIdleMs)\n      // 보다 강한 \"진짜 작업 중\" 신호다. 실로그(가이드 §0.3 H6)에서 busy=true + ptyIdleMs<1s\n      // 인데 10min HARD_CEILING 으로 회수돼 사용자가 작업 중 세션을 뺏긴 사례 확인. 따라서 busy\n      // 면 더 큰 상한(30min)을 적용해 정상 작업 턴을 보호하되, finite 로 유지해(claude 가 Stop\n      // 없이 죽어 'working' 잔류 시 busy=true 영구화 → stuck-handed_off 재발 방지) 복구는 보장.\n      const hardCeiling = busy\n        ? AuthorityResolver.HANDOFF_BUSY_HARD_CEILING_MS\n        : AuthorityResolver.HANDOFF_HARD_CEILING_MS\n      const reclaim =\n        idleMs > AuthorityResolver.HANDOFF_WATCHDOG_MS &&\n        (!protectedNow || idleMs > hardCeiling)\n      if (reclaim) {\n        log.warn(\n          `handed_off watchdog expired machine=${machineId} ` +\n            `idleMs=${idleMs} ptyIdleMs=${ptyIdleMs} busy=${busy} ` +\n            `ceiling=${hardCeiling} — reclaiming desktop`\n        )\n        this.expireGrace(machineId)\n        return\n      }\n      // 본 broadcast 의 분기가 _ensureHeartbeatTicker 를 재호출하지만 timer handle 이\n      // 아직 map 에 남아있어 (아래 set 직전) .has() = true → return — 무한 재귀 차단.\n      this._broadcastViewerMode(machineId, 'handed_off')\n      // [authority redelivery 20260609] handed_off heartbeat 의 _broadcastViewerMode 는\n      // skipMobile:true 라 데스크탑 renderer 전용 — 모바일엔 안 간다. 모바일에 leader=mobile\n      // 을 알리는 유일 메시지인 terminal_authority_changed 는 attach 시 1회뿐이라, 재진입\n      // churn 중 그 1회가 유실되면 모바일이 leader=null 로 영구 고착된다(복구 채널 부재 —\n      // RCA handoff_authority_redelivery). 본 tick 에서 권위도 함께 재방송해 유실을 ~1s 내\n      // 자가치유한다. fan-out perRecipient + 모바일 _authority value-equality dedupe 라 중복\n      // 수신은 무해(notify 0). s.leader === 'mobile' 은 위 가드에서 보장됨.\n      // cols/rows 는 undefined 가능 → onMobileAttach(:371-372) 와 동일 80/24 폴백.\n      this.emitAuthorityChanged(\n        machineId,\n        'mobile',\n        s.cols ?? 80,\n        s.rows ?? 24,\n        s.mobileClientId\n      )\n      const next = this._setTimer(tick, 1000)\n      this._heartbeatTimers.set(machineId, next)\n    }\n    const handle = this._setTimer(tick, 1000)\n    this._heartbeatTimers.set(machineId, handle)\n  }\n\n  /**\n   * handed_off 이탈 시 (interactive / attaching / error / rejected / detaching 전환) ticker stop.\n   * `_broadcastViewerMode` 가 mode != 'handed_off' 인 모든 경로에서 호출.\n   */\n  private _stopHeartbeatTicker(machineId: string): void {\n    const t = this._heartbeatTimers.get(machineId)\n    if (t === undefined) return\n    this._clearTimer(t)\n    this._heartbeatTimers.delete(machineId)\n  }\n}\n","import type { GitAction } from '@arva/shared/types'\r\n\r\n/**\r\n * Git ref(브랜치/이름) shell-injection 가드 — 화이트리스트 정규식.\r\n *\r\n * **SSH 경로 전용**이다. SSH 경로는 [buildGitCommand] 가 ref 를 shell 문자열로\r\n * 조립하므로 메타문자(`;` `$` `` ` `` 공백 등) 주입을 차단해야 한다. relay/local\r\n * 경로는 simple-git 이 ref 를 argv 로 넘겨 shell 이 없으므로 본 가드를 적용하지\r\n * 않는다 — 적용하면 Unicode/CJK 브랜치명(예: `기능/로그인`)을 부당하게 막는다.\r\n * (한때 양 경로 적용을 고려했으나 argv 경로엔 보안 이득이 없고 회귀만 유발해 철회.)\r\n * 호출부는 try 안에 둬야 한다 — throw 가 git_result{success:false} 로 표면화되도록.\r\n *\r\n * `-` 는 합법 브랜치명 문자라 허용되므로 leading-dash 옵션 주입(`--force`/`-D`)은\r\n * 본 가드 범위 밖이다 (checkout/create_branch 의 기존 한계, PR-A scope 아님).\r\n *\r\n * ssh-manager 의 instance state 에 의존하지 않는 순수 함수 — 테스트가 replica\r\n * 가 아닌 본 실제 구현을 직접 import 할 수 있도록 모듈 스코프로 추출한다\r\n * (autoplan F-E-8: Electron/ssh2 무거운 의존 로드 회피).\r\n */\r\nexport function validateGitRef(value: string, field: string): void {\r\n  if (!/^[a-zA-Z0-9._\\-/]+$/.test(value)) {\r\n    throw new Error(`Invalid ${field}: ${value}`)\r\n  }\r\n}\r\n\r\n/** git_result.errorCode 의 분류값. mobile/desktop 공유 스키마(types.ts) 와 동일. */\r\nexport type GitDeleteErrorCode =\r\n  | 'not_fully_merged'\r\n  | 'checked_out'\r\n  | 'not_found'\r\n  | 'generic'\r\n\r\n/**\r\n * git 브랜치 삭제 실패 메시지(simple-git Error.message 또는 SSH stdout+stderr)를\r\n * errorCode 로 분류한다. 모바일이 raw 영문 대신 본 code 로 l10n 메시지를 고르고\r\n * force-delete CTA 노출 여부를 판단한다 (autoplan F-E-5).\r\n *\r\n * 순수 함수 — instance state 없음. 테스트가 replica 가 아닌 본 실제 구현을 import\r\n * 한다 (F-E-8). git 의 영문 메시지 표현에 의존하므로 locale-independent 한\r\n * git 출력(`error: The branch 'x' is not fully merged.` 등)을 가정한다.\r\n */\r\nexport function classifyDeleteError(message: string): GitDeleteErrorCode {\r\n  const m = message.toLowerCase()\r\n  if (m.includes('not fully merged')) return 'not_fully_merged'\r\n  if (m.includes('checked out') || m.includes('used by worktree')) {\r\n    return 'checked_out'\r\n  }\r\n  if (m.includes('not found') || m.includes('no such branch')) {\r\n    return 'not_found'\r\n  }\r\n  return 'generic'\r\n}\r\n\r\n/**\r\n * SSH 경로용 git shell 명령 조립. 순수 함수 — instance state 없음.\r\n *\r\n * `default` 분기는 exhaustiveness 가드다: [GitAction] 유니온에 새 kind 가\r\n * 추가됐는데 case 가 누락되면 (1) 컴파일 단계에서 `never` 할당 실패로 잡고,\r\n * (2) 런타임에서도 `undefined` 명령을 조용히 실행하는 대신 throw 한다.\r\n * 기존엔 default 가 없어 미지원 kind 가 `undefined` 를 반환 → `execSsh(undefined)`\r\n * → 조용한 실패로 이어질 수 있었다 (autoplan F-CEO-2 / F-E-8).\r\n */\r\nexport function buildGitCommand(action: GitAction, cwd: string): string {\r\n  const cd = `cd ${JSON.stringify(cwd)}`\r\n  switch (action.kind) {\r\n    case 'status':\r\n      return (\r\n        `${cd} && ` +\r\n        `echo ___GIT_REPO___ && git rev-parse --is-inside-work-tree 2>/dev/null && ` +\r\n        `echo ___BRANCH___ && git rev-parse --abbrev-ref HEAD 2>/dev/null && ` +\r\n        `echo ___STATUS___ && git status --porcelain 2>/dev/null && ` +\r\n        `echo ___BRANCHES___ && git branch -vva 2>/dev/null && ` +\r\n        `echo ___AHEAD_BEHIND___ && git rev-list --count HEAD...@{upstream} 2>/dev/null || true`\r\n      )\r\n    case 'checkout':\r\n      return `${cd} && git checkout ${JSON.stringify(action.branch)} 2>&1`\r\n    case 'create_branch':\r\n      if (action.checkout) {\r\n        return `${cd} && git checkout -b ${JSON.stringify(action.name)} 2>&1`\r\n      }\r\n      return `${cd} && git branch ${JSON.stringify(action.name)} 2>&1`\r\n    case 'delete_branch':\r\n      // force(-D) 는 미머지 브랜치도 삭제 (커밋 유실 가능). safe(-d) 는 미머지면 git 이\r\n      // 거부 → stderr \"not fully merged\" → ssh-manager 가 errorCode 로 분류해 모바일이\r\n      // force CTA 를 띄운다. defaultBranch/active 거부는 ssh-manager 의 권한 가드 담당.\r\n      return `${cd} && git branch ${action.force ? '-D' : '-d'} ${JSON.stringify(action.branch)} 2>&1`\r\n    case 'pull':\r\n      return `${cd} && git pull 2>&1`\r\n    case 'push':\r\n      return `${cd} && git push 2>&1`\r\n    case 'log': {\r\n      // 탭 구분: hash<TAB>shortHash<TAB>author<TAB>dateRelative<TAB>refs<TAB>subject\r\n      const limit = Math.max(1, Math.min(action.limit | 0, 200))\r\n      const fmt = '%H%x09%h%x09%an%x09%ar%x09%D%x09%s'\r\n      return `${cd} && git log --pretty=format:'${fmt}' -n ${limit} 2>/dev/null || true`\r\n    }\r\n    default: {\r\n      const _exhaustive: never = action\r\n      throw new Error(\r\n        `Unsupported git action: ${(_exhaustive as GitAction).kind}`\r\n      )\r\n    }\r\n  }\r\n}\r\n","import { exec } from 'child_process'\n\nimport type { RunningAgentKind } from '@arva/shared/types'\nimport { createLogger } from '../lib/logger'\n\nconst log = createLogger('agent-detector')\n\n/**\n * [Cockpit] PTY 의 자식 프로세스 트리에서 실행 중인 AI CLI 를 탐지한다.\n *\n * 왜 데스크탑에서만 가능한가: claude TUI 가 PTY stdin 을 점유하면 모바일이 보내는\n * 어떤 명령도 shell 이 아니라 claude 입력칸에 타이핑된다 — 모바일 측 bash 탐지는\n * 구조적으로 불가능하다. 데스크탑은 PTY 를 직접 소유(`proc.pid`)하므로 OS 프로세스\n * 트리를 조회해 신뢰성 있게 판정할 수 있다.\n *\n * 범위: **로컬 PTY 만**. SSH 원격 PTY 는 `pid` 가 원격 호스트의 것이라 본 OS 조회로\n * 닿지 않는다 → 호출자가 local 머신에 대해서만 호출하고, 그 외는 'none' 으로 둔다.\n *\n * @param pid 로컬 PTY 마스터 프로세스 PID (node-pty `proc.pid`).\n * @returns 자식 트리에서 발견된 첫 agent 종류. 없으면 'none'.\n */\nexport async function detectRunningAgent(\n  pid: number\n): Promise<RunningAgentKind> {\n  if (!Number.isInteger(pid) || pid <= 0) return 'none'\n  try {\n    const names =\n      process.platform === 'win32'\n        ? await listDescendantNamesWindows(pid)\n        : await listDescendantNamesUnix(pid)\n    const detected = classifyAgent(names)\n    log.info(\n      `[cockpit] detect pid=${pid} descendants=${names.length} -> ${detected}`\n    )\n    return detected\n  } catch (err) {\n    log.warn(\n      `[cockpit] detect failed pid=${pid}: ${(err as Error).message}`\n    )\n    return 'none'\n  }\n}\n\n/**\n * 프로세스 이름 목록에서 알려진 AI CLI 를 식별한다.\n *\n * 우선순위 claude > codex > gemini (현재는 claude 만 의미). 이름 매칭은 대소문자\n * 무시 + `.exe`(Windows) / 경로 prefix 를 견디도록 substring 검사.\n */\nexport function classifyAgent(processNames: string[]): RunningAgentKind {\n  const joined = processNames.join('\\n').toLowerCase()\n  // 'claude' 단어 경계 — node 가 'claude' 스크립트를 실행하면 보통 명령줄에 'claude'\n  // 가 그대로 노출된다. cmd 까지 보는 Unix 경로(-o args)에서 정확도가 높다.\n  if (/\\bclaude\\b/.test(joined) || joined.includes('claude')) return 'claude'\n  if (joined.includes('codex')) return 'codex'\n  if (joined.includes('gemini')) return 'gemini'\n  return 'none'\n}\n\n/**\n * Unix: `ps` 로 전체 프로세스(ppid + cmd)를 받아 pid 의 자손 트리 명령줄을 모은다.\n *\n * `-o args` 까지 보는 이유: claude 는 node 로 실행될 때 `node .../claude` 형태라\n * comm(=node) 만으로는 안 잡힌다. 전체 명령줄을 봐야 'claude' 가 보인다.\n */\nasync function listDescendantNamesUnix(pid: number): Promise<string[]> {\n  const out = await execText('ps -eo pid=,ppid=,args=')\n  const rows: { pid: number; ppid: number; cmd: string }[] = []\n  for (const line of out.split('\\n')) {\n    const m = line.match(/^\\s*(\\d+)\\s+(\\d+)\\s+(.*)$/)\n    if (!m) continue\n    rows.push({ pid: Number(m[1]), ppid: Number(m[2]), cmd: m[3] })\n  }\n  return collectDescendantCmds(pid, rows)\n}\n\n/**\n * Windows: WMIC 로 (ProcessId, ParentProcessId, CommandLine) 를 받아 자손 트리를 모은다.\n *\n * WMIC 는 deprecated 지만 모든 Win10/11 에 존재. CommandLine 까지 받아 `node ... claude`\n * 패턴을 잡는다. CSV 파싱은 CommandLine 에 콤마가 섞이므로 컬럼 위치 기반으로 처리.\n */\nasync function listDescendantNamesWindows(pid: number): Promise<string[]> {\n  const out = await execText(\n    'wmic process get ProcessId,ParentProcessId,CommandLine /format:csv'\n  )\n  const rows: { pid: number; ppid: number; cmd: string }[] = []\n  const lines = out\n    .split('\\n')\n    .map(l => l.trim())\n    .filter(Boolean)\n  // CSV 헤더: Node,CommandLine,ParentProcessId,ProcessId\n  for (const line of lines) {\n    if (line.startsWith('Node,')) continue\n    // 뒤에서부터 안전: 마지막 2필드가 PPID,PID (숫자), 그 앞 전부가 CommandLine.\n    const parts = line.split(',')\n    if (parts.length < 4) continue\n    const procId = Number(parts[parts.length - 1])\n    const parentId = Number(parts[parts.length - 2])\n    const cmd = parts.slice(1, parts.length - 2).join(',')\n    if (!Number.isInteger(procId)) continue\n    // 진단: root pid 의 직접 자식이 0개일 때(ConPTY 헬퍼로 체인 끊김 의심) 원인 파악용.\n    // claude 띄운 상태 재테스트 시 이 로그로 실제 트리 구조를 확인한다.\n    if (parentId === pid || procId === pid) {\n      log.info(\n        `[cockpit] detect-trace pid=${pid} matched row: proc=${procId} parent=${parentId} cmd=\"${cmd.slice(0, 80)}\"`\n      )\n    }\n    rows.push({ pid: procId, ppid: parentId, cmd })\n  }\n  return collectDescendantCmds(pid, rows)\n}\n\n/**\n * `rootPid` 의 모든 자손 프로세스 명령줄을 BFS 로 수집한다. root 자신은 shell 이라\n * 제외하고 자손만 본다 (shell 의 cmd 에 'claude' 가 우연히 없도록).\n *\n * 순환 ppid (이론상 OS 가 만들 일은 없지만 방어) 는 `seen` set 으로 차단.\n * export 이유: 단위 테스트가 OS 호출 없이 트리 수집 로직만 검증하기 위함.\n */\nexport function collectDescendantCmds(\n  rootPid: number,\n  rows: { pid: number; ppid: number; cmd: string }[]\n): string[] {\n  const childrenByParent = new Map<number, typeof rows>()\n  for (const r of rows) {\n    const arr = childrenByParent.get(r.ppid) ?? []\n    arr.push(r)\n    childrenByParent.set(r.ppid, arr)\n  }\n  const cmds: string[] = []\n  const queue = [...(childrenByParent.get(rootPid) ?? [])]\n  const seen = new Set<number>()\n  for (let i = 0; i < queue.length; i++) {\n    const node = queue[i]\n    if (seen.has(node.pid)) continue\n    seen.add(node.pid)\n    cmds.push(node.cmd)\n    const kids = childrenByParent.get(node.pid)\n    if (kids) queue.push(...kids)\n  }\n  return cmds\n}\n\n/** `exec` 를 Promise<string>(stdout) 로 감싼다. 1.5s timeout — 폴링이라 빨리 포기. */\nfunction execText(command: string): Promise<string> {\n  return new Promise((resolve, reject) => {\n    exec(\n      command,\n      { timeout: 1500, maxBuffer: 4 * 1024 * 1024 },\n      (err, stdout) => {\n        if (err) reject(err)\n        else resolve(stdout)\n      }\n    )\n  })\n}\n","import fs from 'fs'\nimport os from 'os'\nimport path from 'path'\n\nimport { createLogger } from '../lib/logger'\n\nimport type { TranscriptMessage } from '@arva/shared/types'\n\nconst log = createLogger('agent-sessions')\n\n/** transcript 메시지 1건당 텍스트 길이 상한 (초과분은 잘라 `…` 부착) — 거대 붙여넣기 방어. */\nconst TRANSCRIPT_TEXT_CAP = 4000\n\n/**\n * [Cockpit] claude 세션 1건의 메타데이터 — 모바일 \"세션 선택/이어하기\" UI 데이터.\n */\nexport interface AgentSessionMeta {\n  /** session_id (= jsonl 파일명, `--resume` 인자). */\n  id: string\n  /** 마지막 수정 시각 (epoch ms) — 정렬 기준. 가장 큰 값 = 현재/직전 세션. */\n  mtime: number\n  /** 첫 user 메시지 일부 (목록 제목용, 최대 80자). 없으면 빈 문자열. */\n  preview: string\n}\n\n/**\n * [Cockpit] cwd 의 claude 세션 목록을 조회한다.\n *\n * claude 는 세션을 `~/.claude/projects/<cwd-슬러그>/<session-id>.jsonl` 로 저장한다.\n * 슬러그 = cwd 의 `/`,`\\`,`:` 를 모두 `-` 로 치환한 문자열\n * (예: `C:\\Users\\me\\proj` → `C--Users-me-proj`).\n *\n * 반환은 mtime 내림차순 — [0] 이 가장 최근(=현재 터미널에서 돌던 세션일 가능성).\n * 디렉토리 부재/읽기 실패는 빈 배열 (세션 없음과 동일 취급, fail-safe).\n *\n * @param cwd 절대 경로 작업 디렉토리.\n * @param limit 최대 반환 개수 (기본 20). 오래된 세션은 잘라낸다.\n */\nexport function listAgentSessions(cwd: string, limit = 20): AgentSessionMeta[] {\n  try {\n    const root = claudeProjectsRoot()\n    const slug = slugifyCwd(cwd)\n    const dir = path.join(root, slug)\n    log.info(`[cockpit] listAgentSessions cwd=${cwd} slug=${slug}`)\n    if (!fs.existsSync(dir)) {\n      log.info(`[cockpit] sessions dir not found: ${dir}`)\n      // 디버그: 슬러그 불일치 시 실제 존재하는 디렉토리 후보를 찍어 원인 추적을 돕는다.\n      try {\n        const siblings = fs.existsSync(root) ? fs.readdirSync(root) : []\n        const hint = siblings.filter(d => {\n          const a = d.toLowerCase().replace(/-/g, '')\n          const b = slug.toLowerCase().replace(/-/g, '')\n          return a.includes(b.slice(-12)) || b.includes(a.slice(-12))\n        })\n        log.info(\n          `[cockpit] projects root has ${siblings.length} dirs; near-matches=${JSON.stringify(hint)}`\n        )\n      } catch {\n        // ignore (디버그 로그 실패는 무시)\n      }\n      return []\n    }\n    const entries = fs\n      .readdirSync(dir)\n      .filter(f => f.endsWith('.jsonl'))\n      .map(f => {\n        const full = path.join(dir, f)\n        const stat = fs.statSync(full)\n        return {\n          id: f.replace(/\\.jsonl$/, ''),\n          mtime: stat.mtimeMs,\n          full,\n        }\n      })\n      .sort((a, b) => b.mtime - a.mtime)\n      .slice(0, limit)\n\n    return entries.map(e => ({\n      id: e.id,\n      mtime: Math.floor(e.mtime),\n      preview: firstUserMessage(e.full),\n    }))\n  } catch (err) {\n    log.warn(`[cockpit] listAgentSessions failed cwd=${cwd}: ${(err as Error).message}`)\n    return []\n  }\n}\n\n/**\n * [Cockpit 신선도] 디스크 세션 목록에, 데스크탑이 아는 running 세션을 합류시킨다.\n *\n * 방금 콕핏으로 연 장수명 세션은 첫 턴까지 `.jsonl` 을 flush 하지 않아 [listAgentSessions]\n * (디스크 스캔)에서 누락된다. 데스크탑은 그 sessionId 를 in-memory 로 이미 알고 있으므로\n * (AgentRunner.sessions Map), 디스크에 **없는** running id 만 `{ id, mtime: now, preview: '' }`\n * 로 합류시켜 picker 가 \"현재 세션\"을 즉시 보이게 한다.\n *\n * 디스크에 **이미 있는** running id 는 디스크 항목(실제 mtime/preview 보유)을 그대로 둔다 —\n * now 로 덮어쓰면 미리보기를 잃는다. 결과는 id 로 dedupe, mtime 내림차순 정렬(running 항목은\n * now 라 보통 최상단). `agent.sessions` 스키마는 불변이라 모바일 무영향 — 목록만 더 채워진다.\n *\n * @param disk       [listAgentSessions] 결과(디스크 스캔, 이미 mtime 내림차순).\n * @param runningIds 데스크탑이 보유한 running sessionId 목록(AgentRunner.getActiveSessionIds).\n * @param now        running-only 항목에 부여할 mtime(= Date.now(), 최상단 정렬용).\n */\nexport function mergeRunningSessions(\n  disk: AgentSessionMeta[],\n  runningIds: string[],\n  now: number\n): AgentSessionMeta[] {\n  const seen = new Set(disk.map(s => s.id))\n  const merged = [...disk]\n  for (const id of runningIds) {\n    if (seen.has(id)) continue // 디스크 항목(실제 mtime/preview)을 우선 — 덮어쓰지 않음\n    seen.add(id) // runningIds 내부 중복 방어\n    merged.push({ id, mtime: now, preview: '' })\n  }\n  return merged.sort((a, b) => b.mtime - a.mtime)\n}\n\n/**\n * [Cockpit 신선도] 비어 있는 세션 목록을 flush 타이밍 창 동안 **bounded 재스캔**한다.\n *\n * 방금 시작한 claude 세션은 `.jsonl` 을 디스크에 flush 하기 전이라 [listAgentSessions] 가\n * 0건을 낼 수 있다(`agent.sessions.list` 는 1회성 스냅샷이라 그 뒤 갱신 안 됨). `delaysMs`\n * 시점마다 `rescan()` 을 다시 호출해, **처음으로 비어있지 않은 결과**가 나오면 `onSurfaced`\n * 로 알리고 **남은 재스캔을 취소**한다(조기중단). 모두 비면 조용히 종료한다 — 빈 목록 재방출은\n * 무의미하므로 추가 broadcast 를 하지 않는다.\n *\n * 본체(`broadcastAgentSessions`)가 아니라 여기로 분리한 이유: 타이머 스케줄/취소/조기중단\n * 불변식을 OS·파일시스템·Electron 의존 없이 fake timer 로 단위 검증하기 위함(SSHManager 는\n * 생성자가 디스크를 건드려 직접 인스턴스화가 비싸다).\n *\n * @param opts.delaysMs   재스캔 지연(ms) 목록. 길이 = 최대 재스캔 횟수.\n * @param opts.rescan     재스캔 함수(= `listAgentSessions(cwd)`).\n * @param opts.onSurfaced 비어있지 않은 결과가 처음 나왔을 때 1회 호출(= 재방출).\n * @returns 취소 함수. 새 요청/disconnect 시 호출해 남은 타이머를 모두 해제한다.\n */\nexport function scheduleSessionRescan(opts: {\n  delaysMs: number[]\n  rescan: () => AgentSessionMeta[]\n  onSurfaced: (sessions: AgentSessionMeta[]) => void\n}): () => void {\n  const handles: Array<ReturnType<typeof setTimeout>> = []\n  const cancel = (): void => {\n    for (const h of handles) clearTimeout(h)\n    handles.length = 0\n  }\n  for (const delay of opts.delaysMs) {\n    handles.push(\n      setTimeout(() => {\n        const sessions = opts.rescan()\n        if (sessions.length > 0) {\n          opts.onSurfaced(sessions)\n          cancel() // surface 됨 — 남은 재스캔 불필요.\n        }\n      }, delay)\n    )\n  }\n  return cancel\n}\n\n/**\n * [Cockpit] 한 세션의 이전 대화(transcript)를 읽어 user/assistant **텍스트만** 추린다.\n *\n * claude `-p --resume <id>` 출력에는 새 턴만 나오고 과거 대화는 재생되지 않으므로,\n * 모바일이 \"세션 이어하기\" 시 화면을 채우려면 디스크의 `.jsonl` 을 직접 읽어야 한다\n * (claude 자신이 읽는 그 파일). tool_use/tool_result/thinking/attachment 등 노이즈는\n * 모두 제거하고 prose 텍스트만 남긴다.\n *\n * 반환은 **시간순(오래된 것 → 최신)**. 텍스트 메시지가 [limit] 을 넘으면 **최근분만**\n * 담는다(앞부분 잘림 — 이어하기 맥락엔 최근이 더 중요). 파일 부재/파싱 실패는 빈 배열.\n *\n * @param cwd 절대 경로 작업 디렉토리 (슬러그 변환 후 파일 경로 구성).\n * @param sessionId 세션 UUID (= jsonl 파일명).\n * @param limit 최대 반환 메시지 수 (기본 40).\n */\nexport function readSessionTranscript(\n  cwd: string,\n  sessionId: string,\n  limit = 40\n): TranscriptMessage[] {\n  try {\n    const file = path.join(claudeProjectsRoot(), slugifyCwd(cwd), `${sessionId}.jsonl`)\n    if (!fs.existsSync(file)) {\n      log.info(`[cockpit] transcript file not found: ${file}`)\n      return []\n    }\n    const raw = fs.readFileSync(file, 'utf-8')\n    const out: TranscriptMessage[] = []\n    for (const line of raw.split(/\\r?\\n/)) {\n      const trimmed = line.trim()\n      if (!trimmed) continue\n      let obj: Record<string, unknown>\n      try {\n        obj = JSON.parse(trimmed)\n      } catch {\n        continue\n      }\n      const role: 'user' | 'assistant' | null =\n        obj.type === 'user' ? 'user' : obj.type === 'assistant' ? 'assistant' : null\n      if (!role) continue\n      const message = obj.message as { content?: unknown } | undefined\n      let text = extractContentText(message?.content).trim()\n      if (!text) continue // tool_use/tool_result/thinking 만 있는 턴은 스킵\n      if (text.length > TRANSCRIPT_TEXT_CAP) {\n        text = `${text.slice(0, TRANSCRIPT_TEXT_CAP)}…`\n      }\n      const tsRaw = obj.timestamp\n      const ts = typeof tsRaw === 'string' ? Date.parse(tsRaw) || 0 : 0\n      out.push({ role, text, ts })\n    }\n    const sliced = out.length > limit ? out.slice(out.length - limit) : out\n    log.info(\n      `[cockpit] readSessionTranscript session=${sessionId} parsed=${out.length} returned=${sliced.length}`\n    )\n    return sliced\n  } catch (err) {\n    log.warn(\n      `[cockpit] readSessionTranscript failed session=${sessionId}: ${(err as Error).message}`\n    )\n    return []\n  }\n}\n\n/**\n * [Push-Notif Phase 1] transcript 파일을 **절대 경로로 직접** 읽어 tail 컨텍스트를 추출한다.\n *\n * Claude 훅이 `transcript_path` 를 절대 경로로 주므로 슬러그 재구성이 불필요하다 — 이는\n * 단순 단순화가 아니라 **필수**다. `.ccs` 인스턴스/커스텀 `CLAUDE_CONFIG_DIR` 환경에서는\n * projects 루트가 `~/.claude/projects` 가 아니라 `~/.ccs/instances/<i>/projects` 라\n * [slugifyCwd]+[claudeProjectsRoot] 조합(= [readSessionTranscript])이 빈 결과를 낸다\n * (Phase 0 실측). 본 함수는 그 루트 의존을 우회한다.\n *\n * 파일을 끝에서부터 훑어 (1) 마지막 assistant 텍스트, (2) 마지막 `tool_use`(name + 입력\n * 미리보기)를 찾는다 — 후자는 permission_prompt 알림의 \"무엇을 허용?\" 본문이 된다.\n * 파일 부재/파싱 실패는 `{ lastAssistantText: '' }` (fail-safe).\n *\n * @param transcriptPath 훅 stdin 의 `transcript_path` (절대 경로).\n */\nexport function readTranscriptTailByPath(transcriptPath: string): {\n  lastAssistantText: string\n  lastToolUse?: { name: string; inputPreview: string }\n} {\n  try {\n    if (!transcriptPath || !fs.existsSync(transcriptPath)) {\n      return { lastAssistantText: '' }\n    }\n    const lines = fs\n      .readFileSync(transcriptPath, 'utf-8')\n      .split(/\\r?\\n/)\n      .filter(l => l.trim())\n    let lastAssistantText = ''\n    let lastToolUse: { name: string; inputPreview: string } | undefined\n    for (let i = lines.length - 1; i >= 0; i--) {\n      let obj: Record<string, unknown>\n      try {\n        obj = JSON.parse(lines[i])\n      } catch {\n        continue\n      }\n      if (obj.type !== 'assistant') continue\n      const content = (obj.message as { content?: unknown } | undefined)?.content\n      if (!lastToolUse && Array.isArray(content)) {\n        const tu = content.find(\n          c =>\n            c &&\n            typeof c === 'object' &&\n            (c as { type?: unknown }).type === 'tool_use'\n        ) as { name?: unknown; input?: unknown } | undefined\n        if (tu) {\n          lastToolUse = {\n            name: typeof tu.name === 'string' ? tu.name : 'tool',\n            inputPreview: previewToolInput(tu.input),\n          }\n        }\n      }\n      if (!lastAssistantText) {\n        const t = extractContentText(content).trim()\n        if (t) lastAssistantText = t\n      }\n      if (lastAssistantText && lastToolUse) break\n    }\n    return { lastAssistantText, lastToolUse }\n  } catch {\n    return { lastAssistantText: '' }\n  }\n}\n\n/**\n * `tool_use.input` 에서 사람이 읽을 짧은 미리보기를 뽑는다 (최대 200자).\n *\n * Bash 의 `command`, 파일 도구의 `file_path`/`path`, 검색의 `pattern`/`url` 같은 흔한\n * 1차 키를 우선 노출하고, 없으면 JSON 직렬화로 fallback. risk 분류/알림 본문 재료다.\n */\nfunction previewToolInput(input: unknown): string {\n  if (!input || typeof input !== 'object') return ''\n  const o = input as Record<string, unknown>\n  const primary =\n    o.command ?? o.file_path ?? o.path ?? o.pattern ?? o.url ?? o.description\n  const s = typeof primary === 'string' ? primary : JSON.stringify(input)\n  return s.length > 200 ? `${s.slice(0, 200)}…` : s\n}\n\n/**\n * jsonl 메시지의 `content`(문자열 또는 블록 배열)에서 **text 블록만** 이어붙인다.\n *\n * 배열인 경우 `{type:'text', text}` 블록만 취하고 tool_use/tool_result/thinking 은 버린다.\n * 문자열이면 그대로 반환. 그 외(undefined 등)는 빈 문자열.\n */\nfunction extractContentText(content: unknown): string {\n  if (typeof content === 'string') return content\n  if (!Array.isArray(content)) return ''\n  return content\n    .map(c => {\n      if (c && typeof c === 'object' && (c as { type?: unknown }).type === 'text') {\n        const text = (c as { text?: unknown }).text\n        return typeof text === 'string' ? text : ''\n      }\n      return ''\n    })\n    .filter(Boolean)\n    .join('\\n')\n}\n\n/**\n * `~/.claude/projects` 루트 — **크로스 플랫폼**.\n *\n * `os.homedir()` + `path.join` 으로 OS 별 정확히 해석:\n *   Windows = `C:\\Users\\<u>\\.claude\\projects`, macOS = `/Users/<u>/.claude/projects`,\n *   Linux = `/home/<u>/.claude/projects`. 홈 경로/구분자 하드코딩 금지 (CLAUDE.md 규칙).\n */\nfunction claudeProjectsRoot(): string {\n  return path.join(os.homedir(), '.claude', 'projects')\n}\n\n/**\n * cwd → claude 프로젝트 디렉토리 슬러그. **알파벳/숫자가 아닌 모든 문자 → `-`**.\n *\n * claude 본체는 `/`,`\\`,`:` 뿐 아니라 **`_`(언더스코어)·`.`·공백 등 비영숫자 문자를\n * 모두 `-`로 치환**한다. 예: `C:\\...\\claude_code_mobile` →\n * `C--Users-...-claude-code-mobile` (`_` 도 `-`). 처음엔 `[/\\\\:]` 만 치환해서\n * `claude_code_mobile` 의 언더스코어를 놓쳐 디렉토리를 못 찾는 버그가 있었음\n * (2026-06-01 Windows 실기기 로그로 확인 후 `[^a-zA-Z0-9]` 로 수정).\n *\n * **크로스 플랫폼 주의**: macOS 는 `/Users/me/my_proj` → `-Users-me-my-proj` 가 된다\n * (선행 `/` 도 `-`). claude 버전에 따라 규칙이 바뀔 수 있으므로 불일치 시 세션 목록이\n * 빈 배열로 나올 뿐 — 크래시는 없음(fail-safe). listAgentSessions 가 dir 부재 시\n * projects 루트의 near-match 디렉토리를 로그로 남기므로 슬러그 회귀를 즉시 추적 가능.\n */\nexport function slugifyCwd(cwd: string): string {\n  return cwd.replace(/[^a-zA-Z0-9]/g, '-')\n}\n\n/**\n * jsonl 의 첫 `type:\"user\"` 메시지 content 를 미리보기 문자열로 추출 (최대 80자).\n *\n * 큰 파일 전체를 읽지 않도록 앞부분만(최대 64KB) 읽어 줄 단위 파싱한다 — 첫 user\n * 메시지는 보통 파일 맨 앞에 있다. 파싱 실패 / user 메시지 부재는 빈 문자열.\n */\nfunction firstUserMessage(filePath: string): string {\n  let fd: number | null = null\n  try {\n    fd = fs.openSync(filePath, 'r')\n    const buf = Buffer.alloc(64 * 1024)\n    const bytes = fs.readSync(fd, buf, 0, buf.length, 0)\n    const head = buf.toString('utf-8', 0, bytes)\n    for (const line of head.split('\\n')) {\n      const trimmed = line.trim()\n      if (!trimmed) continue\n      let obj: Record<string, unknown>\n      try {\n        obj = JSON.parse(trimmed)\n      } catch {\n        continue\n      }\n      if (obj.type !== 'user') continue\n      const message = obj.message as { content?: unknown } | undefined\n      const content = message?.content\n      const text =\n        typeof content === 'string'\n          ? content\n          : Array.isArray(content)\n            ? content\n                .map(c =>\n                  c && typeof c === 'object' && 'text' in c\n                    ? String((c as { text: unknown }).text)\n                    : ''\n                )\n                .join(' ')\n            : ''\n      const oneLine = text.replace(/\\s+/g, ' ').trim()\n      if (oneLine) return oneLine.slice(0, 80)\n    }\n    return ''\n  } catch {\n    return ''\n  } finally {\n    if (fd !== null) {\n      try {\n        fs.closeSync(fd)\n      } catch {\n        // ignore\n      }\n    }\n  }\n}\n","/**\r\n *\r\n * @param {number} ms\r\n * @description Wait for a given number of milliseconds.\r\n * @example\r\n * await waitFor(1000) // Waits for 1 second\r\n */\r\nexport function waitFor(ms: number) {\r\n  return new Promise(resolve => setTimeout(resolve, ms))\r\n}\r\n\r\n/**\r\n * 작업 디렉토리 경로를 **비교용**으로 정규화한다.\r\n *\r\n * 같은 디렉토리가 소스마다 다른 표기로 들어온다 — OSC7 `file://` 디코드는 `/C:/Users/...`,\r\n * 머신 `localCwd` 는 사용자 입력 그대로, Claude 훅의 `cwd` 는 `C:\\Users\\...`(백슬래시).\r\n * 직접 `===` 비교는 빗나가므로: 백슬래시→슬래시, 드라이브 앞 선행 슬래시 제거(`/C:/`→`C:/`),\r\n * 말미 슬래시 제거, Windows 는 대소문자 무시(경로가 case-insensitive)로 통일한다.\r\n *\r\n * Push-Notif Phase 1 의 cwd→machineId 역매핑(ssh-manager.findLocalMachineIdByCwd)에서 사용.\r\n */\r\nexport function normalizeCwd(cwd: string): string {\r\n  let p = cwd.trim().replace(/\\\\/g, '/')\r\n  p = p.replace(/^\\/([a-zA-Z]:)/, '$1') // /C:/... → C:/...  (OSC7 file:// 디코드 형태)\r\n  p = p.replace(/\\/+$/, '') // 말미 슬래시 제거\r\n  if (process.platform === 'win32') p = p.toLowerCase()\r\n  return p\r\n}\r\n","/**\r\n * NoneSessionManager — multiplexer 미사용. 현 직접 spawn 동작과 동일.\r\n *\r\n * Phase 0 (docs/plans/0001-terminal-multiplexer-integration.md) 의 안전망. 본 파일 머지\r\n * 후에도 production 동작이 정확히 같아야 함 — ssh-manager.connectLocal 의 기존 cmd.exe /\r\n * zsh 직접 spawn 을 그대로 호출하도록 SpawnSpec 을 만든다.\r\n *\r\n * 본 매니저는 multiplexer wrapping 없음:\r\n *   - attachOrCreate: hint 그대로 SpawnSpec 으로 echo. session 영속 X.\r\n *   - detach: no-op (어차피 영속화 안 됨)\r\n *   - kill: no-op (caller 가 ssh-manager.disconnect 로 PTY 종료)\r\n *   - resize: ssh-manager 가 자체적으로 setWindow 호출 — 본 함수도 no-op\r\n *   - list: 항상 빈 배열\r\n *   - probe: { healthy: true, version: null } — 'none' 은 항상 가용\r\n */\r\n\r\nimport type {\r\n  ProbeResult,\r\n  ResizeSource,\r\n  SessionDescriptor,\r\n  SessionManager,\r\n  SpawnHint,\r\n  SpawnSpec,\r\n} from './types'\r\n\r\nexport class NoneSessionManager implements SessionManager {\r\n  readonly kind = 'none' as const\r\n  readonly version = null\r\n\r\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars -- machineId 는 본 매니저에 의미 없음\r\n  async attachOrCreate(_machineId: string, hint: SpawnHint): Promise<SpawnSpec> {\r\n    // hint 그대로 SpawnSpec 으로 변환 — multiplexer wrapping 없음.\r\n    // ssh-manager 의 connectLocal / connectSSH 가 했던 일을 그대로 재현.\r\n    return {\r\n      shell: hint.shell,\r\n      args: hint.args ?? [],\r\n      cwd: hint.cwd,\r\n      env: hint.env,\r\n      cols: hint.cols,\r\n      rows: hint.rows,\r\n    }\r\n  }\r\n\r\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\r\n  async detach(_machineId: string): Promise<void> {\r\n    // none 모드 — 영속화 없으므로 detach 도 no-op.\r\n  }\r\n\r\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\r\n  async kill(_machineId: string): Promise<void> {\r\n    // none 모드 — caller (ssh-manager.disconnect) 가 PTY 종료 책임.\r\n  }\r\n\r\n  async list(): Promise<SessionDescriptor[]> {\r\n    return []\r\n  }\r\n\r\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\r\n  async resize(_machineId: string, _cols: number, _rows: number, _source: ResizeSource): Promise<void> {\r\n    // ssh-manager.resize 가 PTY layer 직접 setWindow — 본 매니저는 no-op.\r\n  }\r\n\r\n  async probe(): Promise<ProbeResult> {\r\n    return { healthy: true, version: null }\r\n  }\r\n}\r\n","/**\n * Multiplexer factory.\n *\n * 멀티플렉서(psmux/tmux) 동봉·설정은 2026-06-23 제거됨\n * (docs/requests/2026-06/20260623-112930-*, 후속 dead-code sweep 포함). 현재 유일한 구현은\n * {@link NoneSessionManager}(직접 spawn)이며 createSessionManager 는 항상 그것을 반환한다.\n * resolver/psmux/tmux 구현·테스트는 함께 삭제됨 — 되살리려면 git 이력에서 복원.\n */\n\nimport type { MultiplexerSetting } from '@arva/shared/types'\n\nimport { NoneSessionManager } from './none'\nimport type { SessionManager } from './types'\n\n// re-exports — 외부 모듈(ssh-manager)이 본 barrel 만 import 하면 충분.\nexport type {\n  MultiplexerKind,\n  ProbeResult,\n  ResizeSource,\n  SessionDescriptor,\n  SessionManager,\n  SpawnHint,\n  SpawnSpec,\n} from './types'\nexport { NoneSessionManager } from './none'\n\n/**\n * createSessionManager 옵션 — 모두 선택적이며 현재 구현(None)은 무시한다(호출부 시그니처 호환용).\n */\nexport interface CreateSessionManagerOptions {\n  /** production resources 경로 override. 현재 무시됨. */\n  resourcesRootOverride?: string\n  /** sessionName prefix 일부. 현재 무시됨. */\n  agentInstanceId?: string\n  /** multiplexer mouse 옵션. 현재 무시됨. */\n  mouse?: boolean\n}\n\n/**\n * SessionManager 를 만든다 — 항상 {@link NoneSessionManager}(직접 spawn).\n *\n * 멀티플렉서 동봉/설정 제거(2026-06-23)로 `setting`·`_opts` 와 무관하게 None 을 반환한다.\n * 과거 저장된 머신의 `multiplexer:'bundled'` 등도 throw 없이 안전하게 None 으로 동작한다.\n * 시그니처는 호출부(ssh-manager) 무변경을 위해 유지.\n */\nexport function createSessionManager(\n  setting: MultiplexerSetting | undefined,\n  _opts: CreateSessionManagerOptions = {},\n): SessionManager {\n  if (setting && setting !== 'none' && setting !== 'auto') {\n    console.info(`[multiplexer] setting='${setting}' 무시 — 동봉 제거(2026-06-23)로 항상 None(직접 spawn).`)\n  }\n  return new NoneSessionManager()\n}\n","import os from 'os'\r\nimport path from 'path'\r\nimport fs from 'fs'\r\nimport { createHmac, randomBytes } from 'crypto'\r\nimport { WebSocketServer, WebSocket } from 'ws'\r\nimport type { IncomingMessage } from 'http'\r\n\r\nimport type {\r\n  BroadcastFn,\r\n  BroadcastRecipient,\r\n  ClientMessage,\r\n  ServerMessage,\r\n  ConnectionDetails,\r\n  RelayStatus,\r\n  HostConflictReason,\r\n} from '@arva/shared/types'\r\nimport { type SSHManager, PTY_MIN_ROWS } from './ssh-manager'\r\nimport { toPairUrl } from './pair-url'\r\nimport { listAgentSessions, readSessionTranscript } from './agent-sessions'\r\nimport { AgentRunner } from './agent-runner'\r\nimport { FileUploadReceiver } from './file-upload-receiver'\r\nimport { FcmTokenStore } from './fcm-token-store'\r\nimport { REPLAY_ATTACH_TAIL_BYTES, REPLAY_CHUNK_BYTES } from '@arva/shared/constants'\r\nimport {\r\n  OPCODE,\r\n  PROTOCOL_ERROR_REASON,\r\n  FrameParseError,\r\n  parseFrame,\r\n  encodeAppJson,\r\n  encodeFrameSnapshot,\r\n  encodeHelloAck,\r\n  encodeProtocolError,\r\n} from '../protocol/binary-protocol'\r\nimport type { AgentConfigStore, AgentIdentity } from '../config/agent-config'\r\nimport { createLogger } from '../lib/logger'\r\n\r\n// Per-component scoped logger — writes to logs/ws.log (separate from main.log).\r\nconst log = createLogger('ws')\r\n// [handoff diag 20260607 — C] 모바일이 forward 한 인앱 디버그 로그(`mobile.diag_log`)를\r\n// 별도 파일 logs/mobile-diag.log 에 적재 — authority.log/ws.log 와 같은 폴더에서 모바일+\r\n// 데스크탑 로그를 시간순으로 함께 확인(끊김 진단 교차검증). ws.log 오염 방지로 파일 분리.\r\nconst mobileDiagLog = createLogger('mobile-diag')\r\n\r\n/**\r\n * [attach frame-snapshot 20260610] session_attach 시 GridEmulator 직렬화 ANSI 를\r\n * FRAME_SNAPSHOT (0x03) 으로 송신하는 기능의 kill-switch. 기본 ON.\r\n *\r\n * busy alt-screen TUI(claude 등)는 raw scrollback tail 16KB 로는 화면 복원이 불가해 모바일에\r\n * \"조각\"만 보이던 버그를 고친다. 회귀/문제 시 `VIEWER_ATTACH_FRAME_SNAPSHOT=false` 로 비활성.\r\n */\r\nconst attachFrameSnapshotEnabled = (): boolean =>\r\n  process.env.VIEWER_ATTACH_FRAME_SNAPSHOT !== 'false'\r\n\r\n/**\r\n * [resize-resettle frame-snapshot 20260610] RESIZE 수신 후 FRAME_SNAPSHOT 재방출까지의\r\n * 디바운스(ms). 모바일이 attach 직후 미측정 크기(80x24)로 붙었다가 ~233ms 뒤 실제 viewport\r\n * 로 RESIZE 하는 race 에서, 그 사이 직렬화된 스냅샷은 잘못된 행수 + claude repaint 전 reflow\r\n * 잔해라 \"조각\"으로 보인다. RESIZE 가 멎고 claude 가 SIGWINCH repaint 를 끝낸 뒤 한 번 더\r\n * 직렬화해 올바른 화면을 재방출한다. 연속 RESIZE(0x0→실제)는 마지막 것 기준으로 coalesce.\r\n */\r\nconst RESIZE_SNAPSHOT_DEBOUNCE_MS = 250\r\n\r\n// ── Relay flap(중복 host) 감지 임계값 ────────────────────────────────────────\r\n// 데스크탑·헤드리스가 같은 relay 토큰으로 host 2개를 등록하면 relay가 서로를 evict 해\r\n// \"connected → 수명 짧음 → close(1006) → 5s 뒤 재연결\"을 무한 반복한다(핑퐁). relay 는\r\n// close frame 없이(1006) 끊으므로 전용 코드로 구분할 수 없어, 아래 패턴으로 추론한다.\r\n/** 이 시간(ms)보다 짧게 살고 끊긴 relay 연결은 flap 후보로 집계. */\r\nconst RELAY_FLAP_LIFETIME_MS = 15_000\r\n/** flap 집계 롤링 윈도(ms). 이 시간 밖의 flap 기록은 폐기. */\r\nconst RELAY_FLAP_WINDOW_MS = 60_000\r\n/** 윈도 내 flap 이 이 횟수 이상이면 중복 host 로 판단해 경고 발화. */\r\nconst RELAY_FLAP_THRESHOLD = 3\r\n/** 연결이 이 시간(ms) 이상 유지되면 정상으로 보고 경고를 해제한다. */\r\nconst RELAY_STABLE_MS = 30_000\r\n\r\n/**\r\n * relay 연결 수명 + 최근 flap 기록으로 새 기록 배열과 경고 발화 여부를 계산하는 순수 함수.\r\n *\r\n * 클래스 상태(this)·타이머·WebSocket 과 분리해 임계값 로직만 단위 테스트로 회귀 잠금한다.\r\n *\r\n * @param prevTimes  이전까지 누적된 flap 타임스탬프(ms).\r\n * @param now        이번 close 시각(ms).\r\n * @param lifetimeMs 이번 relay 연결이 유지된 시간(ms).\r\n * @returns `times`=윈도 프루닝 후(필요 시 now 추가) 기록, `warn`=중복 host 경고를 켜야 하는지.\r\n */\r\nexport function evaluateRelayFlap(\r\n  prevTimes: number[],\r\n  now: number,\r\n  lifetimeMs: number\r\n): { times: number[]; warn: boolean } {\r\n  // 충분히 오래 산 연결은 flap 아님 — 기록을 그대로 두고 경고도 없음.\r\n  if (lifetimeMs >= RELAY_FLAP_LIFETIME_MS) return { times: prevTimes, warn: false }\r\n  const times = prevTimes.filter(t => now - t < RELAY_FLAP_WINDOW_MS)\r\n  times.push(now)\r\n  return { times, warn: times.length >= RELAY_FLAP_THRESHOLD }\r\n}\r\n\r\n/**\r\n * `mobile.diag_log` 의 untrusted 모바일 필드를 로그 파일에 쓰기 전 정화한다.\r\n *\r\n * WHY (review 20260607 #1·#2): relay 클라이언트가 보낸 값을 그대로 기록하면 임베드된 개행으로\r\n * 프리픽스 없는 **가짜 로그 라인을 위조**(인시던트 진단 오도)하거나 ANSI/제어문자로 `tail -f`\r\n * 를 깨뜨릴 수 있다. 같은 파일의 `terminal_input` 핸들러와 동일한 제어문자 escape 를 적용하고,\r\n * flood/멀티-MB 단일 메시지 방지로 길이도 cap 한다(cap 후 escape 라 CPU 도 bound).\r\n *\r\n * `value: unknown` — relay JSON 은 타입 미보장이라 string 이 아닐 수 있어 방어적으로 coerce.\r\n */\r\nconst sanitizeDiagField = (value: unknown, max: number): string =>\r\n  String(value ?? '')\r\n    .slice(0, max)\r\n    // biome-ignore lint/suspicious/noControlCharactersInRegex: 제어문자 정화가 목적 — 의도적 매칭(terminal_input 핸들러와 동일 패턴)\r\n    .replace(/[\\x00-\\x1f\\x7f]/g, c => `\\\\x${c.charCodeAt(0).toString(16).padStart(2, '0')}`)\r\n// ── Session token persistence ─────────────────────────────────────────────────\r\n// 앱 재시작 / 네트워크 순단 후에도 모바일이 동일 Relay URL로 재연결할 수 있도록\r\n// 토큰을 디스크에 영속화한다.\r\n\r\n/**\r\n * 에이전트 데이터 디렉터리 이름.\r\n *\r\n * package.json 의 `name` 이 아닌 고정 식별자로 pin 한다 — 모노리포 전환으로 패키지명이\r\n * `@arva/desktop` 등으로 바뀌어도(또는 헤드리스 앱이 별도 name 을 가져도) 세션 토큰 경로가\r\n * 흔들리지 않게 한다. 데스크탑·헤드리스가 같은 규약(`~/.ai_remote_vibe_agent`)을 공유한다.\r\n */\r\nconst APP_DATA_DIR_NAME = '.ai_remote_vibe_agent'\r\nconst DATA_DIR = path.join(os.homedir(), APP_DATA_DIR_NAME)\r\nconst SESSION_TOKEN_FILE = path.join(DATA_DIR, 'session-token.txt')\r\n\r\nfunction loadOrCreateSessionToken(): string {\r\n  try {\r\n    const existing = fs.readFileSync(SESSION_TOKEN_FILE, 'utf-8').trim()\r\n    if (existing.length === 36) return existing\r\n  } catch {\r\n    // 파일 없음 → 새로 생성\r\n  }\r\n  const token = crypto.randomUUID()\r\n  try {\r\n    if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true })\r\n    fs.writeFileSync(SESSION_TOKEN_FILE, token, { mode: 0o600 })\r\n  } catch (err) {\r\n    console.error('[ws-manager] session-token 저장 실패:', err)\r\n  }\r\n  return token\r\n}\r\n\r\nconst SESSION_TOKEN = loadOrCreateSessionToken()\r\n\r\n// 이 데스크탑 프로세스가 cap.ack 로 'first start' 를 이미 알렸는지 — agent_started 대리 발화를\r\n// 프로세스당 1회로 제한하는 모듈 전역 가드(WsManager 는 프로세스당 단일 인스턴스). 첫 cap.ack(첫\r\n// 폰 협상)에만 agent_first_start=true 를 실어, 모바일이 agent_started 를 1회 발화하게 한다.\r\nlet agentFirstStartSignaled = false\r\n\r\n/**\r\n * cap.ack 응답을 한 곳에서 조립한다 — mobile WS path 와 relay path 가 동일 페이로드를 보내도록 DRY.\r\n *\r\n * server_ver/max_cols/max_rows(서버-진실 capability) 외에 **익명 페어링 메타**\r\n * (agent_install_id/version/os/arch)를 동봉한다. 데스크탑엔 analytics 프레임워크가 없어 GA4\r\n * 직접 전송이 불가하므로, 모바일이 이 메타로 GA4 'agent_paired' 를 대리 발화해 공급측 활성화\r\n * (= distinct install_id, GitHub 다운로드 vanity 대체)를 계측한다. PII 0: install_id 는 랜덤\r\n * UUID(머신/유저 식별 아님)이며 relay 토큰 등 시크릿은 절대 동봉하지 않는다.\r\n *\r\n * 부수효과: 프로세스 **첫 호출** 시 `agent_first_start: true` 를 동봉하고 모듈 가드를 세운다 —\r\n * 모바일이 'agent_started'(데스크탑 launch 가 페어링까지 도달, 프로세스당 1회)를 'agent_paired'\r\n * (페어링 이벤트, 반복 가능)와 구분해 대리 발화하도록. 이후 호출은 플래그를 생략한다.\r\n */\r\nfunction buildCapAck(\r\n  selected: 'pty.data' | 'grid.diff',\r\n  installId: string,\r\n  version: string,\r\n): ServerMessage {\r\n  const firstStart = !agentFirstStartSignaled\r\n  if (firstStart) agentFirstStartSignaled = true\r\n  return {\r\n    type: 'cap.ack',\r\n    selected,\r\n    server_ver: 3,\r\n    max_cols: 500,\r\n    max_rows: 200,\r\n    agent_install_id: installId,\r\n    agent_version: version,\r\n    agent_os: process.platform,\r\n    agent_arch: process.arch,\r\n    // 프로세스 첫 cap.ack 에만 동봉 — 모바일이 agent_started 1회 대리 발화.\r\n    ...(firstStart ? { agent_first_start: true } : {}),\r\n  }\r\n}\r\n\r\nfunction getLocalIP(): string {\r\n  const interfaces = os.networkInterfaces()\r\n  for (const iface of Object.values(interfaces)) {\r\n    if (!iface) continue\r\n    for (const addr of iface) {\r\n      if (addr.family === 'IPv4' && !addr.internal) {\r\n        return addr.address\r\n      }\r\n    }\r\n  }\r\n  return '127.0.0.1'\r\n}\r\n\r\n/**\r\n * Phase 1 [Review Q2] — mobile WS 메타데이터. `(ws as any).clientId` 패턴 대신\r\n * WeakMap<WebSocket, MobileClientMeta> 로 저장. ws GC 시 자동 정리.\r\n *\r\n * - `clientId`: HMAC auth 성공 직후 부여 (`mob-<ts>-<rand>`). AuthorityResolver 가\r\n *   handover/detach 시 동일 모바일을 식별하는 키.\r\n * - `attachedMachineId`: 마지막 `session_attach` 의 machineId. ws.on('close') 에서\r\n *   resolver.onMobileDetach 호출 시 머신 식별.\r\n */\r\ninterface MobileClientMeta {\r\n  clientId: string\r\n  attachedMachineId?: string\r\n}\r\n\r\nexport class WsManager {\r\n  private ssh: SSHManager\r\n  private broadcast: BroadcastFn\r\n  /**\r\n   * [Cockpit] `agent.run` 별도 프로세스 실행기. stdout/stderr/exit 을 `agent.output`\r\n   * 으로 broadcast — interactive PTY (`terminal_data`) 와 완전 별개 채널.\r\n   */\r\n  private agentRunner: AgentRunner\r\n\r\n  /**\r\n   * [file-upload 20260614] 모바일→데스크탑 첨부 업로드 수신기. begin/chunk/end 3-메시지를\r\n   * in-memory 누적 후 머신 cwd 에 기록한다. cwd 는 ssh.resolveLocalCwd 로 해석(머신별 OSC7).\r\n   */\r\n  private fileUpload: FileUploadReceiver\r\n\r\n  /**\r\n   * [Cockpit Phase C3] takeFromTerminal 진입 시 killPtyAgent 후 ~500ms 지연 open 의 타이머\r\n   * (machineId 별). 지연 중 콕핏을 이탈(close)하면 이 타이머를 취소해 \"닫을 주체 없는\" orphan\r\n   * 세션이 떠버리는 것을 막는다.\r\n   */\r\n  private pendingHandoffOpens = new Map<string, ReturnType<typeof setTimeout>>()\r\n\r\n  private mobileClients = new Set<WebSocket>()\r\n  /**\r\n   * [A2 spike 20260523] machineId → 그 머신에 attach 된 mobile WS lookup.\r\n   *\r\n   * handleDesktopTakeback 에서 close code 4007 (TAKEN_BY_DESKTOP) 전송 시 사용.\r\n   * session_attach 시 set, ws.on('close') 시 delete. mobileClients (Set) 는 보존 — 다른\r\n   * callsite (broadcastToMobile fan-out) 가 의존.\r\n   */\r\n  private mobileClientByMachine = new Map<string, WebSocket>()\r\n  private clientMeta = new WeakMap<WebSocket, MobileClientMeta>()\r\n  /**\r\n   * A3 capability negotiation (binary protocol v3 §6.5).\r\n   *\r\n   * 각 mobile WS 의 선택된 path. cap.req 받기 전 default = 'pty.data' (legacy 호환).\r\n   * cap.req 받으면 cap.ack 응답 + 본 Map 갱신. 한 세션 내 path 전환 금지 (재연결 시 재협상).\r\n   *\r\n   * cap='grid.diff' client 는 PTY_DATA (0x00) 미수신, GRID_SNAPSHOT (0x04) + GRID_DIFF\r\n   * (0x05) 만 수신. cap='pty.data' 는 기존 path 그대로.\r\n   */\r\n  private mobileClientCaps = new Map<WebSocket, 'pty.data' | 'grid.diff'>()\r\n  /**\r\n   * snapshot.req frequency cap (3/min/session). plan §13-9.\r\n   * sliding window — 최근 60s 안 timestamp 배열. 3개 초과 시 throttle.\r\n   * WeakMap 사용 — ws close 시 자동 GC, 명시 cleanup 불요.\r\n   */\r\n  private snapshotReqTimes = new WeakMap<WebSocket, number[]>()\r\n  private wss: WebSocketServer | null = null\r\n\r\n  private relayWs: WebSocket | null = null\r\n  private relayQrUrl: string | null = null\r\n  private relayRetryCount = 0\r\n  private relayStatus: RelayStatus = 'disconnected'\r\n  private relayUrlIndex = 0\r\n  private destroyed = false\r\n\r\n  // ── Relay flap(중복 host) 감지 상태 ────────────────────────────────────────\r\n  /** 현재 relay 연결이 open 된 시각(ms). close 시 수명(=flap 여부) 계산에 사용. */\r\n  private relayConnectedAt = 0\r\n  /** 최근 flap(수명 짧은 종료) 타임스탬프들. RELAY_FLAP_WINDOW_MS 밖은 프루닝. */\r\n  private relayFlapTimes: number[] = []\r\n  /** 연결 안정화(RELAY_STABLE_MS 유지) 감지 타이머 — 유지되면 경고 해제. */\r\n  private relayStableTimer: NodeJS.Timeout | null = null\r\n  /**\r\n   * 현재 활성 host_conflict 사유(`null`=경고 없음). 활성-전이 판별(중복 broadcast 억제)과\r\n   * 렌더러 늦은 mount 시 resync(broadcastCurrentHostConflict)의 사유 소스를 겸한다.\r\n   */\r\n  private hostConflict: HostConflictReason | null = null\r\n\r\n  private wsPort: number = 0\r\n  private heartbeatIntervalMs: number = 0\r\n  private pongTimeoutMs: number = 0\r\n\r\n  // ── 타이머 (cleanup 시 반드시 해제) ──────────────────────────────────────────\r\n  private heartbeatTimer: NodeJS.Timeout | null = null\r\n  private pongTimer: NodeJS.Timeout | null = null\r\n\r\n  /**\r\n   * 마지막 session_attach 의 machineId. RESIZE opcode 가 machineId 없이 도착하므로\r\n   * (spec 상 wire 에 안 담김), 이 cache 로 routing. 1 mobile = 1 machine attach 가정.\r\n   * ws 별 cache (`ws.attachedMachineId`) 도 유지하지만 race 또는 reconnect 시 누락 발생 →\r\n   * global fallback 으로 보강. F2 (task 13 v3) — production dogfood 의 RESIZE 누락 회귀.\r\n   */\r\n  private lastAttachedMachineId: string | null = null\r\n\r\n  /**\r\n   * [resize-resettle frame-snapshot 20260610] 머신별 RESIZE→재스냅샷 디바운스 타이머.\r\n   * 새 RESIZE 가 오면 이전 타이머를 교체(coalesce)해 마지막 settle 시점에만 1회 방출.\r\n   * late-fire 는 `sendBinaryToMachine` 가 미attach 시 no-op 이라 무해(별도 cleanup 불필요).\r\n   */\r\n  private readonly resizeSnapshotTimers = new Map<string, NodeJS.Timeout>()\r\n\r\n  /** [FCM Phase 3] 이 데스크탑에 등록된 폰 FCM 토큰 — device.fcm_register/unregister 로 갱신. */\r\n  private readonly fcmTokenStore: FcmTokenStore\r\n\r\n  /**\r\n   * @param config   런타임 설정 + FCM 토큰맵 영속 — 데스크탑은 electron-store, 헤드리스는 JSON.\r\n   *                 코어가 호스트 저장소를 직접 import 하지 않게 하는 DI 경계(Phase A3).\r\n   * @param identity 호스트 앱 식별(현재 `version` — cap.ack `agent_version`).\r\n   */\r\n  constructor(\r\n    ssh: SSHManager,\r\n    broadcast: BroadcastFn,\r\n    private readonly config: AgentConfigStore,\r\n    private readonly identity: AgentIdentity,\r\n  ) {\r\n    this.ssh = ssh\r\n    this.fcmTokenStore = new FcmTokenStore(config)\r\n    // A3 (Sub-Part 1F-ii-b): GRID_DIFF binary broadcast 콜백 inject.\r\n    // ssh-manager 가 PTY data flush 시점 (16ms 디바운스 후) 마다 emulator dirty cell 을\r\n    // GRID_DIFF (0x05) 로 직렬화 → 본 콜백 호출. cap=grid.diff client filter 는 우리 책임.\r\n    ssh.setBinaryBroadcast((machineId, frame) =>\r\n      this.broadcastGridFrame(machineId, frame)\r\n    )\r\n    this.broadcast = broadcast\r\n    // [Cockpit] agent.run 출력 → agent.output broadcast. mobile 이 requestId 로 필터.\r\n    this.agentRunner = new AgentRunner(\r\n      (machineId, requestId, stream, payload) => {\r\n        // 디버깅 trace — stdout 은 byte 수만(시끄러움 방지), stderr/exit 은 내용 포함.\r\n        if (stream === 'stdout') {\r\n          log.info(\r\n            `[cockpit] agent.output→mobile machine=${machineId} req=${requestId} stdout bytes=${payload.data?.length ?? 0}`\r\n          )\r\n        } else {\r\n          log.info(\r\n            `[cockpit] agent.output→mobile machine=${machineId} req=${requestId} ${stream}` +\r\n              (stream === 'exit' ? ` code=${payload.exitCode}` : ` \"${payload.data?.trim() ?? ''}\"`)\r\n          )\r\n        }\r\n        const out = {\r\n          type: 'agent.output' as const,\r\n          machineId,\r\n          requestId,\r\n          stream,\r\n          data: payload.data,\r\n          exitCode: payload.exitCode,\r\n        }\r\n        // [Cockpit review-fix] exit 는 렌더러 콕핏도 받아 '응답 중'(activeRequestId) 을 해제해야\r\n        // 한다 — turn_complete 가 없는 비정상 종료(exit≠0/spawn-fail) 대비. → broadcast(renderer\r\n        // +mobile). stdout/stderr 는 기존대로 모바일 전용(렌더러는 cockpit.card 로 표시).\r\n        if (stream === 'exit') {\r\n          this.broadcast(out)\r\n        } else {\r\n          this.broadcastToMobile(out)\r\n        }\r\n      },\r\n      // [Cockpit source of truth] 파싱한 카드/세션은 broadcast(renderer + mobile 양쪽 fan-out)\r\n      // 로 — 데스크탑 콕핏이 원본 렌더, 모바일이 미러. raw agent.output(mobile 전용)과 병행.\r\n      {\r\n        onCard: (machineId, requestId, agent, card) => {\r\n          this.broadcast({\r\n            type: 'cockpit.card',\r\n            machineId,\r\n            requestId,\r\n            ts: Date.now(),\r\n            agent,\r\n            card,\r\n          })\r\n        },\r\n        onSessionId: (machineId, requestId, sessionId) => {\r\n          this.broadcast({\r\n            type: 'cockpit.session_started',\r\n            machineId,\r\n            requestId,\r\n            sessionId,\r\n          })\r\n        },\r\n      }\r\n    )\r\n\r\n    // [Cockpit 신선도 20260614] running 콕핏 세션 id provider 후주입 — broadcastAgentSessions\r\n    // 가 디스크 flush 전 running 장수명 세션을 picker 목록에 합류시킬 때 읽는다(자족적, index.ts 무변경).\r\n    this.ssh.setActiveSessionProvider(mid => this.agentRunner.getActiveSessionIds(mid))\r\n\r\n    // [file-upload 20260614] 첨부 업로드 수신기 — cwd 는 머신별 resolveLocalCwd 로 해석.\r\n    this.fileUpload = new FileUploadReceiver({\r\n      resolveCwd: (mid) => this.ssh.resolveLocalCwd(mid),\r\n    })\r\n\r\n    const wsPort = this.config.getWsPort()\r\n    const heartbeatMs = this.config.getHeartbeatIntervalMs()\r\n    const pongMs = this.config.getPongTimeoutMs()\r\n    const authMs = this.config.getAuthTimeoutMs()\r\n\r\n    log.info(\r\n      `[ws-manager] config: port=${wsPort} heartbeat=${heartbeatMs} pong=${pongMs} auth=${authMs}`\r\n    )\r\n\r\n    this.wsPort = wsPort\r\n    this.heartbeatIntervalMs = heartbeatMs\r\n    this.pongTimeoutMs = pongMs\r\n    this.startLocalServer(wsPort, authMs)\r\n    this.connectToRelay()\r\n  }\r\n\r\n  // ── Mobile broadcast (IPC handles renderer separately) ────────────────────\r\n\r\n  /**\r\n   * mobile WS fan-out — v3 binary protocol APP_JSON opcode wrap.\r\n   *\r\n   * Phase 1 — `perRecipient` 콜백이 주어지면 receiver 별로 메시지 일부를 customize 후\r\n   * encode. `terminal_authority_changed` 의 `isAuthority` 필드처럼 receiver 마다 다른\r\n   * 값이 필요한 경우 사용. relay 경유 모바일은 clientId 가 미상이라 (relayWs 자체는\r\n   * 단일 channel) `clientId: undefined` 로 콜백 호출 — resolver 가 보수적 false fallback.\r\n   */\r\n  broadcastToMobile(\r\n    msg: ServerMessage,\r\n    perRecipient?: (recipient: BroadcastRecipient) => Partial<ServerMessage>\r\n  ): void {\r\n    for (const ws of this.mobileClients) {\r\n      if (ws.readyState !== WebSocket.OPEN) continue\r\n      if (perRecipient) {\r\n        const meta = this.clientMeta.get(ws)\r\n        const customized = {\r\n          ...msg,\r\n          ...perRecipient({ kind: 'mobile', clientId: meta?.clientId }),\r\n        } as ServerMessage\r\n        ws.send(encodeAppJson(customized))\r\n      } else {\r\n        ws.send(encodeAppJson(msg))\r\n      }\r\n    }\r\n    if (this.relayWs?.readyState === WebSocket.OPEN) {\r\n      if (perRecipient) {\r\n        const meta = this.clientMeta.get(this.relayWs)\r\n        const customized = {\r\n          ...msg,\r\n          ...perRecipient({ kind: 'mobile', clientId: meta?.clientId }),\r\n        } as ServerMessage\r\n        this.relayWs.send(encodeAppJson(customized))\r\n      } else {\r\n        this.relayWs.send(encodeAppJson(msg))\r\n      }\r\n    }\r\n  }\r\n\r\n  // ── [FCM Phase 3] FCM 토큰맵 + relay 발송 ──────────────────────────────────\r\n\r\n  /** 등록된 폰 FCM 토큰 목록(push-dispatcher 발송용). */\r\n  getFcmTokens(): string[] {\r\n    return this.fcmTokenStore.tokens()\r\n  }\r\n\r\n  /** 해당 머신에 폰이 attach 중인지(presence 게이트 — true 면 push 생략). */\r\n  isMobilePresent(machineId: string): boolean {\r\n    return this.mobileClientByMachine.has(machineId)\r\n  }\r\n\r\n  /** relay 로 ServerMessage 송신(fcm.send 등). relay 미연결 시 no-op. */\r\n  sendToRelay(msg: ServerMessage): void {\r\n    if (this.relayWs?.readyState === WebSocket.OPEN) {\r\n      this.relayWs.send(encodeAppJson(msg))\r\n    }\r\n  }\r\n\r\n  // ── Renderer-only broadcast (relay 상태 등 내부 정보) ────────────────────────\r\n\r\n  private broadcastToRenderer(msg: ServerMessage): void {\r\n    if (this.destroyed) return\r\n    this.broadcast(msg)\r\n  }\r\n\r\n  // ── Relay status helpers ──────────────────────────────────────────────────\r\n\r\n  private setRelayStatus(status: RelayStatus): void {\r\n    this.relayStatus = status\r\n    log.info(\r\n      `[relay] status=${status} retryCount=${this.relayRetryCount} ts=${new Date().toISOString()}`\r\n    )\r\n    // relay_status는 렌더러에만 전달 (Flutter는 broadcastToMobile을 통해 별도 수신)\r\n    this.broadcastToRenderer({\r\n      type: 'relay_status',\r\n      status,\r\n      retryCount: this.relayRetryCount,\r\n    })\r\n    // Flutter 앱에도 relay_status 전달 (상태 표시용)\r\n    this.broadcastToMobile({\r\n      type: 'relay_status',\r\n      status,\r\n      retryCount: this.relayRetryCount,\r\n    })\r\n  }\r\n\r\n  // 렌더러 마운트 후 초기 상태 동기화용 — get_machines 응답 시 함께 호출\r\n  broadcastCurrentRelayStatus(): void {\r\n    this.broadcastToRenderer({\r\n      type: 'relay_status',\r\n      status: this.relayStatus,\r\n      retryCount: this.relayRetryCount,\r\n    })\r\n  }\r\n\r\n  // ── 중복 host 경고 (host_conflict) ─────────────────────────────────────────\r\n\r\n  /**\r\n   * 호스트 중복 실행 경고를 host(데스크탑 렌더러·헤드리스 TUI)에만 통지한다.\r\n   *\r\n   * 항상 `skipMobile:true` — 모바일에 도달하면 안 되는 로컬 진단 신호다(모바일 프로토콜 무영향).\r\n   * 활성-전이(null↔사유) 시에만 broadcast 해 중복을 억제한다 — 이미 활성이면 사유가 바뀌어도\r\n   * (예: port_in_use 활성 중 relay_flapping 도달) 최초 사유를 유지한다(flap 마다 발화 방지).\r\n   *\r\n   * @param reason 활성 사유(`port_in_use`/`relay_flapping`), 또는 `null`=해소.\r\n   */\r\n  private setHostConflict(reason: HostConflictReason | null): void {\r\n    const wasActive = this.hostConflict !== null\r\n    const nowActive = reason !== null\r\n    if (wasActive === nowActive) return\r\n    const payloadReason = reason ?? this.hostConflict ?? 'relay_flapping'\r\n    this.hostConflict = reason\r\n    log.warn(\r\n      `[relay] host_conflict active=${nowActive} reason=${reason ?? '(cleared)'} ts=${new Date().toISOString()}`\r\n    )\r\n    this.broadcast(\r\n      { type: 'host_conflict', active: nowActive, reason: payloadReason },\r\n      { skipMobile: true }\r\n    )\r\n  }\r\n\r\n  /**\r\n   * 현재 host_conflict 상태를 렌더러에 재통지(늦게 mount 한 클라이언트 resync용).\r\n   *\r\n   * WHY: 데스크탑은 IPC(`webContents.send`)가 구독 전 메시지를 버퍼링하지 않아, 부팅 중 발화하는\r\n   * `port_in_use` edge 를 렌더러가 놓칠 수 있다. `broadcastCurrentRelayStatus` 와 동일하게\r\n   * get_machines(렌더러 mount) 시 함께 호출해 level 상태를 재동기화한다. 헤드리스는 store 가\r\n   * 상태를 보유해 자연 resync 되므로 불필요(데스크탑 전용 갭 보정). 활성 아니면 no-op.\r\n   */\r\n  broadcastCurrentHostConflict(): void {\r\n    if (this.hostConflict === null) return\r\n    this.broadcast(\r\n      { type: 'host_conflict', active: true, reason: this.hostConflict },\r\n      { skipMobile: true }\r\n    )\r\n  }\r\n\r\n  /**\r\n   * relay 연결 수명이 짧으면(flap) 집계하고, 롤링 윈도 내 임계 초과 시 중복 host 경고를 켠다.\r\n   *\r\n   * 왜 패턴 추론인가: relay 는 중복 host 를 code 없는 1006 으로 끊어 close 사유로 구분 불가.\r\n   * → open→close 수명이 {@link RELAY_FLAP_LIFETIME_MS} 미만인 종료를 {@link RELAY_FLAP_WINDOW_MS}\r\n   * 윈도로 세어 {@link RELAY_FLAP_THRESHOLD} 회 이상이면 \"같은 토큰 host 충돌\"로 판단한다.\r\n   */\r\n  private recordRelayFlap(): void {\r\n    if (this.relayConnectedAt === 0) return // open 을 못 본 실패 — 수명 계산 불가\r\n    const now = Date.now()\r\n    const lifetime = now - this.relayConnectedAt\r\n    this.relayConnectedAt = 0\r\n    const { times, warn } = evaluateRelayFlap(this.relayFlapTimes, now, lifetime)\r\n    this.relayFlapTimes = times\r\n    if (warn) this.setHostConflict('relay_flapping')\r\n  }\r\n\r\n  // ── QR / relay info ───────────────────────────────────────────────────────\r\n\r\n  requestQr(): void {\r\n    const info = this.getMobileQrInfo()\r\n    this.broadcast({ type: 'qr_code', ...info })\r\n  }\r\n\r\n  /**\r\n   * [A2 spike 20260523] desktop renderer 의 Take Back 처리.\r\n   *\r\n   * 호출 경로: main/index.ts 의 `ipcMain.handle('app:send', ...)` → handleMessage 의\r\n   * `case 'desktop.takeback'`. ClientMessage 의 일부로 도착.\r\n   *\r\n   * 동작:\r\n   *   1. mobileClientByMachine 에서 해당 머신의 mobile WS lookup → close code 4007\r\n   *      (TAKEN_BY_DESKTOP) 으로 종료. mobile 측 KickedByDesktopBanner 트리거 (C6).\r\n   *   2. authority.forceDesktopLeader(machineId) → leader='desktop' + 5s cooldown 시작 +\r\n   *      viewer_mode='interactive' broadcast → desktop UI 의 placeholder unmount.\r\n   *   3. mobile WS 가 5s 안에 재attach 시도하면 viewer_mode='rejected' + close code 4008\r\n   *      (TAKEBACK_COOLDOWN) — Eng Fix-M 의 cooldown bypass race close.\r\n   *\r\n   * leader 가 mobile 이 아닌 경우에도 safe — forceDesktopLeader 가 idempotent (이미 desktop\r\n   * 이면 cooldown 만 갱신). mobile WS 가 없는 경우 (이미 closed) 도 try/catch 로 무해 처리.\r\n   */\r\n  handleDesktopTakeback(machineId: string): void {\r\n    log.info(`[ws-manager] desktop.takeback machine=${machineId}`)\r\n    // [Bug #3 fix 20260526] App-level kick frame — relay 경로 mobile 도 도달.\r\n    // 기존 ws.close(4007) 는 relay 경로에서 agent-relay 연결만 끊고 mobile 에는\r\n    // 도달 안 함. 본 frame 은 broadcast 채널로 forward 되어 mobile 이 자체 close +\r\n    // KickedByDesktopBanner 발화 가능. 신버전 mobile 만 본 frame 처리, 구버전은\r\n    // 아래 ws.close(4007) fallback 으로 처리.\r\n    this.broadcast({\r\n      type: 'desktop.kick',\r\n      machineId,\r\n      reason: 'takeback',\r\n    })\r\n    const ws = this.mobileClientByMachine.get(machineId)\r\n    // Local WS 경로 호환 — relayWs 닫지 말 것 (agent 자체 연결 끊김 + auto-reconnect 부작용).\r\n    if (ws && ws !== this.relayWs) {\r\n      try {\r\n        ws.close(4007, 'TAKEN_BY_DESKTOP')\r\n      } catch (err) {\r\n        log.warn(\r\n          `[ws-manager] handleDesktopTakeback close failed machine=${machineId}`,\r\n          err\r\n        )\r\n      }\r\n    }\r\n    this.ssh.authority.forceDesktopLeader(machineId)\r\n  }\r\n\r\n  /**\r\n   * [Cockpit] `agent.*` 메시지 공유 dispatch.\r\n   *\r\n   * 렌더러(IPC, `index.ts` handleMessage)와 모바일(WS handleMessage)이 같은 `ClientMessage`\r\n   * 를 보내지만 진입점이 분리돼 있어, agent.* 처리를 한 곳으로 모은다 — 한쪽 switch 에만 case\r\n   * 를 두어 누락되는 사고를 막는다(1b 콕핏 무반응 회귀의 근본 수정). agent.* 들은 `source`\r\n   * (모바일 클라이언트)에 무관하므로 양쪽이 안전하게 공유한다. 결과 `cockpit.card`/출력은\r\n   * broadcast(renderer+mobile)로 양쪽에 동일하게 흐른다.\r\n   *\r\n   * [Cockpit v2 장수명] `agent.session.open/send/close` 는 AgentRunner 의 장수명 메서드로\r\n   * 위임 — one-shot `agent.run` 과 병존(back-compat). 모바일은 미전환 시 본 타입을 안 보낸다.\r\n   */\r\n  handleAgentMessage(\r\n    msg: Extract<\r\n      ClientMessage,\r\n      {\r\n        type:\r\n          | 'agent.run'\r\n          | 'agent.kill'\r\n          | 'agent.sessions.list'\r\n          | 'agent.session.transcript.get'\r\n          | 'agent.session.open'\r\n          | 'agent.session.send'\r\n          | 'agent.session.close'\r\n          | 'agent.session.handoffToTerminal'\r\n      }\r\n    >\r\n  ): void {\r\n    switch (msg.type) {\r\n      case 'agent.run':\r\n        log.info(\r\n          `[ws-manager] agent.run machine=${msg.machineId} requestId=${msg.requestId} argv=${JSON.stringify(msg.argv)}`\r\n        )\r\n        this.agentRunner.run(msg.machineId, msg.requestId, msg.argv, msg.cwd)\r\n        break\r\n      case 'agent.kill':\r\n        log.info(\r\n          `[ws-manager] agent.kill machine=${msg.machineId} requestId=${msg.requestId ?? '(pty)'}`\r\n        )\r\n        if (msg.requestId) {\r\n          // 별도 프로세스 종료.\r\n          this.agentRunner.kill(msg.machineId, msg.requestId)\r\n        } else {\r\n          // requestId 없음 = interactive PTY 의 foreground agent 종료 (\"닫고 새로 열기\").\r\n          this.ssh.killPtyAgent(msg.machineId)\r\n        }\r\n        break\r\n      case 'agent.sessions.list':\r\n        // [Cockpit] cwd 의 claude 세션 목록 스캔 → agent.sessions broadcast.\r\n        log.info(`[ws-manager] agent.sessions.list machine=${msg.machineId}`)\r\n        this.ssh.broadcastAgentSessions(msg.machineId)\r\n        break\r\n      case 'agent.session.transcript.get':\r\n        // [Cockpit] 세션 이전 대화 읽기 → agent.session.transcript broadcast.\r\n        log.info(\r\n          `[ws-manager] agent.session.transcript.get machine=${msg.machineId} session=${msg.sessionId}`\r\n        )\r\n        this.ssh.broadcastSessionTranscript(msg.machineId, msg.sessionId)\r\n        break\r\n      case 'agent.session.open': {\r\n        // [Cockpit v2 장수명] 양방향 stream-json 세션 열기. stdout 카드는 cockpit.card 로 흐른다.\r\n        // 콕핏 claude 는 머신의 실제 cwd 에서 띄워야 의미가 있다(터미널과 같은 ~/.claude 세션 풀,\r\n        // Phase C). 렌더러는 live cwd(OSC7)를 모르므로 main 이 resolveLocalCwd 로 해석(미지정 시).\r\n        const cwd = msg.cwd ?? this.ssh.resolveLocalCwd(msg.machineId)\r\n        // 세션 id 결정:\r\n        // - [Phase D] resume + sessionId 가 목록에 있으면 그 세션 resume(picker/영속 복원).\r\n        // - [Phase C] preferLatest(또는 위 id 부재) 면 최신 세션(mtime[0]) resume(터미널 인계).\r\n        // - 둘 다 안 되면 제공된 sessionId 로 새 세션(fresh).\r\n        let openId = msg.sessionId\r\n        let resume = false\r\n        const sessions = listAgentSessions(cwd)\r\n        if (msg.preferLatest || msg.resume) {\r\n          // resume 요청 id 가 목록에 있으면 그것, 아니면 최신, 둘 다 없으면 fresh(아래 openId 유지).\r\n          const picked =\r\n            msg.resume && sessions.some(s => s.id === msg.sessionId)\r\n              ? msg.sessionId\r\n              : sessions[0]?.id\r\n          if (picked) {\r\n            openId = picked\r\n            resume = true\r\n          }\r\n        }\r\n        // [bug 5] fresh(\"새 세션\": preferLatest/resume 둘 다 false)일 때도 **항상** seed 를 보낸다.\r\n        // 이전엔 preferLatest||resume 일 때만 broadcast 해, picker '새 세션'(fresh)이 renderer 의\r\n        // sessionId·카드를 갱신하지 못해 \"무반응\"이었다(COCKPIT_SESSION_OPEN 은 transcript.seed 로만\r\n        // dispatch 됨). fresh 면 빈 messages → 빈 콕핏 + 새 sessionId 확정. 콕핏 전용 메시지 —\r\n        // 모바일 picker 용 agent.session.transcript 와 분리(데스크탑 콕핏 카드 교차오염 방지).\r\n        const messages = resume ? readSessionTranscript(cwd, openId) : []\r\n        this.broadcast({\r\n          type: 'cockpit.transcript.seed',\r\n          machineId: msg.machineId,\r\n          sessionId: openId,\r\n          messages,\r\n        })\r\n        // picker 목록도 같은 스캔으로 함께 broadcast — 렌더러가 별도 agent.sessions.list 를\r\n        // 보내 디렉토리를 두 번 스캔하던 낭비 제거.\r\n        this.broadcast({\r\n          type: 'agent.sessions',\r\n          machineId: msg.machineId,\r\n          cwd,\r\n          sessions,\r\n        })\r\n        log.info(\r\n          `[ws-manager] agent.session.open machine=${msg.machineId} session=${openId} cwd=${cwd} resume=${resume} preferLatest=${msg.preferLatest ?? false} reqResume=${msg.resume ?? false} takeFromTerminal=${msg.takeFromTerminal ?? false}`\r\n        )\r\n        if (msg.takeFromTerminal) {\r\n          // [Phase C3 single-owner] 터미널 claude 를 먼저 Ctrl-C 종료(killPtyAgent: Ctrl-C ×2,\r\n          // 200ms 간격)한 뒤 resume — 같은 세션 동시 write 경합 회피. 카드 시드(위)는 이미 broadcast\r\n          // 됐으니 사용자는 대화를 바로 본다. claude 종료 + jsonl flush 를 기다려 ~500ms 후 open.\r\n          // 지연 중 콕핏 이탈(close)이 오면 타이머를 취소(orphan 세션 방지).\r\n          const mid = msg.machineId\r\n          const prev = this.pendingHandoffOpens.get(mid)\r\n          if (prev) clearTimeout(prev)\r\n          this.pendingHandoffOpens.set(\r\n            mid,\r\n            setTimeout(() => {\r\n              this.pendingHandoffOpens.delete(mid)\r\n              this.agentRunner.openSession(mid, openId, cwd, resume)\r\n            }, 500)\r\n          )\r\n          this.ssh.killPtyAgent(mid)\r\n        } else {\r\n          this.agentRunner.openSession(msg.machineId, openId, cwd, resume)\r\n        }\r\n        break\r\n      }\r\n      case 'agent.session.send':\r\n        // [Cockpit v2 장수명] 열린 세션 stdin 으로 user 메시지 push(프롬프트/approval 응답).\r\n        this.agentRunner.sendToSession(msg.sessionId, msg.text)\r\n        break\r\n      case 'agent.session.close': {\r\n        // [Cockpit v2 장수명] 세션 종료(뷰 전환/탭 닫기/disconnect). sessionId 미지정 시 머신 단위\r\n        // (Phase C: resume 으로 연 실제 id 를 렌더러가 몰라도 닫게).\r\n        log.info(\r\n          `[ws-manager] agent.session.close machine=${msg.machineId} session=${msg.sessionId ?? '(by machine)'}`\r\n        )\r\n        // [Phase C3] 지연 중인 takeFromTerminal open 이 있으면 취소 — 닫은 직후 orphan 세션이\r\n        // 떠버리는 것을 막는다.\r\n        const pending = this.pendingHandoffOpens.get(msg.machineId)\r\n        if (pending) {\r\n          clearTimeout(pending)\r\n          this.pendingHandoffOpens.delete(msg.machineId)\r\n        }\r\n        this.agentRunner.closeSession(msg.sessionId, msg.machineId)\r\n        break\r\n      }\r\n      case 'agent.session.handoffToTerminal': {\r\n        // [Cockpit Phase C/C2] 콕핏 → 터미널 핸드오프. 콕핏 세션을 닫고(single-owner) PTY 에\r\n        // `claude --resume <id>` 주입 → 인터랙티브 터미널이 콕핏 대화를 이어받는다.\r\n        // handleAgentMessage 가 void 라 case 안에서만 async IIFE 로 detect 를 await 한다.\r\n        const mid = msg.machineId\r\n        const sid = msg.sessionId\r\n        void (async () => {\r\n          // [bug 1] main 권위 가드: 주입 직전 실시간 탐지로 최신화. renderer 의 runningAgent 는\r\n          // 5s 폴링이라 stale 일 수 있어 — 그 가드만 믿으면 살아있는 claude 프롬프트에 `claude\r\n          // --resume` 이 텍스트로 박힌다. main 이 최종 차단한다.\r\n          await this.ssh.detectAndBroadcastRunningAgent(mid)\r\n          this.agentRunner.closeSession(undefined, mid) // 콕핏에서 나가므로 콕핏 세션은 닫는다\r\n          if (this.ssh.getRunningAgent(mid) === 'claude') {\r\n            log.warn(\r\n              `[ws-manager] handoff skipped — claude already running in terminal machine=${mid}`\r\n            )\r\n            return\r\n          }\r\n          log.info(\r\n            `[ws-manager] agent.session.handoffToTerminal machine=${mid} session=${sid}`\r\n          )\r\n          // sessionId 는 PTY(셸)로 그대로 주입되므로 charset 검증 필수 — id 는 claude 세션 UUID\r\n          // (`[A-Za-z0-9_-]`)인데, listAgentSessions 가 임의 jsonl 파일명에서 유래할 수 있어 셸\r\n          // 메타문자(`; | $ \\n` 등)가 섞이면 셸 인젝션이 된다. 안전 패턴이 아니면 주입 안 함.\r\n          if (!/^[A-Za-z0-9_-]+$/.test(sid)) {\r\n            log.warn(\r\n              `[ws-manager] handoff rejected — unsafe sessionId charset machine=${mid}`\r\n            )\r\n            return\r\n          }\r\n          // [bug 2] 주입 전 현재 셸 입력 라인 클리어(Ctrl-C) — 셸에 입력 중이던 텍스트(예: dir)\r\n          // 에 명령이 이어붙어 'dirclaude --resume' 이 되는 것 방지. cross-platform: Ctrl-C 는\r\n          // bash/zsh/PowerShell/cmd 모두 현재 라인을 버리고 새 프롬프트를 그린다. 직후엔\r\n          // 프롬프트 재그리기 전이라 ~120ms 뒤 명령을 주입한다.\r\n          this.ssh.sendInput(mid, '\\x03')\r\n          setTimeout(() => {\r\n            this.ssh.sendInput(mid, `claude --resume ${sid}\\n`)\r\n          }, 120)\r\n        })()\r\n        break\r\n      }\r\n    }\r\n  }\r\n\r\n  private getMobileQrInfo(): {\r\n    url: string\r\n    mode: 'relay' | 'lan'\r\n    details: ConnectionDetails\r\n  } {\r\n    if (this.relayQrUrl) {\r\n      return {\r\n        // 폰 기본 카메라로 찍어도 동작하는 HTTPS 딥링크로 노출 — 미설치 시 스토어,\r\n        // 설치 시 앱 실행. 내부 relayQrUrl 은 원시 ws url(접속 진실원)로 유지.\r\n        url: toPairUrl(this.relayQrUrl),\r\n        mode: 'relay',\r\n        details: {\r\n          relayToken: SESSION_TOKEN.slice(0, 8),\r\n          localPort: this.wsPort,\r\n        },\r\n      }\r\n    }\r\n    // relay 연결 중이거나 실패한 경우 LAN fallback 반환 (절대 null 반환하지 않음)\r\n    const lanIp = getLocalIP()\r\n    return {\r\n      url: `ws://${lanIp}:${this.wsPort}/mobile`,\r\n      mode: 'lan',\r\n      details: { lanIp, localPort: this.wsPort },\r\n    }\r\n  }\r\n\r\n  // ── Heartbeat (ping/pong) ─────────────────────────────────────────────────\r\n\r\n  private startHeartbeat(): void {\r\n    this.stopHeartbeat()\r\n    this.heartbeatTimer = setInterval(() => {\r\n      if (this.relayWs?.readyState !== WebSocket.OPEN) return\r\n      this.relayWs.ping()\r\n      this.pongTimer = setTimeout(() => {\r\n        log.info(\r\n          `[relay] heartbeat_miss ts=${new Date().toISOString()} — closing zombie connection`\r\n        )\r\n        this.relayWs?.terminate()\r\n        this.relayWs = null\r\n      }, this.pongTimeoutMs)\r\n    }, this.heartbeatIntervalMs)\r\n  }\r\n\r\n  private stopHeartbeat(): void {\r\n    if (this.heartbeatTimer) {\r\n      clearInterval(this.heartbeatTimer)\r\n      this.heartbeatTimer = null\r\n    }\r\n    if (this.pongTimer) {\r\n      clearTimeout(this.pongTimer)\r\n      this.pongTimer = null\r\n    }\r\n  }\r\n\r\n  // ── Message handler (forwarded from mobile/relay) ─────────────────────────\r\n  //\r\n  // source: 메시지가 들어온 ws (LAN 모바일). 없으면 (relay 경유) broadcast.\r\n  // session_attach 의 replay 응답을 해당 클라이언트에게만 보내려면 source 가 필요.\r\n\r\n  /**\r\n   * 메시지 타입 → 핸들러 테이블. switch 를 대체한다 — 메시지가 늘어도 한 줄만 추가되고,\r\n   * 매핑 타입이 per-type narrowing 을 유지해 핸들러 안에서 캐스팅 없이 페이로드를 쓴다.\r\n   * 큰 본문은 private 메서드로 위임하고, 트리비얼 케이스만 인라인으로 둔다.\r\n   */\r\n  private readonly messageHandlers: {\r\n    [K in ClientMessage['type']]?: (\r\n      msg: Extract<ClientMessage, { type: K }>,\r\n      source: WebSocket | null\r\n    ) => void\r\n  } = {\r\n    // relay_status 는 auth 시점에 전달됨(startLocalServer auth_ok) — 여기선 머신만.\r\n    get_machines: () => this.ssh.broadcastMachines(),\r\n    add_machine: msg => {\r\n      this.ssh.addMachine(msg.machine)\r\n      this.ssh.broadcastMachines()\r\n    },\r\n    edit_machine: msg => {\r\n      this.ssh.editMachine(msg.machine)\r\n      this.ssh.broadcastMachines()\r\n    },\r\n    remove_machine: msg => {\r\n      this.ssh.removeMachine(msg.machineId)\r\n      this.ssh.broadcastMachines()\r\n    },\r\n    // 구 클라이언트 backward-compat. connect = \"PTY 보장 + 가능하면 active 화\"\r\n    // (ssh-manager 가 ensure 패턴). F2 — lastAttachedMachineId 갱신으로 connect-only\r\n    // path 에서도 RESIZE opcode 의 machineId fallback 이 동작.\r\n    connect: msg => {\r\n      this.lastAttachedMachineId = msg.machineId\r\n      this.ssh.connect(msg.machineId)\r\n    },\r\n    // 명시적 disconnect — PTY 와 scrollback 모두 폐기(사용자 의도 종료).\r\n    disconnect: msg => this.ssh.disconnect(msg.machineId),\r\n    terminal_input: (msg, source) => this.onTerminalInput(msg, source),\r\n    // [FCM Phase 3] 폰 FCM 토큰 등록/해제 — userData 토큰맵(앱 killed 푸시 대상).\r\n    'device.fcm_register': msg =>\r\n      this.fcmTokenStore.register(msg.token, msg.platform, msg.locale),\r\n    'device.fcm_unregister': msg => this.fcmTokenStore.unregister(msg.token),\r\n    // Phase 1 — 모바일 우선 권위. 여기로 오는 terminal_resize 는 모두 외부(mobile) 발신\r\n    // (데스크톱 renderer 는 IPC 경로). source='mobile' 로 60s mobileAuthorityUntil 갱신 →\r\n    // 그 동안 데스크톱 resize 차단(ResizeObserver ping-pong 봉쇄).\r\n    terminal_resize: msg => this.ssh.resize(msg.machineId, msg.cols, msg.rows, 'mobile'),\r\n    request_qr: () => this.requestQr(),\r\n    request_ssh_keys: () => this.ssh.broadcastSshKeys(),\r\n    init_commands: msg => this.ssh.runInitCommands(msg.machineId),\r\n    active_machine: msg =>\r\n      this.broadcast({ type: 'active_machine', machineId: msg.machineId }),\r\n    reconnect_relay: () => this.reconnect(),\r\n    git_action: msg => this.ssh.executeGitAction(msg.machineId, msg.action),\r\n    // ─── PTY persistence (PROTOCOL_VERSION 2+) ───\r\n    // F2 — global cache 로 RESIZE opcode 의 machineId fallback 보강.\r\n    session_attach: (msg, source) => {\r\n      this.lastAttachedMachineId = msg.machineId\r\n      this.handleSessionAttach(msg.machineId, source, msg.cols, msg.rows)\r\n      // [Git 첫 진입 지연 fix 20260705] 신규 attach 클라이언트가 다음 폴링(≤60초)까지\r\n      // 기다리지 않도록 git status 를 1회 즉시 조회. git_result 는 mobile 전체 fan-out 이라\r\n      // 방금 attach 한 클라이언트도 즉시 수신한다. 프로토콜 불변 — 발화 시점만 앞당김.\r\n      // 폴링/OSC7 과 같은 machine 별 limiter 로 직렬화되도록 refreshGitStatus 경유.\r\n      this.ssh.refreshGitStatus(msg.machineId)\r\n    },\r\n    session_detach: (msg, source) => this.onSessionDetach(msg, source),\r\n    session_keepalive: (msg, source) => this.onSessionKeepalive(msg, source),\r\n    'mobile.diag_log': (msg, source) => this.onMobileDiagLog(msg, source),\r\n    'file.upload.begin': (msg, source) => this.onFileUploadBegin(msg, source),\r\n    // 청크 누적 — 알 수 없는 id 는 receiver 가 무시.\r\n    'file.upload.chunk': msg => this.fileUpload.chunk(msg.uploadId, msg.index, msg.data),\r\n    'file.upload.end': (msg, source) => this.onFileUploadEnd(msg, source),\r\n    // E4 — 사용자 명시 PTY 재시작(ssh-manager 200ms debounce 로 더블클릭 방어).\r\n    pty_restart: msg => this.ssh.restartPty(msg.machineId),\r\n    terminal_authority_takeover_request: (msg, source) =>\r\n      this.onTakeoverRequest(msg, source),\r\n    // [Cockpit] agent.* 는 렌더러(IPC)와 공유하는 handleAgentMessage 로 위임.\r\n    'agent.run': msg => this.handleAgentMessage(msg),\r\n    'agent.kill': msg => this.handleAgentMessage(msg),\r\n    'agent.sessions.list': msg => this.handleAgentMessage(msg),\r\n    'agent.session.transcript.get': msg => this.handleAgentMessage(msg),\r\n    'agent.session.open': msg => this.handleAgentMessage(msg),\r\n    'agent.session.send': msg => this.handleAgentMessage(msg),\r\n    'agent.session.close': msg => this.handleAgentMessage(msg),\r\n    'agent.session.handoffToTerminal': msg => this.handleAgentMessage(msg),\r\n  }\r\n\r\n  /**\r\n   * 외부(LAN/relay mobile) 메시지 1건을 타입별 핸들러로 라우팅한다.\r\n   * 미지의 타입은 무시 — 기존 switch 에 default 가 없던 동작을 그대로 보존한다.\r\n   */\r\n  private handleMessage(msg: ClientMessage, source: WebSocket | null = null): void {\r\n    // `msg as never`: 호출부는 union 전체를 넘기지만 각 핸들러 시그니처는\r\n    // Extract<…, {type:K}> 로 narrowed 라 내부는 타입 안전(좁힘은 맵 키가 보장).\r\n    this.messageHandlers[msg.type]?.(msg as never, source)\r\n  }\r\n\r\n  /**\r\n   * terminal_input — PTY 로 입력 전달 + mobile 활동 시각 갱신.\r\n   * F2 진단: Ctrl+C(0x03) 도착 시 STATUS_CONTROL_C_EXIT(-1073741510) 회귀 추적용 warn 로그.\r\n   */\r\n  private onTerminalInput(\r\n    msg: Extract<ClientMessage, { type: 'terminal_input' }>,\r\n    source: WebSocket | null\r\n  ): void {\r\n    const data = msg.data ?? ''\r\n    const codes = [...data].map(c => c.charCodeAt(0))\r\n    const hasCtrlC = codes.includes(0x03)\r\n    const ascii = data.replace(\r\n      /[\\x00-\\x1f\\x7f]/g,\r\n      c => `\\\\x${c.charCodeAt(0).toString(16).padStart(2, '0')}`\r\n    )\r\n    if (hasCtrlC) {\r\n      log.warn(\r\n        `[ws-manager] ⚠ terminal_input Ctrl+C machine=${msg.machineId} bytes=${codes.length} ascii=\"${ascii}\"`\r\n      )\r\n    } else if (data.length > 0) {\r\n      log.info(\r\n        `[ws-manager] terminal_input machine=${msg.machineId} bytes=${codes.length} ascii=\"${ascii.slice(0, 40)}\"`\r\n      )\r\n    }\r\n    this.ssh.sendInput(msg.machineId, msg.data)\r\n    // [A2 spike] mobile input 마다 lastActivityAt 갱신(HandoffPlaceholder heartbeat 정확도).\r\n    if (source) {\r\n      const meta = this.clientMeta.get(source)\r\n      if (meta?.clientId) {\r\n        this.ssh.authority.touchMobileActivity(msg.machineId, meta.clientId)\r\n      }\r\n    }\r\n  }\r\n\r\n  /**\r\n   * session_detach — PTY/scrollback 은 보존(영속화). lifecycle idle/running 재계산 +\r\n   * [M1] 명시 detach → 권위 즉시 회수(relay 경로의 유일한 즉시 detach 신호). 내부 가드:\r\n   * leader=mobile + clientId 매칭 시에만 grace 시작(비권위/오매칭 detach 는 noop).\r\n   */\r\n  private onSessionDetach(\r\n    msg: Extract<ClientMessage, { type: 'session_detach' }>,\r\n    source: WebSocket | null\r\n  ): void {\r\n    log.info(`[ws-manager] session_detach machine=${msg.machineId}`)\r\n    this.ssh.lifecycle.onDetach(msg.machineId)\r\n    if (source) {\r\n      const meta = this.clientMeta.get(source)\r\n      if (meta?.clientId) {\r\n        this.ssh.authority.onMobileDetach(msg.machineId, meta.clientId)\r\n      }\r\n    }\r\n  }\r\n\r\n  /**\r\n   * session_keepalive — passive-watch 중 handed_off watchdog(60s) 무활동 회수 방지.\r\n   * lastActivityAt 만 갱신(sendInput 미경유 → PTY/pty_status 무영향). 내부가 leader=mobile +\r\n   * clientId 매칭 가드(비권위/오매칭 keepalive 는 noop).\r\n   */\r\n  private onSessionKeepalive(\r\n    msg: Extract<ClientMessage, { type: 'session_keepalive' }>,\r\n    source: WebSocket | null\r\n  ): void {\r\n    if (source) {\r\n      const meta = this.clientMeta.get(source)\r\n      if (meta?.clientId) {\r\n        this.ssh.authority.touchMobileActivity(msg.machineId, meta.clientId)\r\n      }\r\n    }\r\n  }\r\n\r\n  /**\r\n   * mobile.diag_log — 모바일이 forward 한 인앱 로그 1건을 logs/mobile-diag.log 에 적재.\r\n   * untrusted 입력이라 제어문자 escape + 길이 cap(로그 위조/flood 방지). level 외 값은\r\n   * info 로 삼키지 않고 warn 으로 fail-loud(에러 누락 방지).\r\n   */\r\n  private onMobileDiagLog(\r\n    msg: Extract<ClientMessage, { type: 'mobile.diag_log' }>,\r\n    source: WebSocket | null\r\n  ): void {\r\n    const meta = source ? this.clientMeta.get(source) : undefined\r\n    const client = meta?.clientId ?? 'unknown' // 데스크탑 생성 id — untrusted 아님\r\n    const dLevel = sanitizeDiagField(msg.level, 16)\r\n    const dSource = sanitizeDiagField(msg.source, 64)\r\n    const dMessage = sanitizeDiagField(msg.message, 2000)\r\n    const dDetails = msg.details ? ` — ${sanitizeDiagField(msg.details, 2000)}` : ''\r\n    const dTs = sanitizeDiagField(msg.ts, 32)\r\n    const dMachine = msg.machineId ? sanitizeDiagField(msg.machineId, 64) : '(none)'\r\n    const line =\r\n      `[${dLevel}] [${dSource}] ${dMessage}${dDetails}` +\r\n      ` (mobileTs=${dTs} machine=${dMachine} client=${client})`\r\n    // 런타임 값은 untrusted relay JSON 이라 타입(union) 밖일 수 있어 string 으로 보고 라우팅.\r\n    const rawLevel: string = msg.level\r\n    if (rawLevel === 'error') mobileDiagLog.error(line)\r\n    else if (rawLevel === 'warning') mobileDiagLog.warn(line)\r\n    else if (rawLevel === 'info') mobileDiagLog.info(line)\r\n    else mobileDiagLog.warn(line)\r\n  }\r\n\r\n  /**\r\n   * file.upload.begin — 이름/크기 검증 후 누적 entry 생성. 거절이면 즉시 done(ok:false).\r\n   */\r\n  private onFileUploadBegin(\r\n    msg: Extract<ClientMessage, { type: 'file.upload.begin' }>,\r\n    source: WebSocket | null\r\n  ): void {\r\n    log.info(\r\n      `[ws-manager] file.upload.begin machine=${msg.machineId} id=${msg.uploadId} size=${msg.size} chunks=${msg.totalChunks}`\r\n    )\r\n    const rejected = this.fileUpload.begin(\r\n      msg.machineId,\r\n      msg.uploadId,\r\n      msg.name,\r\n      msg.size,\r\n      msg.totalChunks\r\n    )\r\n    if (rejected) {\r\n      log.warn(\r\n        `[ws-manager] file.upload.begin rejected machine=${msg.machineId} id=${msg.uploadId} error=${rejected.error}`\r\n      )\r\n      this.sendToTarget(source, {\r\n        type: 'file.upload.done',\r\n        machineId: msg.machineId,\r\n        uploadId: msg.uploadId,\r\n        ok: false,\r\n        error: rejected.error,\r\n      })\r\n    }\r\n  }\r\n\r\n  /**\r\n   * file.upload.end — atomic write 후 done 응답. handleMessage 동기 시그니처를 안 바꾸려\r\n   * .then 으로 회신(end 는 fs write 라 Promise). .then 콜백 throw 시도 write_failed 로 즉시 회신\r\n   * (모바일이 30s 타임아웃까지 매달리는 것 방지).\r\n   */\r\n  private onFileUploadEnd(\r\n    msg: Extract<ClientMessage, { type: 'file.upload.end' }>,\r\n    source: WebSocket | null\r\n  ): void {\r\n    const { machineId, uploadId } = msg\r\n    log.info(`[ws-manager] file.upload.end machine=${machineId} id=${uploadId}`)\r\n    this.fileUpload\r\n      .end(uploadId)\r\n      .then(res => {\r\n        log.info(\r\n          `[ws-manager] file.upload.done machine=${machineId} id=${uploadId} ok=${res.ok}` +\r\n            (res.ok ? ` path=${res.path}` : ` error=${res.error}`)\r\n        )\r\n        this.sendToTarget(source, {\r\n          type: 'file.upload.done',\r\n          machineId,\r\n          uploadId,\r\n          ...res,\r\n        })\r\n      })\r\n      .catch(err => {\r\n        log.error(`[ws-manager] file.upload.end failed id=${uploadId}: ${err}`)\r\n        this.sendToTarget(source, {\r\n          type: 'file.upload.done',\r\n          machineId,\r\n          uploadId,\r\n          ok: false,\r\n          error: 'write_failed',\r\n        })\r\n      })\r\n  }\r\n\r\n  /**\r\n   * terminal_authority_takeover_request — 모바일 권위 탈취 요청. resolver 가 desktop\r\n   * renderer 에 모달 재발행(서버는 라우팅만 보장 — UI 는 Phase 2).\r\n   */\r\n  private onTakeoverRequest(\r\n    msg: Extract<ClientMessage, { type: 'terminal_authority_takeover_request' }>,\r\n    source: WebSocket | null\r\n  ): void {\r\n    if (source) {\r\n      const meta = this.clientMeta.get(source)\r\n      if (meta?.clientId) {\r\n        this.ssh.authority.onMobileTakeoverRequest(msg.machineId, meta.clientId)\r\n      }\r\n    }\r\n  }\r\n\r\n  /**\r\n   * session_attach 처리 — ensure PTY + scrollback 청크 분할 송신.\r\n   * 핵심: 구 모바일이 보내는 connect 와 달리, 신 모바일은 attach 후 session_replay 를\r\n   * 받아 자신의 xterm 에 prepend. attach 시점에 PTY 가 살아있던 자식 프로세스의 출력이\r\n   * 모두 모바일 화면에 복원된다.\r\n   *\r\n   * task 10 C7: cols/rows 가 들어오면 첫 PTY spawn 의 size 로 사용 (default 80x24\r\n   * race 차단). 기존 PTY 가 살아있으면 즉시 ssh.resize(source='mobile') 1회 적용해\r\n   * mobile 의 첫 layout 까지의 race window 동안 grid mismatch 방지.\r\n   */\r\n  private handleSessionAttach(\r\n    machineId: string,\r\n    source: WebSocket | null,\r\n    cols?: number,\r\n    rows?: number\r\n  ): void {\r\n    log.info(\r\n      `[ws-manager] session_attach machine=${machineId} ` +\r\n        `hasSession=${this.ssh.hasSession(machineId)} ` +\r\n        `attachSize=${cols ?? '?'}x${rows ?? '?'}`\r\n    )\r\n\r\n    // [A2 spike 20260523] cooldown 체크 — Take Back 후 5s 안에 모바일 attach 시도면 거부.\r\n    // close code 4008 (TAKEBACK_COOLDOWN) → mobile DisconnectReason.rejectedCooldown →\r\n    // RejectedBanner 풀스크린 표시 (C6 에서 구현). source=null 인 IPC 경로는 skip.\r\n    if (source && this.ssh.authority.isInTakebackCooldown(machineId)) {\r\n      log.warn(\r\n        `[ws-manager] session_attach REJECTED (cooldown) machine=${machineId}`\r\n      )\r\n      // [Bug #3 fix 20260526] App-level kick frame (cooldown reason).\r\n      // relay 경로 mobile 도 RejectedBanner 발화 가능 — 기존 source.close(4008) 만\r\n      // 으로는 relayWs 닫기 부작용 + relay forward 없어서 mobile 무반응.\r\n      this.broadcast({\r\n        type: 'desktop.kick',\r\n        machineId,\r\n        reason: 'cooldown',\r\n        cooldownUntil: this.ssh.authority.getTakebackCooldownUntil(machineId),\r\n      })\r\n      if (source !== this.relayWs) {\r\n        try {\r\n          source.close(4008, 'TAKEBACK_COOLDOWN')\r\n        } catch {\r\n          // 이미 closed\r\n        }\r\n      }\r\n      // resolver 의 onMobileAttach 가 viewer_mode='rejected' broadcast — desktop 측 UI 갱신.\r\n      // 단 clientId 가 있어야 의미 있으므로 meta lookup 후 호출.\r\n      const meta = this.clientMeta.get(source)\r\n      if (meta?.clientId) {\r\n        this.ssh.authority.onMobileAttach(machineId, meta.clientId, cols, rows)\r\n      }\r\n      return\r\n    }\r\n\r\n    // [A2 spike] mobileClientByMachine 트래킹 — handleDesktopTakeback 의 WS lookup 용.\r\n    if (source) {\r\n      this.mobileClientByMachine.set(machineId, source)\r\n    }\r\n\r\n    // [A2 spike] viewer_mode='attaching' broadcast — desktop UI 의 \"Phone connecting…\"\r\n    // overlay 트리거. resolver.onMobileAttach 호출 전에 발화되어야 핸드오프 직전 race\r\n    // window UX 가 자연스러움 (Design Fix-E D2-δ row 1).\r\n    this.ssh.authority.broadcastAttaching(machineId, cols, rows)\r\n\r\n    // [REQ1 busy-attach 20260608] 기존 살아있는 PTY 에 재attach 하는지 여부 — 아래\r\n    // nudgeFullRepaintOnAttach 게이트로 사용(신규 spawn 은 fresh 출력이라 넛지 불필요).\r\n    const hadSession = this.ssh.hasSession(machineId)\r\n\r\n    // [재접속 scrollback fix 20260620] mobile resize *직전* 의 PTY 폭 = scrollback tail 이\r\n    // 생성된 폭. 아래 session_replay 에 동봉해 모바일이 그 폭으로 raw replay 를 그린 뒤\r\n    // viewport 폭으로 reflow → 재접속 시 스크롤백이 좁은 폭으로 재-wrap 되어 좌측으로 붕괴하던\r\n    // 버그 회피. 미측정(콜드 spawn — PTY 아직 없음)이면 undefined → omit → 모바일 기존 동작.\r\n    const genSize = this.ssh.getPtySize(machineId)\r\n    const replaySizeFields =\r\n      genSize && genSize.cols > 0 && genSize.rows > 0\r\n        ? { cols: genSize.cols, rows: genSize.rows }\r\n        : {}\r\n\r\n    // C7 — viewport size 흡수. cols/rows 동봉이 없는 구 모바일 (PROTOCOL_VERSION<3)\r\n    // 은 hint 미사용으로 default fallback (이전 동작).\r\n    if (\r\n      typeof cols === 'number' &&\r\n      typeof rows === 'number' &&\r\n      cols > 0 &&\r\n      rows > 0\r\n    ) {\r\n      if (!this.ssh.hasSession(machineId)) {\r\n        // spawn 직전 hint — ssh-manager 가 spawn cols/rows 로 사용.\r\n        this.ssh.setSpawnSizeHint(machineId, cols, rows)\r\n      } else {\r\n        // 이미 살아있는 PTY — mobile size 로 즉시 resize (mobile-priority).\r\n        // RESIZE opcode 가 곧 따라오지만 첫 frame race 를 막기 위해 attach 시점 1회.\r\n        // ssh.resize 는 sync (void) — 내부 throw 자체 흡수 처리됨.\r\n        this.ssh.resize(machineId, cols, rows, 'mobile')\r\n      }\r\n    }\r\n\r\n    // PTY 가 없으면 spawn, 있으면 재사용 (ssh.connect 가 ensure 패턴).\r\n    this.ssh.connect(machineId)\r\n    // lifecycle: lastAttach 갱신 + pty_status 즉시 broadcast (throttle bypass).\r\n    // mobile 첫 화면이 정확한 status 받음.\r\n    this.ssh.lifecycle.onAttach(machineId)\r\n\r\n    // Phase 1 — AuthorityResolver 에 mobile attach 통지 → 즉시 leader=mobile (A2 spike).\r\n    // grace-pending + 같은 clientId 면 즉시 복귀. (desktop-priority 정책은 2026-06-07 제거.)\r\n    if (source) {\r\n      const meta = this.clientMeta.get(source)\r\n      if (meta?.clientId) {\r\n        this.ssh.authority.onMobileAttach(machineId, meta.clientId, cols, rows)\r\n      }\r\n    }\r\n\r\n    // [REQ1 busy-attach 20260608] 기존 PTY 재attach + 유효 size 면 보장된 full-repaint 넛지.\r\n    // busy alt-screen TUI(claude 등)는 attach 시 raw tail(마지막 상태바 조각)만 복원되는데,\r\n    // 위 line~699 의 attach resize 는 이미 동일 size 면 noop 이거나 desktop-leader reattach 면\r\n    // authority drop 이라, 어느 쪽이든 SIGWINCH 가 안 나 화면이 다시 안 그려질 수 있다.\r\n    // nudgeFullRepaintOnAttach 가 onMobileAttach(leader=mobile) *뒤* 에서 보장된 size 델타로\r\n    // redraw 를 강제한다.\r\n    if (\r\n      hadSession &&\r\n      typeof cols === 'number' &&\r\n      typeof rows === 'number' &&\r\n      cols > 0 &&\r\n      rows > 1\r\n    ) {\r\n      this.nudgeFullRepaintOnAttach(machineId, cols, rows)\r\n    }\r\n\r\n    // [attach frame-snapshot 20260610] busy alt-screen TUI(claude 등) 화면 정확 복원.\r\n    // raw scrollback tail 만 보내는 아래 session_replay 로는 alt-screen 을 재구성할 수 없어\r\n    // 모바일이 \"조각\"만 보던 버그를 고친다 — desktop 의 정확한-size GridEmulator 를 ANSI 로\r\n    // 직렬화해 FRAME_SNAPSHOT (0x03) binary frame 으로 송신한다.\r\n    //\r\n    // ⚠️ 순서 의존: 모바일은 이 0x03 frame 을 buffer 했다가 session_replay 가 *끝난* 직후\r\n    // (`\\x1b[2J\\x1b[H` reset + ANSI) overlay 한다. 동일 WS 가 순서를 보존하므로 반드시\r\n    // session_replay 송신 *전에* 보낸다.\r\n    if (\r\n      attachFrameSnapshotEnabled() &&\r\n      hadSession &&\r\n      typeof cols === 'number' &&\r\n      typeof rows === 'number' &&\r\n      cols > 0 &&\r\n      rows > 1\r\n    ) {\r\n      const frame = this.buildFrameSnapshot(machineId)\r\n      if (frame) {\r\n        const diag = this.ssh.lifecycle.gridSnapshotDiag(machineId)\r\n        log.info(\r\n          `[ws-manager] attach_frame_snapshot machine=${machineId} bytes=${frame.length}` +\r\n            (diag\r\n              ? ` bufType=${diag.bufType} nonBlankLines=${diag.nonBlankLines}/${diag.rows} glyphs=${diag.glyphs}`\r\n              : '')\r\n        )\r\n        if (source && source.readyState === WebSocket.OPEN) {\r\n          source.send(frame)\r\n        } else {\r\n          // relay 경로 등 source 가 OPEN 이 아니면 이 머신에 attach 된 모바일에 fan-out.\r\n          // legacy pty.data path 이므로 cap filter 없이 attachedMachineId 일치만 검사.\r\n          this.sendBinaryToMachine(machineId, frame)\r\n        }\r\n      }\r\n    }\r\n\r\n    const buffer = this.ssh.getScrollback(machineId)\r\n    if (!buffer || buffer.isEmpty()) {\r\n      // 빈 replay 라도 한 번 송신 — 클라이언트가 \"attach 완료\" 시그널로 사용 (live data 받기 시작).\r\n      this.sendToTarget(source, {\r\n        type: 'session_replay',\r\n        machineId,\r\n        chunkIndex: 0,\r\n        totalChunks: 1,\r\n        data: '',\r\n        ...replaySizeFields,\r\n      })\r\n      return\r\n    }\r\n\r\n    // cross-repo with claude_code_mobile autoplan 2026-05-22 UC-3 — attach 시점\r\n    // 송신은 tail cap (`REPLAY_ATTACH_TAIL_BYTES`). scrollback buffer 의 cap\r\n    // (`SCROLLBACK_BUFFER_BYTES_DEFAULT` 또는 머신별 `ringBufferBytes`) 는 그대로 —\r\n    // append 누적 정책 변경 0, mobile attach 시 송신 한도만 제한.\r\n    const chunks = buffer.snapshotChunksForAttach(\r\n      REPLAY_ATTACH_TAIL_BYTES,\r\n      REPLAY_CHUNK_BYTES\r\n    )\r\n    const total = chunks.length\r\n    log.info(\r\n      `[ws-manager] session_replay machine=${machineId} bytes=${buffer.size()} ` +\r\n        `chunks=${total} cap=${buffer.capacity()} tail=${REPLAY_ATTACH_TAIL_BYTES}`\r\n    )\r\n    for (let i = 0; i < total; i++) {\r\n      this.sendToTarget(source, {\r\n        type: 'session_replay',\r\n        machineId,\r\n        chunkIndex: i,\r\n        totalChunks: total,\r\n        data: chunks[i],\r\n        ...replaySizeFields,\r\n      })\r\n    }\r\n  }\r\n\r\n  /**\r\n   * [resize-resettle frame-snapshot 20260610] GridEmulator 현재 화면을 ANSI 로 직렬화해\r\n   * FRAME_SNAPSHOT (0x03) Buffer 로 인코딩한다. 직렬화 실패/빈 화면이면 null.\r\n   *\r\n   * attach·resize-resettle 두 경로가 공유한다(DRY) — 송신 채널만 호출부가 결정.\r\n   * env 게이트(`attachFrameSnapshotEnabled`)는 호출부 책임(여긴 순수 build).\r\n   */\r\n  private buildFrameSnapshot(machineId: string): Buffer | null {\r\n    const ansi = this.ssh.lifecycle.serializeGridAnsi(machineId)\r\n    if (!ansi || ansi.length === 0) return null\r\n    return encodeFrameSnapshot(machineId, ansi)\r\n  }\r\n\r\n  /**\r\n   * [resize-resettle frame-snapshot 20260610] RESIZE 수신 후 디바운스해 FRAME_SNAPSHOT 재방출.\r\n   *\r\n   * WHY: 모바일이 attach 직후 미측정 80x24 로 붙었다가 ~233ms 뒤 실제 viewport(예: 77x67)로\r\n   * RESIZE 하는 race 에서, attach 시 직렬화된 스냅샷은 잘못된 행수 + claude SIGWINCH repaint 전\r\n   * reflow 잔해라 \"조각\"으로 보인다. RESIZE 가 멎고(디바운스) claude 가 새 size 로 repaint 를\r\n   * 끝낸 뒤 한 번 더 직렬화해 올바른 화면을 재방출한다. 키보드 토글/회전 리사이즈도 같이 커버.\r\n   *\r\n   * 송신은 `sendBinaryToMachine`(머신 라우팅) — relay/LAN attach 모바일 모두 도달. 모바일은 이\r\n   * standalone 0x03(뒤따르는 session_replay 없음)을 즉시 적용한다(`_applyFrameSnapshotNow`).\r\n   */\r\n  private scheduleResizeFrameSnapshot(machineId: string): void {\r\n    if (!attachFrameSnapshotEnabled()) return\r\n    const existing = this.resizeSnapshotTimers.get(machineId)\r\n    if (existing) clearTimeout(existing)\r\n    this.resizeSnapshotTimers.set(\r\n      machineId,\r\n      setTimeout(() => {\r\n        this.resizeSnapshotTimers.delete(machineId)\r\n        const frame = this.buildFrameSnapshot(machineId)\r\n        if (frame) {\r\n          const diag = this.ssh.lifecycle.gridSnapshotDiag(machineId)\r\n          log.info(\r\n            `[ws-manager] resize_frame_snapshot machine=${machineId} bytes=${frame.length}` +\r\n              (diag\r\n                ? ` bufType=${diag.bufType} nonBlankLines=${diag.nonBlankLines}/${diag.rows} glyphs=${diag.glyphs}`\r\n                : '')\r\n          )\r\n          this.sendBinaryToMachine(machineId, frame)\r\n        }\r\n      }, RESIZE_SNAPSHOT_DEBOUNCE_MS)\r\n    )\r\n  }\r\n\r\n  /**\r\n   * [REQ1 busy-attach 20260608] busy alt-screen TUI 가 attach 시 화면 전체를 다시 그리도록\r\n   * 강제하는 SIGWINCH 넛지.\r\n   *\r\n   * WHY: `ssh.resize` 는 size 가 직전과 동일하면 noop(setWindow/multiplexer refresh skip,\r\n   * ssh-manager.ts D-1) 이라, attach 시점에 PTY 가 이미 mobile size 면 (line ~699 의 resize 가\r\n   * noop → ) SIGWINCH 가 안 나 화면 repaint 가 안 일어난다. 그 결과 모바일은 raw scrollback\r\n   * tail 의 마지막 부분(busy TUI 의 in-place 상태바 재기록 = \"Actioning…\" 조각)만 복원하고\r\n   * full frame 을 못 받는다 (docs/requests/2026-06/20260608-busy-attach-full-state-restore-...).\r\n   *\r\n   * off-size 로 한 번 흔든 뒤 즉시 정상 size 로 되돌려, 두 resize 모두 실제 델타가 되게 해\r\n   * setWindow(SIGWINCH) + multiplexer refresh-client(alt-screen TUI 강제 redraw, ssh-manager.ts\r\n   * resize 주석)를 발화시킨다 → 앱이 현재 geometry 의 full frame 을 새 viewer 에 흘려보낸다.\r\n   *\r\n   * [adversarial review fix 20260608 — 2건]\r\n   *  ① clamp 충돌 회피: `clampRows` 가 PTY_MIN_ROWS(5) 미만을 5 로 floor 하므로 rows<=5 면\r\n   *     rows-1 이 같은 5 로 접혀 둘 다 noop(D-1)이 돼 repaint 가 안 난다. 바닥 근처면 off 를\r\n   *     위(PTY_MIN_ROWS+1)로 잡아 clamp 후에도 distinct 하게.\r\n   *  ② 동기 실행(setTimeout 제거): 지연 step 은 그 사이 도착한 모바일의 실제 RESIZE 를 stale\r\n   *     attach-time size(머신전환 시 80x24 fallback)로 덮어써 영구 misfit 을 만든다(모바일은\r\n   *     resize ack 무시). 같은 tick 에 동기로 치면 모바일 RESIZE 가 항상 뒤에 와서 이기고\r\n   *     (기존 순서 보존), untracked timer 가 재spawn 세션을 잘못 resize 하는 race 도 없다.\r\n   *\r\n   * source='mobile' — onMobileAttach 직후 leader=mobile 이라 `authority.allowsResize` 통과\r\n   * (이 호출이 onMobileAttach *뒤* 인 것에 정확성이 의존한다 — 그 전엔 desktop-leader 라 drop).\r\n   */\r\n  private nudgeFullRepaintOnAttach(\r\n    machineId: string,\r\n    cols: number,\r\n    rows: number\r\n  ): void {\r\n    const off = rows > PTY_MIN_ROWS ? rows - 1 : PTY_MIN_ROWS + 1\r\n    this.ssh.resize(machineId, cols, off, 'mobile')\r\n    this.ssh.resize(machineId, cols, rows, 'mobile')\r\n  }\r\n\r\n  /**\r\n   * source ws 가 있으면 그 클라이언트에게만, 없으면 (relay 경유) 전체 broadcast.\r\n   * session_replay 는 큰 데이터라서 broadcast 낭비 — LAN 모바일 attach 시 source 사용.\r\n   * v3 — APP_JSON opcode wrap.\r\n   */\r\n  private sendToTarget(target: WebSocket | null, msg: ServerMessage): void {\r\n    if (target && target.readyState === WebSocket.OPEN) {\r\n      target.send(encodeAppJson(msg))\r\n    } else {\r\n      this.broadcastToMobile(msg)\r\n    }\r\n  }\r\n\r\n  /**\r\n   * A3 (Sub-Part 1F-ii-b) — GRID_SNAPSHOT (0x04) / GRID_DIFF (0x05) binary frame\r\n   * broadcast helper.\r\n   *\r\n   * Filter 정책 (spec §6.5):\r\n   *   - cap=`grid.diff` client 에만 fan-out (cap=`pty.data` 는 PTY_DATA 그대로 받음 = legacy)\r\n   *   - 동시에 attachedMachineId 가 일치해야 함 (다른 머신 attach 한 client 무관)\r\n   *   - mobile (LAN ws) + relayWs 둘 다 처리\r\n   *\r\n   * 호출 path:\r\n   *   - ssh-manager 의 flushData (16ms 디바운스) → lifecycle.flushDiff → 본 함수\r\n   *   - bonus: session_attach 시 lifecycle.onAttachGridDiff 도 본 함수 거치지 않고 ws.send 직접\r\n   *     (1:1 단일 client 대상 — broadcast 가 아님)\r\n   */\r\n  broadcastGridFrame(machineId: string, frame: Buffer): void {\r\n    for (const ws of this.mobileClients) {\r\n      if (\r\n        this.mobileClientCaps.get(ws) === 'grid.diff' &&\r\n        this.clientMeta.get(ws)?.attachedMachineId === machineId &&\r\n        ws.readyState === WebSocket.OPEN\r\n      ) {\r\n        ws.send(frame)\r\n      }\r\n    }\r\n    if (\r\n      this.relayWs &&\r\n      this.mobileClientCaps.get(this.relayWs) === 'grid.diff' &&\r\n      this.clientMeta.get(this.relayWs)?.attachedMachineId === machineId &&\r\n      this.relayWs.readyState === WebSocket.OPEN\r\n    ) {\r\n      this.relayWs.send(frame)\r\n    }\r\n  }\r\n\r\n  /**\r\n   * 특정 머신에 attach 된 mobile/relay client 에 binary frame fan-out — **cap filter 없음**.\r\n   *\r\n   * `broadcastGridFrame` 과 달리 cap=`grid.diff` 필터를 적용하지 않는다. session_attach 의\r\n   * FRAME_SNAPSHOT (0x03) 은 legacy pty.data path 의 모바일도 화면 복원에 사용하므로,\r\n   * attachedMachineId 일치 + OPEN 만 검사한다. source 가 OPEN 이 아닐 때(relay 경로 등)의 fallback.\r\n   */\r\n  private sendBinaryToMachine(machineId: string, frame: Buffer): void {\r\n    for (const ws of this.mobileClients) {\r\n      if (\r\n        this.clientMeta.get(ws)?.attachedMachineId === machineId &&\r\n        ws.readyState === WebSocket.OPEN\r\n      ) {\r\n        ws.send(frame)\r\n      }\r\n    }\r\n    if (\r\n      this.relayWs &&\r\n      this.clientMeta.get(this.relayWs)?.attachedMachineId === machineId &&\r\n      this.relayWs.readyState === WebSocket.OPEN\r\n    ) {\r\n      this.relayWs.send(frame)\r\n    }\r\n  }\r\n\r\n  /**\r\n   * snapshot.req frequency cap (3/min/session) — sliding window.\r\n   *\r\n   * 동일 session (ws) 의 최근 60s 안 timestamp 가 3개 이상이면 throttle (`true` 반환).\r\n   * `snapshotReqTimes` 가 WeakMap 이라 ws GC 시 자동 정리.\r\n   *\r\n   * spec §6.6 + plan §13-9. emergency_hatch_button_pressed_count rate alarm 의 source.\r\n   */\r\n  private snapshotReqThrottle(ws: WebSocket): boolean {\r\n    const now = Date.now()\r\n    const cutoff = now - 60_000\r\n    const times = this.snapshotReqTimes.get(ws) ?? []\r\n    const recent = times.filter(t => t > cutoff)\r\n    if (recent.length >= 3) {\r\n      this.snapshotReqTimes.set(ws, recent)\r\n      return true\r\n    }\r\n    recent.push(now)\r\n    this.snapshotReqTimes.set(ws, recent)\r\n    return false\r\n  }\r\n\r\n  // ── 수동 재연결 (렌더러 또는 Flutter 앱에서 요청) ──────────────────────────\r\n\r\n  reconnect(): void {\r\n    if (this.destroyed) return\r\n    log.info('[relay] manual reconnect requested')\r\n    this.stopHeartbeat()\r\n    if (this.relayWs) {\r\n      this.relayWs.terminate()\r\n      this.relayWs = null\r\n    }\r\n    this.relayStatus = 'disconnected'\r\n    this.relayRetryCount = 0\r\n    this.connectToRelay()\r\n  }\r\n\r\n  // ── Local WebSocket server (mobile clients on LAN) ────────────────────────\r\n\r\n  private startLocalServer(wsPort: number, authMs: number): void {\r\n    try {\r\n      // WebSocketServer 는 포트 바인딩을 **비동기**로 수행한다 — 포트가 이미 사용 중이면\r\n      // EADDRINUSE 가 동기 throw 가 아니라 wss 인스턴스의 'error' 이벤트로 방출된다. 핸들러가\r\n      // 없으면 Node 가 \"Unhandled 'error' event\" 로 프로세스 전체를 죽여, 헤드리스 TUI/데스크탑\r\n      // 렌더러가 뜨기도 전에 종료된다(= 옵션에서 포트를 바꿔 복구할 길조차 막힘). 따라서\r\n      // try/catch(동기 한정)에 더해 반드시 'error' 리스너를 걸어 비동기 바인딩 실패를 흡수한다.\r\n      const wss = new WebSocketServer({ port: wsPort, host: '0.0.0.0' })\r\n      this.wss = wss\r\n\r\n      wss.on('error', (err: NodeJS.ErrnoException) => {\r\n        // 로컬 LAN 서버 바인딩 실패는 치명적이지 않다 — 모바일 주 접속 경로인 relay 는 이 서버와\r\n        // 독립적으로 살아있다. 크래시 대신 호스트(헤드리스 verbose 로그 / 데스크탑 console)에\r\n        // 알리고 계속 진행해, 사용자가 옵션에서 WS 포트를 바꾼 뒤 재시작할 수 있게 한다.\r\n        const message =\r\n          err.code === 'EADDRINUSE'\r\n            ? `WS_PORT_IN_USE:${wsPort} (LAN direct unavailable; relay unaffected)`\r\n            : `WS_SERVER_ERROR:${err.code ?? err.message}`\r\n        log.error(`[ws-manager] local server error on port ${wsPort}: ${err.message}`)\r\n        if (this.wss === wss) this.wss = null\r\n        // host 측에만 — 로컬 서버 진단이며 부팅 시점엔 모바일이 없다(relay flap 회피).\r\n        this.broadcast({ type: 'error', message }, { skipMobile: true })\r\n        // EADDRINUSE = 이 포트를 이미 다른 인스턴스가 선점 = 같은 PC 에 host 중복 실행.\r\n        // 부팅 즉시 사용자에게 경고(relay 핑퐁을 기다릴 필요 없이 곧바로 감지).\r\n        if (err.code === 'EADDRINUSE') {\r\n          this.setHostConflict('port_in_use')\r\n        }\r\n      })\r\n\r\n      // 실제 바인딩 성공 시점에만 로그. 동기 try-블록 끝에서 찍으면 EADDRINUSE(다음 tick 의\r\n      // 'error' 이벤트)일 때도 \"Listening on\" 이 먼저 찍혀 진단을 오도한다 — 성공 신호인\r\n      // 'listening' 이벤트로 옮겨 성공/실패 로그가 모순되지 않게 한다.\r\n      wss.on('listening', () => {\r\n        log.info(`[ws-manager] Listening on ws://0.0.0.0:${wsPort}`)\r\n      })\r\n\r\n      wss.on('connection', (ws: WebSocket, req: IncomingMessage) => {\r\n        const isMobile = req.url === '/mobile'\r\n        if (!isMobile) {\r\n          // Non-mobile connections are not supported in Electron mode\r\n          ws.close(1008, 'Renderer uses IPC')\r\n          return\r\n        }\r\n\r\n        // ── HMAC 챌린지-응답 인증 ────────────────────────────────────────────\r\n        // v3 — 모든 ws.send 가 APP_JSON opcode wrap. 수신은 binary buffer parse.\r\n        let authenticated = false\r\n        const nonce = randomBytes(16).toString('hex')\r\n        ws.send(\r\n          encodeAppJson({\r\n            type: 'auth_challenge',\r\n            nonce,\r\n          } satisfies ServerMessage)\r\n        )\r\n\r\n        const authTimer = setTimeout(() => {\r\n          if (!authenticated) {\r\n            ws.send(\r\n              encodeAppJson({\r\n                type: 'auth_fail',\r\n                reason: 'timeout',\r\n              } satisfies ServerMessage)\r\n            )\r\n            ws.close()\r\n          }\r\n        }, authMs)\r\n\r\n        ws.on('message', raw => {\r\n          // v3 binary protocol — text frame 거부. v2 client 가 v3 server 에 붙으면\r\n          // 첫 frame 이 text 이므로 즉시 close.\r\n          if (typeof raw === 'string') {\r\n            ws.send(\r\n              encodeProtocolError(PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD)\r\n            )\r\n            ws.close(4005, 'binary only (v3)')\r\n            return\r\n          }\r\n          const buffer = Buffer.isBuffer(raw)\r\n            ? raw\r\n            : Array.isArray(raw)\r\n              ? Buffer.concat(raw as unknown as readonly Uint8Array[])\r\n              : Buffer.from(raw as ArrayBuffer)\r\n\r\n          let parsed: ReturnType<typeof parseFrame>\r\n          try {\r\n            parsed = parseFrame(buffer)\r\n          } catch (err) {\r\n            const reason =\r\n              err instanceof FrameParseError\r\n                ? err.reason\r\n                : PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD\r\n            ws.send(encodeProtocolError(reason))\r\n            return\r\n          }\r\n\r\n          // RESIZE opcode (0x02) — 첫 frame 으로 도착 시 auth 안 됨 상태에서 무시.\r\n          if (parsed.type === 'resize') {\r\n            if (!authenticated) return\r\n            const machineId =\r\n              this.clientMeta.get(ws)?.attachedMachineId ??\r\n              this.lastAttachedMachineId ??\r\n              undefined\r\n            if (machineId) {\r\n              this.ssh.resize(machineId, parsed.cols, parsed.rows, 'mobile')\r\n              // [resize-resettle 20260610] settle 후 올바른-size 스냅샷 재방출 (조각 race 차단).\r\n              this.scheduleResizeFrameSnapshot(machineId)\r\n            } else {\r\n              log.warn(\r\n                `[ws-manager] RESIZE opcode 도착했으나 attachedMachineId 없음 — 무시`\r\n              )\r\n            }\r\n            return\r\n          }\r\n\r\n          // PROTOCOL_ERROR — client 가 server 에 mismatch 통보. 단순 close.\r\n          if (parsed.type === 'protocol_error') {\r\n            log.warn(\r\n              `[ws-manager] client PROTOCOL_ERROR reason=0x${parsed.reason.toString(16)}`\r\n            )\r\n            ws.close()\r\n            return\r\n          }\r\n\r\n          // 그 외 (PTY_DATA / FRAME_SNAPSHOT) — desktop 측은 송신만, 수신 무시\r\n          if (parsed.type !== 'app_json') return\r\n\r\n          try {\r\n            const msg = parsed.json as ClientMessage\r\n\r\n            if (!authenticated) {\r\n              // hello — auth 전 도착 가능. server 가 아직 hello_ack 보낼 자격 없음 (auth 후로 미룸).\r\n              // hello_ack 는 auth_ok 직후 송신.\r\n              if ((msg as any).type === 'hello') {\r\n                ;(ws as any).pendingHello = true\r\n                return\r\n              }\r\n              if (msg.type === 'auth') {\r\n                const expected = createHmac('sha256', SESSION_TOKEN)\r\n                  .update(nonce)\r\n                  .digest('hex')\r\n                if (msg.token === expected) {\r\n                  authenticated = true\r\n                  clearTimeout(authTimer)\r\n                  ws.send(\r\n                    encodeAppJson({ type: 'auth_ok' } satisfies ServerMessage)\r\n                  )\r\n                  // 인증 성공 + pending hello 가 있었으면 hello_ack 송신\r\n                  if ((ws as any).pendingHello) {\r\n                    ws.send(encodeHelloAck())\r\n                  }\r\n                  // 초기 상태 동기화\r\n                  this.mobileClients.add(ws)\r\n                  // Phase 1 [Review Q2] — clientId 부여. AuthorityResolver 가 handover/\r\n                  // detach 시 동일 모바일 식별하는 key. WeakMap 이라 ws GC 시 자동 정리.\r\n                  this.clientMeta.set(ws, {\r\n                    clientId: `mob-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\r\n                  })\r\n                  // A3: cap nego 받기 전 default = legacy 'pty.data'. cap.req 받으면 갱신.\r\n                  this.mobileClientCaps.set(ws, 'pty.data')\r\n                  ws.send(\r\n                    encodeAppJson({\r\n                      type: 'machine_list',\r\n                      machines: this.ssh.getMachines(),\r\n                    })\r\n                  )\r\n                  ws.send(\r\n                    encodeAppJson({\r\n                      type: 'relay_status',\r\n                      status: this.relayStatus,\r\n                      retryCount: this.relayRetryCount,\r\n                    })\r\n                  )\r\n                } else {\r\n                  ws.send(\r\n                    encodeAppJson({\r\n                      type: 'auth_fail',\r\n                      reason: 'invalid_token',\r\n                    } satisfies ServerMessage)\r\n                  )\r\n                  ws.close()\r\n                }\r\n              }\r\n              return\r\n            }\r\n\r\n            // hello — auth 후 도착 (relay path 등). hello_ack 즉시 응답.\r\n            if ((msg as any).type === 'hello') {\r\n              ws.send(encodeHelloAck())\r\n              return\r\n            }\r\n\r\n            // session_attach 시 clientMeta.attachedMachineId 갱신 (RESIZE opcode + close handler 의 라우팅 키).\r\n            // [/review fix Q2-complete] (ws as any).attachedMachineId 잔존 제거 — WeakMap 단일화.\r\n            if (\r\n              msg.type === 'session_attach' &&\r\n              typeof (msg as any).machineId === 'string'\r\n            ) {\r\n              const mid = (msg as any).machineId as string\r\n              const existing = this.clientMeta.get(ws)\r\n              if (existing) existing.attachedMachineId = mid\r\n              // A3 (Sub-Part 1F-ii-a bonus): cap=grid.diff client 면 attach 직후 GRID_SNAPSHOT 자동 송신.\r\n              // 사용자가 snapshot.req 명시 발사 안 해도 cold attach 시 grid 즉시 받음.\r\n              if (this.mobileClientCaps.get(ws) === 'grid.diff') {\r\n                log.info(\r\n                  `[ws-manager] session_attach + cap=grid.diff → auto GRID_SNAPSHOT machineId=${mid}`\r\n                )\r\n                this.ssh.lifecycle.onAttachGridDiff(mid, frame =>\r\n                  ws.send(frame)\r\n                )\r\n              }\r\n            }\r\n\r\n            // A3: capability negotiation (binary protocol v3 §6.5).\r\n            // Sub-Part 1F-ii-c: 'grid.diff' 활성화. server-side emulator (PtyLifecycleManager 의 emulators)\r\n            // 가 silent 로 grid 누적 중 (Sub-Part 1F-i 완료). GRID_SNAPSHOT/GRID_DIFF wire 송신은 추후\r\n            // 1F-ii-a (snapshot.req hookup) + 1F-ii-b (flushDiff broadcast) 완료 시 활성화.\r\n            // 현 시점: cap.ack 는 'grid.diff' 응답하나 PTY_DATA 도 그대로 broadcast (filter 미적용 = 1F-ii-d).\r\n            // mobile 측 (Part 2E) 도 미활성이라 production mobile 영향 0.\r\n            if ((msg as any).type === 'cap.req') {\r\n              const clientCaps = ((msg as any).client_caps as string[]) ?? []\r\n              const selected: 'pty.data' | 'grid.diff' = clientCaps.includes(\r\n                'grid.diff'\r\n              )\r\n                ? 'grid.diff'\r\n                : 'pty.data'\r\n              this.mobileClientCaps.set(ws, selected)\r\n              ws.send(\r\n                encodeAppJson(\r\n                  buildCapAck(selected, this.config.getInstallId(), this.identity.version)\r\n                )\r\n              )\r\n              return\r\n            }\r\n\r\n            // A3: snapshot.req — throttle (3/min/session) 검사 후 GRID_SNAPSHOT 응답.\r\n            // Sub-Part 1F-ii-a: ssh.lifecycle.onSnapshotReq hookup. emulator 가 grid 누적 중 (1F-i)\r\n            // 이라 snapshot 즉시 송신 가능. 사용자 새로고침 탭 (manual) / auto-recovery (recover) /\r\n            // cold reattach 모두 동일 처리. 응답 frame 은 GRID_SNAPSHOT (0x04) 1개.\r\n            if ((msg as any).type === 'snapshot.req') {\r\n              if (this.snapshotReqThrottle(ws)) {\r\n                log.warn(\r\n                  '[ws-manager] snapshot.req throttled (3/min/session cap)'\r\n                )\r\n                return\r\n              }\r\n              const machineId = this.clientMeta.get(ws)?.attachedMachineId\r\n              if (!machineId) return\r\n              const reason = (msg as any).reason ?? 'manual'\r\n              log.info(\r\n                `[ws-manager] snapshot.req machineId=${machineId} reason=${reason} — sending GRID_SNAPSHOT`\r\n              )\r\n              this.ssh.lifecycle.onSnapshotReq(machineId, frame =>\r\n                ws.send(frame)\r\n              )\r\n              return\r\n            }\r\n\r\n            this.handleMessage(msg, ws)\r\n          } catch {\r\n            ws.send(\r\n              encodeAppJson({\r\n                type: 'error',\r\n                message: 'Invalid message format',\r\n              })\r\n            )\r\n          }\r\n        })\r\n\r\n        ws.on('close', () => {\r\n          clearTimeout(authTimer)\r\n          // Phase 1 — close handler 안에서는 ws ref 가 살아있어 WeakMap 안전 lookup.\r\n          // resolver 가 leader=mobile + clientId 매칭 시에만 grace 시작 (false-positive 방지).\r\n          const meta = this.clientMeta.get(ws)\r\n          this.mobileClients.delete(ws)\r\n          this.mobileClientCaps.delete(ws)\r\n          // snapshotReqTimes 는 WeakMap — ws GC 시 자동 정리, 명시 cleanup 불요.\r\n          if (meta?.clientId && meta.attachedMachineId) {\r\n            // [A2 spike] machineId → WS lookup cleanup (handleDesktopTakeback 대상 정리).\r\n            // 다른 ws 가 같은 머신에 이미 attach 했을 가능성은 mobileClientByMachine.get 으로\r\n            // 본인 ws 확인 후만 delete.\r\n            if (this.mobileClientByMachine.get(meta.attachedMachineId) === ws) {\r\n              this.mobileClientByMachine.delete(meta.attachedMachineId)\r\n            }\r\n            this.ssh.authority.onMobileDetach(\r\n              meta.attachedMachineId,\r\n              meta.clientId\r\n            )\r\n          }\r\n        })\r\n      })\r\n    } catch (err) {\r\n      console.error(\r\n        `[ws-manager] Failed to start: port ${this.wsPort} may be in use.`,\r\n        err\r\n      )\r\n    }\r\n  }\r\n\r\n  // ── Relay client (connects to Fly.io relay as \"host\") ─────────────────────\r\n\r\n  private connectToRelay(): void {\r\n    // 이미 연결 중이거나 연결된 상태면 중복 연결 방지\r\n    if (\r\n      this.destroyed ||\r\n      this.relayStatus === 'connecting' ||\r\n      this.relayStatus === 'connected'\r\n    )\r\n      return\r\n\r\n    const relayUrls = this.config.getRelayUrls()\r\n    const rawBase = relayUrls[this.relayUrlIndex % relayUrls.length]\r\n    // trailing slash 정규화: 설정/env 에 `.../relay/` 처럼 끝 슬래시가 있으면\r\n    // 아래 `${base}/host/...` 결합 시 `//host/` 더블 슬래시가 생겨 일부 프록시에서\r\n    // 모바일/호스트 경로가 깨진다. `/relay` `/relay/` 모두 동일하게 정규화한다.\r\n    const relayBase = rawBase.replace(/\\/+$/, '')\r\n\r\n    try {\r\n      void new URL(\r\n        relayBase\r\n          .replace(/^wss:\\/\\//, 'https://')\r\n          .replace(/^ws:\\/\\//, 'http://')\r\n      )\r\n    } catch {\r\n      console.error(\r\n        '[relay] Invalid RELAY_URL (cannot parse origin):',\r\n        relayBase\r\n      )\r\n      return\r\n    }\r\n\r\n    this.relayRetryCount++\r\n    this.setRelayStatus('connecting')\r\n\r\n    const hostUrl = `${relayBase}/host/${SESSION_TOKEN}`\r\n    const mobileUrl = `${relayBase}/mobile/${SESSION_TOKEN}`\r\n\r\n    log.info(\r\n      `[relay] connecting attempt=${this.relayRetryCount} urlIndex=${this.relayUrlIndex} token=${SESSION_TOKEN.slice(0, 8)}... ts=${new Date().toISOString()}`\r\n    )\r\n    // 진단: env/설정에 들어온 raw base 와 정규화 결과를 함께 남긴다.\r\n    // (더블 슬래시·잘못된 호스트가 모바일 미접속 원인인지 즉시 판별하기 위함)\r\n    // debug 레벨 — 매 재연결마다 찍히므로 프로덕션 로그 노이즈를 줄인다.\r\n    log.debug(`[relay] base raw=\"${rawBase}\" normalized=\"${relayBase}\" host=${relayBase}/host/<token> mobile=${relayBase}/mobile/<token>`)\r\n\r\n    const agentSecret = this.config.getRelayAgentSecret()\r\n\r\n    const ws = new WebSocket(hostUrl, {\r\n      headers: { Authorization: `Bearer ${agentSecret}` },\r\n    })\r\n    let hadError = false\r\n\r\n    ws.on('open', () => {\r\n      this.relayWs = ws\r\n      this.relayQrUrl = mobileUrl\r\n      this.relayRetryCount = 0\r\n      this.relayUrlIndex = 0 // 성공 시 index 초기화\r\n      // flap 감지: open 시각 기록 + 안정화 타이머 무장. RELAY_STABLE_MS 동안 끊기지 않으면\r\n      // 정상 연결로 보고 relay_flapping 경고를 해제한다(중복 host 종료 후 자동 복구).\r\n      this.relayConnectedAt = Date.now()\r\n      if (this.relayStableTimer) clearTimeout(this.relayStableTimer)\r\n      this.relayStableTimer = setTimeout(() => {\r\n        this.relayFlapTimes = []\r\n        this.setHostConflict(null)\r\n      }, RELAY_STABLE_MS)\r\n      // [/review fix] relay ws 에도 clientMeta 부여 — terminal_authority_changed 의\r\n      // perRecipient 콜백이 clientId 비교를 위해 사용. 본 PR (Phase 1) 단일 권위 모바일\r\n      // 시나리오 한정: relay 는 multi-mobile fan-out path 라 권위 모바일이 relay 뒤\r\n      // 여러 명 중 하나면 isAuthority 가 모두 동일하게 적용되는 한계 존재. 정확한 per-mobile\r\n      // 분리는 protocol 차원 변경 필요 (relay 가 mobile-id 헤더 forward 등) — follow-up.\r\n      this.clientMeta.set(ws, {\r\n        clientId: `relay-mob-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\r\n      })\r\n      this.setRelayStatus('connected')\r\n      this.broadcast({\r\n        type: 'qr_code',\r\n        // getMobileQrInfo 와 동일 — 표시 url 은 HTTPS 딥링크, relayQrUrl 은 원시 ws 유지.\r\n        url: toPairUrl(mobileUrl),\r\n        mode: 'relay',\r\n        details: {\r\n          relayToken: SESSION_TOKEN.slice(0, 8),\r\n          localPort: this.wsPort,\r\n        },\r\n      })\r\n      this.startHeartbeat()\r\n    })\r\n\r\n    ws.on('pong', () => {\r\n      if (this.pongTimer) {\r\n        clearTimeout(this.pongTimer)\r\n        this.pongTimer = null\r\n      }\r\n    })\r\n\r\n    ws.on('message', raw => {\r\n      // v3 binary protocol — text frame 거부.\r\n      if (typeof raw === 'string') {\r\n        log.warn('[relay] text frame received — v3 violation, ignoring')\r\n        return\r\n      }\r\n      const buffer = Buffer.isBuffer(raw)\r\n        ? raw\r\n        : Array.isArray(raw)\r\n          ? Buffer.concat(raw as unknown as readonly Uint8Array[])\r\n          : Buffer.from(raw as ArrayBuffer)\r\n\r\n      let parsed: ReturnType<typeof parseFrame>\r\n      try {\r\n        parsed = parseFrame(buffer)\r\n      } catch (err) {\r\n        log.warn(`[relay] frame parse failed: ${(err as Error).message}`)\r\n        return\r\n      }\r\n\r\n      if (parsed.type === 'resize') {\r\n        const machineId =\r\n          (this.relayWs &&\r\n            this.clientMeta.get(this.relayWs)?.attachedMachineId) ??\r\n          this.lastAttachedMachineId ??\r\n          undefined\r\n        if (machineId) {\r\n          this.ssh.resize(machineId, parsed.cols, parsed.rows, 'mobile')\r\n          // [resize-resettle 20260610] settle 후 올바른-size 스냅샷 재방출 (조각 race 차단).\r\n          this.scheduleResizeFrameSnapshot(machineId)\r\n        } else {\r\n          log.warn(\r\n            '[relay] RESIZE opcode 도착했으나 attachedMachineId 없음 (relay path)'\r\n          )\r\n        }\r\n        return\r\n      }\r\n\r\n      if (parsed.type !== 'app_json') return\r\n\r\n      try {\r\n        const msg = parsed.json as ClientMessage\r\n        // hello — relay path 의 첫 frame. desktop 이 hello_ack 응답 후 ssh 흐름.\r\n        if ((msg as any).type === 'hello') {\r\n          // 진단: 이 로그가 찍히면 모바일이 relay 의 /mobile/<token> 까지 도달해\r\n          // host 소켓으로 forward 된 것 — 미접속 원인이 relay 이전(모바일 URL/토큰)인지\r\n          // 이후(데스크탑 처리)인지 가르는 분기점. debug 레벨 — 연결마다 찍히므로 노이즈 절감.\r\n          log.debug('[relay] hello received (mobile reached relay → forwarded to host) — sending hello_ack')\r\n          this.relayWs?.send(encodeHelloAck())\r\n          return\r\n        }\r\n        // session_attach 시 clientMeta.attachedMachineId 갱신 (RESIZE opcode 의 machineId 추적용).\r\n        // [/review fix Q2-complete] (relayWs as any) 패턴 제거 — clientMeta 단일화.\r\n        if (\r\n          msg.type === 'session_attach' &&\r\n          typeof (msg as any).machineId === 'string'\r\n        ) {\r\n          const mid = (msg as any).machineId as string\r\n          if (this.relayWs) {\r\n            const existing = this.clientMeta.get(this.relayWs)\r\n            if (existing) existing.attachedMachineId = mid\r\n          }\r\n          // A3 (Sub-Part 1F-ii-a bonus): mobile path 와 동기화.\r\n          if (\r\n            this.relayWs &&\r\n            this.mobileClientCaps.get(this.relayWs) === 'grid.diff'\r\n          ) {\r\n            log.info(\r\n              `[relay] session_attach + cap=grid.diff → auto GRID_SNAPSHOT machineId=${mid}`\r\n            )\r\n            this.ssh.lifecycle.onAttachGridDiff(mid, frame =>\r\n              this.relayWs?.send(frame)\r\n            )\r\n          }\r\n        }\r\n        // A3 (relay path): capability nego + snapshot.req. mobile path 와 동일 로직.\r\n        // Sub-Part 1F-ii-c: 'grid.diff' 활성화 — mobile path 와 동기화.\r\n        if ((msg as any).type === 'cap.req' && this.relayWs) {\r\n          const clientCaps = ((msg as any).client_caps as string[]) ?? []\r\n          const selected: 'pty.data' | 'grid.diff' = clientCaps.includes(\r\n            'grid.diff'\r\n          )\r\n            ? 'grid.diff'\r\n            : 'pty.data'\r\n          this.mobileClientCaps.set(this.relayWs, selected)\r\n          this.relayWs.send(\r\n            encodeAppJson(\r\n              buildCapAck(selected, this.config.getInstallId(), this.identity.version)\r\n            )\r\n          )\r\n          return\r\n        }\r\n        if ((msg as any).type === 'snapshot.req' && this.relayWs) {\r\n          if (this.snapshotReqThrottle(this.relayWs)) {\r\n            log.warn('[relay] snapshot.req throttled (3/min/session cap)')\r\n            return\r\n          }\r\n          const machineId = this.clientMeta.get(this.relayWs)\r\n            ?.attachedMachineId as string | undefined\r\n          if (!machineId) return\r\n          const reason = (msg as any).reason ?? 'manual'\r\n          log.info(\r\n            `[relay] snapshot.req machineId=${machineId} reason=${reason} — sending GRID_SNAPSHOT`\r\n          )\r\n          // relayWs.send 는 binary frame 직접 — relay 가 forward 해 mobile 도달.\r\n          this.ssh.lifecycle.onSnapshotReq(machineId, frame =>\r\n            this.relayWs?.send(frame)\r\n          )\r\n          return\r\n        }\r\n        // [Fix 2026-05-26] relay path 도 source 로 relayWs 전달.\r\n        // 누락 시 handleSessionAttach 의 `if (source) { onMobileAttach(...) }` 분기가\r\n        // skip 되어 viewer_mode 가 'attaching' 에서 'handed_off' 로 전이 안 됨 →\r\n        // desktop UI 가 \"폰 연결중\" placeholder 에 영구 stuck.\r\n        // relayWs 는 line ~1009 에서 clientMeta.set 으로 clientId='relay-mob-...' 보유.\r\n        this.handleMessage(msg, this.relayWs)\r\n      } catch {\r\n        log.warn('[relay] Invalid app message from mobile')\r\n      }\r\n    })\r\n\r\n    ws.on('close', (code, reasonBuf) => {\r\n      const reason = reasonBuf?.toString() || '(none)'\r\n      log.info(\r\n        `[relay] close code=${code} reason=${reason} ts=${new Date().toISOString()}`\r\n      )\r\n      this.relayWs = null\r\n      this.relayQrUrl = null\r\n      this.stopHeartbeat()\r\n      // flap 감지: 안정화 타이머 해제 + 이번 연결 수명 집계(짧으면 flap → 임계 시 경고 on).\r\n      if (this.relayStableTimer) {\r\n        clearTimeout(this.relayStableTimer)\r\n        this.relayStableTimer = null\r\n      }\r\n      this.recordRelayFlap()\r\n      this.setRelayStatus('disconnected')\r\n\r\n      if (hadError && relayUrls.length > 1) {\r\n        // 연결 오류 발생 시 다음 URL로 즉시 전환\r\n        const nextIndex = (this.relayUrlIndex + 1) % relayUrls.length\r\n        const isFullCycle = nextIndex === 0\r\n        this.relayUrlIndex = nextIndex\r\n        log.info(\r\n          `[relay] failover → urlIndex=${nextIndex} (${relayUrls[nextIndex]}) ts=${new Date().toISOString()}`\r\n        )\r\n        if (!this.destroyed) {\r\n          // 한 바퀴 다 돌았으면 지수 백오프 적용, 아니면 즉시 재시도\r\n          const delay = isFullCycle\r\n            ? Math.min(5_000 * 2 ** Math.min(this.relayRetryCount, 3), 60_000)\r\n            : 0\r\n          setTimeout(() => this.connectToRelay(), delay)\r\n        }\r\n        return\r\n      }\r\n\r\n      const delay = Math.min(\r\n        5_000 * 2 ** Math.min(this.relayRetryCount, 3),\r\n        60_000\r\n      )\r\n      log.info(\r\n        `[relay] disconnected — reconnecting in ${delay / 1000}s (attempt ${this.relayRetryCount + 1}) ts=${new Date().toISOString()}`\r\n      )\r\n      if (!this.destroyed) setTimeout(() => this.connectToRelay(), delay)\r\n    })\r\n\r\n    ws.on('error', e => {\r\n      hadError = true\r\n      log.info(\r\n        `[relay] error message=\"${e.message}\" ts=${new Date().toISOString()}`\r\n      )\r\n    })\r\n\r\n    ws.on('unexpected-response', (_req, res) => {\r\n      log.info(\r\n        `[relay] unexpected-response status=${res.statusCode} statusMessage=${res.statusMessage} ts=${new Date().toISOString()}`\r\n      )\r\n    })\r\n  }\r\n\r\n  // ── Auto-update graceful shutdown (autoplan E1) ───────────────────────────\r\n\r\n  /**\r\n   * 현재 attach 된 mobile client 의 수 (LAN + relay 합산).\r\n   *\r\n   * 업데이트 install gate 가 사용자 confirmation 필요 여부를 판단하는데 사용 —\r\n   * count > 0 이면 무경고 quitAndInstall 가 mobile attached PTY 를 무경고로 죽임.\r\n   *\r\n   * Relay 경로는 단일 WebSocket 이지만 그 뒤에 다중 모바일이 있을 수 있어 정확한 수는\r\n   * 알 수 없다. 본 카운트는 \"active 한 mobile transport 가 존재하는지\" 의 보수적\r\n   * 신호 — 1 이상이면 confirmation 띄움, 0 이면 silent install.\r\n   */\r\n  getActiveMobileClientCount(): number {\r\n    let n = this.mobileClients.size\r\n    if (this.relayWs?.readyState === WebSocket.OPEN) n += 1\r\n    return n\r\n  }\r\n\r\n  /**\r\n   * Auto-update install 직전 graceful shutdown (autoplan E1).\r\n   *\r\n   * 1. 모든 mobile client (LAN + relay) 에 `app:update-installing` broadcast — 모바일은\r\n   *    \"데스크탑 업데이트 중\" 토스트 표시 후 reconnect retry 준비.\r\n   * 2. `timeoutMs` 동안 대기 — 모바일이 자체 detach + scrollback 보존.\r\n   * 3. 시간 만료 후 caller (main/index.ts before-quit) 가 `cleanup()` 호출 → 모든 WS close.\r\n   *\r\n   * 본 함수는 cleanup 자체를 하지 않는다 — caller 가 quitAndInstall 흐름에서 그대로 진행.\r\n   * 본 함수는 idempotent — 이미 destroyed 면 즉시 resolve.\r\n   */\r\n  async gracefulShutdown(timeoutMs: number): Promise<void> {\r\n    if (this.destroyed) return\r\n    // 빌드타임 글로벌(__APP_VERSION__) 대신 주입된 identity 사용 — 코어 host 비의존(A4).\r\n    const version = this.identity.version\r\n    log.info(\r\n      `[ws-manager] gracefulShutdown begin graceMs=${timeoutMs} ` +\r\n        `mobileClients=${this.mobileClients.size} relay=${this.relayWs?.readyState === WebSocket.OPEN}`\r\n    )\r\n    try {\r\n      this.broadcast({\r\n        type: 'app:update-installing',\r\n        graceMs: timeoutMs,\r\n        version,\r\n      })\r\n    } catch (err) {\r\n      log.warn('[ws-manager] gracefulShutdown broadcast failed', err)\r\n    }\r\n    await new Promise<void>(resolve => setTimeout(resolve, timeoutMs))\r\n    log.info(\r\n      '[ws-manager] gracefulShutdown grace expired — caller should cleanup'\r\n    )\r\n  }\r\n\r\n  // ── Cleanup ───────────────────────────────────────────────────────────────\r\n\r\n  cleanup(): void {\r\n    this.destroyed = true\r\n    // [Phase C3] 대기 중인 takeFromTerminal open 타이머 정리(종료 후 openSession 방지).\r\n    for (const timer of this.pendingHandoffOpens.values()) clearTimeout(timer)\r\n    this.pendingHandoffOpens.clear()\r\n    this.agentRunner.cleanup()\r\n    this.stopHeartbeat()\r\n    if (this.relayStableTimer) {\r\n      clearTimeout(this.relayStableTimer)\r\n      this.relayStableTimer = null\r\n    }\r\n    this.relayWs?.terminate()\r\n    this.relayWs = null\r\n    for (const ws of this.mobileClients) {\r\n      ws.close()\r\n    }\r\n    this.mobileClients.clear()\r\n    // [A2 spike] machineId → WS lookup map 도 정리.\r\n    this.mobileClientByMachine.clear()\r\n    this.wss?.close()\r\n    this.wss = null\r\n  }\r\n}\r\n","/**\n * 릴레이/WS 에이전트 설정의 공통 기본값 + URL 해석 로직 (단일 출처).\n *\n * 데스크탑(electron-store + `import.meta.env` 빌드주입)·헤드리스(JSON + `process.env` 런타임)\n * 두 {@link AgentConfigStore} 구현이 **같은 기본값·같은 URL 해석**을 쓰도록 모은다.\n *\n * **순수성 (WHY)**: env 접근 방식이 패키징 환경마다 다르므로(데스크탑=빌드 define, 헤드리스=런타임\n * process.env), 본 모듈은 `process.env`/`import.meta.env` 를 **직접 참조하지 않고** 이미 읽은 값을\n * 인자로 받는다. 그래야 한 함수가 양쪽에서 동일 동작하고, 패키징 환경차·no-electron 가드에 안 걸린다.\n */\n\n/** 릴레이 기본 엔드포인트 — stored 설정도 env 도 없을 때의 최종 fallback. */\nexport const RELAY_URL_DEFAULT = 'wss://juny-api.kr/relay'\n\n/**\n * QR 페어링 딥링크(Universal/App Link)의 base origin — **릴레이 ws 호스트와 의도적으로 분리**한다.\n *\n * 릴레이 ws 는 `wss://juny-api.kr/relay/...`(별도 `apps/relay`)지만, 딥링크는 ① 게시된 앱의\n * associated-domains(iOS)·App Link autoVerify(Android) 와 ② `apple-app-site-association`·\n * `assetlinks.json`·`/pair` 리다이렉트를 **서빙하는 도메인**에 매여야 한다. 그 도메인이\n * `arv.juny-api.kr`(apps/arv) 이므로 여기에 고정한다. 실제 ws url 은 `?u=` 로 링크 안에 실어\n * 옮기므로 두 도메인이 달라도 무방하다(자기 기술적). 게시 앱과 묶인 값이라 사실상 비-가변.\n */\nexport const PAIR_LINK_BASE_DEFAULT = 'https://arv.juny-api.kr'\n\n/** 로컬 WS(모바일 LAN 직결) 서버 기본 포트. */\nexport const WS_PORT_DEFAULT = 3001\n/** relay ping 주기(ms). */\nexport const HEARTBEAT_INTERVAL_MS_DEFAULT = 30_000\n/** relay pong 미수신 타임아웃(ms) — 초과 시 좀비 연결로 보고 terminate. */\nexport const PONG_TIMEOUT_MS_DEFAULT = 10_000\n/** LAN 모바일 auth 핸드셰이크 타임아웃(ms). */\nexport const AUTH_TIMEOUT_MS_DEFAULT = 5_000\n\n/**\n * 릴레이 URL 목록 해석. 우선순위: **stored(명시 설정) > envUrl(콤마=다중 fallback) > 기본값**.\n *\n * @param stored ConfigStore 에 영속된 명시 URL 목록(있으면 최우선).\n * @param envUrl 호출자가 읽은 env 값 — 데스크탑 `import.meta.env.MAIN_VITE_RELAY_URL`,\n *               헤드리스 `process.env.MAIN_VITE_RELAY_URL ?? VIBE_RELAY_URL`. 콤마로 여러 개 가능.\n */\nexport function resolveRelayUrls(\n  stored: string[] | undefined,\n  envUrl: string | undefined\n): string[] {\n  if (stored && stored.length > 0) return stored\n  if (envUrl) {\n    const urls = envUrl\n      .split(',')\n      .map((u) => u.trim())\n      .filter(Boolean)\n    if (urls.length > 0) return urls\n  }\n  return [RELAY_URL_DEFAULT]\n}\n","import { PAIR_LINK_BASE_DEFAULT } from '@arva/shared/relay-config'\n\n/**\n * relay 모바일 ws URL 을 폰 기본 카메라로도 동작하는 HTTPS 페어링 딥링크로 감싼다.\n *\n * 폰 카메라가 `wss://` QR 을 찍으면 아무 동작도 못 하고, 앱 미설치 시 스토어로 유도할 방법도 없다.\n * Universal Link(iOS)/App Link(Android) 로 가로채이려면 **HTTPS** 여야 하므로 ws URL 을\n * `${pairBase}/pair?u=<원시 ws url>` 로 감싼다 — 앱 설치 시 OS 가 가로채 앱을 실행하고, 미설치 시\n * 브라우저가 `/pair` 페이지를 열어 App Store/Play Store 로 리다이렉트한다.\n *\n * **pairBase 는 릴레이 ws 호스트가 아니다**: ws 는 `wss://juny-api.kr/relay/...` 지만 딥링크는\n * association 파일·`/pair` 를 서빙하고 게시 앱의 associated-domains 와 묶인 도메인\n * ({@link PAIR_LINK_BASE_DEFAULT} = `arv.juny-api.kr`)이어야 한다. 원시 ws url 을 `u` 쿼리에\n * 통째로 실어 ① 모바일이 검증된 기존 파서를 그대로 재사용하고 ② 두 도메인이 달라도 self-describing\n * 하게 한다. LAN(`ws://<lanIp>`)은 공개 도메인이 없어 본 함수를 호출하지 않는다(원시 ws QR 유지).\n *\n * @param wsMobileUrl `wss://<host>/.../mobile/<uuid>` (relay 모드 전용).\n * @param pairBase 딥링크 origin. 기본 {@link PAIR_LINK_BASE_DEFAULT}. 테스트/스테이징용 주입 가능.\n * @returns `${pairBase}/pair?u=<encoded wsMobileUrl>`.\n */\nexport function toPairUrl(\n  wsMobileUrl: string,\n  pairBase: string = PAIR_LINK_BASE_DEFAULT\n): string {\n  const base = pairBase.replace(/\\/+$/, '')\n  return `${base}/pair?u=${encodeURIComponent(wsMobileUrl)}`\n}\n","import { spawn, type ChildProcess } from 'child_process'\n\nimport { createLogger } from '../lib/logger'\n\nimport type { CockpitAgentKind, CockpitCard } from '@arva/shared/types'\n\nimport { CockpitStreamParser } from './cockpit-parser'\n\nconst log = createLogger('agent-runner')\n\n/**\n * [Cockpit] `agent.run` 으로 요청된 별도 프로세스 1건의 핸들.\n *\n * interactive PTY 와 무관한 `child_process` — `claude -p ... --output-format\n * stream-json` 같은 비대화형 one-shot 실행에 사용한다. PTY 가 아니므로 TUI / width\n * 협상 / alt-screen 충돌이 없고, stdout 은 줄 단위 stream-json 만 흐른다.\n */\ninterface RunningAgentProcess {\n  /** 모바일이 출력을 라우팅하는 키 (`agent.output` 의 requestId). */\n  requestId: string\n  /** 이 프로세스가 속한 머신. */\n  machineId: string\n  /** 실제 child process 핸들. */\n  proc: ChildProcess\n  /**\n   * [Cockpit source of truth] 이 프로세스의 stream-json 파서. stdout 을 feed 하여 구조화\n   * 카드를 emit 한다. cockpit 콜백 미주입 시(테스트 등) undefined — raw `agent.output` 만 흐른다.\n   */\n  parser?: CockpitStreamParser\n}\n\n/**\n * [Cockpit v2 장수명] `agent.session.open` 으로 연 양방향 stream-json 세션 1건의 핸들.\n *\n * one-shot [RunningAgentProcess] 와 달리 턴 사이에도 프로세스가 살아있어, stdin 으로 user\n * 메시지를 계속 push 한다(`sendToSession`). `sessionId` 는 cockpit.card 라우팅 키로 쓰며\n * (one-shot 의 requestId 자리), 터미널 측과 공유되는 claude 세션 id(`--session-id`)와 동일.\n */\ninterface CockpitSessionProcess {\n  /** cockpit.card 라우팅 키 겸 claude `--session-id`. */\n  sessionId: string\n  /** 이 세션이 속한 머신. */\n  machineId: string\n  /** 살아있는 child process (stdin 유지). */\n  proc: ChildProcess\n  /** stream-json 파서. stdout 을 feed 해 카드 emit. cockpit 콜백 미주입 시 undefined. */\n  parser?: CockpitStreamParser\n}\n\n/**\n * [Cockpit source of truth] 파싱한 카드/세션을 broadcast 로 보내는 콜백 묶음.\n *\n * ws-manager 가 inject — `broadcast({ type:'cockpit.card'|'cockpit.session_started', ... })`\n * 로 위임한다(renderer + mobile 양쪽 fan-out). raw `agent.output`(mobile 전용) 과 **병행**.\n */\nexport interface CockpitRunnerEmit {\n  onCard: (\n    machineId: string,\n    requestId: string,\n    agent: CockpitAgentKind,\n    card: CockpitCard\n  ) => void\n  onSessionId: (machineId: string, requestId: string, sessionId: string) => void\n}\n\n/**\n * [Cockpit] `agent.run` / `agent.kill` 실행기.\n *\n * SSHManager 와 분리한 이유: interactive PTY(localSessions/sessions) 와 lifecycle 이\n * 완전히 다르고(요청-응답 1회성, PTY 아님), CLAUDE.md 10-메서드 규칙상 ssh-manager 에\n * 더 얹지 않는다. ws-manager 가 `agent.run`/`agent.kill` 핸들러에서 본 클래스에 위임한다.\n *\n * **보안**: argv 는 모바일 어댑터가 stream-json flag 를 붙여 보낸 배열이지만, 데스크탑은\n * 신뢰 경계로서 [validateAgentArgv] 로 1차 검사한다(argv[0] allowlist(claude/codex/gemini)\n * + 원소 개수·총 길이 상한, mobile 측 P3a 가드와 이중 방어). 검사 통과 후 `shell:false` 로\n * 배열 인자 spawn 한다 — 셸을 안 거치므로 prompt 의 공백/따옴표가 인자 경계를 깨지 않는다\n * (인젝션 표면 최소화).\n */\nexport class AgentRunner {\n  /** requestId → 실행 중 프로세스. exit/kill 시 제거. */\n  private processes = new Map<string, RunningAgentProcess>()\n\n  /** [Cockpit v2 장수명] sessionId → 열린 양방향 세션. close/exit 시 제거. */\n  private sessions = new Map<string, CockpitSessionProcess>()\n\n  /**\n   * stdout/stderr/exit 을 모바일로 보내는 콜백. ws-manager 가 생성자에서 inject —\n   * `broadcast({ type: 'agent.output', ... })` 로 위임한다.\n   */\n  private emit: (\n    machineId: string,\n    requestId: string,\n    stream: 'stdout' | 'stderr' | 'exit',\n    payload: { data?: string; exitCode?: number }\n  ) => void\n\n  /**\n   * [Cockpit source of truth] 파싱 카드/세션 emit. 미주입 시(테스트 등) stdout 을 파싱하지\n   * 않고 raw `agent.output` 만 흐른다 — 기존 동작 보존.\n   */\n  private cockpit?: CockpitRunnerEmit\n\n  constructor(\n    emit: (\n      machineId: string,\n      requestId: string,\n      stream: 'stdout' | 'stderr' | 'exit',\n      payload: { data?: string; exitCode?: number }\n    ) => void,\n    cockpit?: CockpitRunnerEmit\n  ) {\n    this.emit = emit\n    this.cockpit = cockpit\n  }\n\n  /**\n   * 별도 프로세스 실행. [validateAgentCommand] 실패 시 즉시 stderr + exit(1) emit 하고\n   * spawn 하지 않는다 — 모바일이 에러 카드를 그릴 수 있게.\n   *\n   * 같은 requestId 가 이미 실행 중이면 무시(중복 송신 방어). 동일 머신에 다른 requestId\n   * 의 동시 실행은 허용 — 호출자(모바일)가 1회성으로 쓰는 게 일반적이지만 강제하지 않음.\n   */\n  run(\n    machineId: string,\n    requestId: string,\n    argv: string[],\n    cwd?: string\n  ): void {\n    if (this.processes.has(requestId)) {\n      log.warn(`[agent-runner] duplicate run ignored requestId=${requestId}`)\n      return\n    }\n\n    // stdin='ignore' — `claude -p` 는 prompt 를 argv 로 받으므로 stdin 불필요. 기본 'pipe'\n    // 로 두면 claude 가 stdin 입력을 3초 대기(\"no stdin data received in 3s\") 후 진행한다.\n    const sp = spawnAgentProcess(argv, cwd, 'ignore')\n    if (!sp.ok) {\n      log.warn(\n        `[agent-runner] ${sp.stage} fail machine=${machineId} reason=${sp.reason}`\n      )\n      this.emit(machineId, requestId, 'stderr', {\n        data:\n          sp.stage === 'validate'\n            ? `agent.run rejected: ${sp.reason}\\n`\n            : `spawn failed: ${sp.reason}\\n`,\n      })\n      this.emit(machineId, requestId, 'exit', { exitCode: 1 })\n      return\n    }\n    const proc = sp.proc\n\n    log.info(\n      `[agent-runner] run machine=${machineId} requestId=${requestId} cwd=${cwd ?? '(default)'} argv=${JSON.stringify(argv)}`\n    )\n\n    const entry: RunningAgentProcess = { requestId, machineId, proc }\n    // [Cockpit source of truth] stream-json 파서 부착 — stdout 을 구조화 카드로 변환해\n    // cockpit.card broadcast. cockpit 콜백 미주입 시 파서 없음(raw agent.output 만).\n    if (this.cockpit) {\n      const cockpit = this.cockpit\n      entry.parser = new CockpitStreamParser({\n        onCard: card => cockpit.onCard(machineId, requestId, 'claude', card),\n        onSessionId: sid => cockpit.onSessionId(machineId, requestId, sid),\n        onParseFail: (line, err) =>\n          log.warn(\n            `[cockpit] parse fail req=${requestId}: ${(err as Error).message} line=${line.slice(0, 120)}`\n          ),\n      })\n    }\n    this.processes.set(requestId, entry)\n\n    proc.stdout?.setEncoding('utf-8')\n    proc.stdout?.on('data', (chunk: string) => {\n      this.emit(machineId, requestId, 'stdout', { data: chunk })\n      // raw 송신과 병행 — 파서가 카드를 emit (cockpit.card broadcast).\n      entry.parser?.feed(chunk)\n    })\n    proc.stderr?.setEncoding('utf-8')\n    proc.stderr?.on('data', (chunk: string) => {\n      this.emit(machineId, requestId, 'stderr', { data: chunk })\n    })\n    proc.on('error', (err: Error) => {\n      this.emit(machineId, requestId, 'stderr', {\n        data: `process error: ${err.message}\\n`,\n      })\n      // spawn 실패(ENOENT 등)는 'exit' 이 뒤따라오지 않는다 — Node 는 'error' 만 emit.\n      // 모바일은 'exit' frame 에서만 busy 를 해제하므로, 여기서 exit 을 보장하지 않으면\n      // macOS(직접 spawn) 에서 스피너가 영구 hang 한다. Windows(cmd.exe /c)는 항상\n      // 프로세스가 떠 'exit' 이 와서 이 분기를 안 타므로, 같은 오류에 플랫폼 동작이 갈렸다.\n      // 드물게 'exit' 가 뒤따를 수 있어 processes 존재 여부로 중복 emit 을 가드한다.\n      if (this.processes.has(requestId)) {\n        this.processes.delete(requestId)\n        this.emit(machineId, requestId, 'exit', { exitCode: 1 })\n      }\n    })\n    proc.on('exit', (code: number | null) => {\n      // 'error' 경로가 먼저 종료 처리했으면 중복 emit 방지.\n      if (!this.processes.has(requestId)) return\n      this.processes.delete(requestId)\n      // [Cockpit] buffer 잔여 + 진행 중 assistant text final flush (마지막 카드 누락 방지).\n      entry.parser?.close()\n      log.info(\n        `[agent-runner] exit machine=${machineId} requestId=${requestId} code=${code ?? -1}`\n      )\n      this.emit(machineId, requestId, 'exit', { exitCode: code ?? -1 })\n    })\n  }\n\n  /**\n   * 실행 중 프로세스 종료. requestId 지정 시 그것만, 미지정 시 해당 머신의 모든\n   * agent.run 프로세스를 SIGTERM. (interactive PTY agent 종료는 ssh-manager 책임 —\n   * 본 클래스는 자기가 spawn 한 것만 안다.)\n   */\n  kill(machineId: string, requestId?: string): void {\n    if (requestId) {\n      const entry = this.processes.get(requestId)\n      if (entry) {\n        log.info(`[agent-runner] kill requestId=${requestId}`)\n        entry.proc.kill('SIGTERM')\n      }\n      return\n    }\n    for (const entry of this.processes.values()) {\n      if (entry.machineId === machineId) {\n        log.info(\n          `[agent-runner] kill (by machine) requestId=${entry.requestId}`\n        )\n        entry.proc.kill('SIGTERM')\n      }\n    }\n  }\n\n  /**\n   * [Cockpit v2 장수명] stream-json 양방향 세션을 연다.\n   *\n   * one-shot [run] 과 달리 프로세스가 턴 사이에도 살아있어, stdin 으로 user 메시지를 계속\n   * push(`sendToSession`)하고 stdout 의 stream-json 은 파서가 카드로 변환한다. cockpit.card\n   * 라우팅 키 자리(requestId)에 sessionId 를 그대로 써 기존 broadcast 경로를 재사용한다.\n   *\n   * `resume` (Phase C): true 면 `--resume <sessionId>` 로 **기존 세션을 이어받는다**(터미널이\n   * 만든 세션을 콕핏이 인계 — 같은 cwd=같은 풀). false 면 `--session-id <sessionId>` 로 새 세션을\n   * 그 id 로 생성. 둘 다 같은 jsonl 풀이라 추후 터미널이 `claude --resume <id>` 로 도로 이어받을 수 있다.\n   *\n   * 부수효과: stdout → cockpit.onCard broadcast(renderer+mobile). 종료/실패 시 sessions 에서\n   * 제거 + 'exit' emit(렌더러가 세션 종료 인지). raw stdout 은 mobile 로 fan-out 하지 않는다 —\n   * 장수명은 현재 데스크탑 전용(back-compat).\n   */\n  openSession(\n    machineId: string,\n    sessionId: string,\n    cwd?: string,\n    resume?: boolean\n  ): void {\n    if (this.sessions.has(sessionId)) {\n      log.warn(\n        `[agent-runner] duplicate openSession ignored sessionId=${sessionId}`\n      )\n      return\n    }\n    // 장수명 claude: prompt 는 stdin(stream-json user 봉투)으로 받으므로 positional 없음.\n    // resume → 기존 세션 이어받기, 아니면 그 id 로 새 세션(둘 다 같은 cwd 풀, 터미널 공유 — Phase C).\n    // --verbose 는 -p stream-json 에서 중간 이벤트(stream_event/assistant/result) 방출에 필수 —\n    // 없으면 최종 result 만 와 스트리밍이 죽는다. --include-partial-messages 로 text_delta 까지.\n    // user_input echo 는 렌더러가 직접 카드로 그린다(파서가 user 텍스트로 카드를 안 만듦) — 그래서\n    // --replay-user-messages 는 콕핏에 무의미(향후 파서 확장 시 중복 위험)하므로 넣지 않는다.\n    const argv = [\n      'claude',\n      '-p',\n      '--input-format',\n      'stream-json',\n      '--output-format',\n      'stream-json',\n      '--verbose',\n      '--include-partial-messages',\n      resume ? '--resume' : '--session-id',\n      sessionId,\n    ]\n    const sp = spawnAgentProcess(argv, cwd, 'pipe')\n    if (!sp.ok) {\n      log.warn(\n        `[agent-runner] openSession ${sp.stage} fail machine=${machineId} sessionId=${sessionId} reason=${sp.reason}`\n      )\n      this.emit(machineId, sessionId, 'stderr', {\n        data: `agent.session.open failed: ${sp.reason}\\n`,\n      })\n      this.emit(machineId, sessionId, 'exit', { exitCode: 1 })\n      return\n    }\n    const proc = sp.proc\n\n    log.info(\n      `[agent-runner] openSession machine=${machineId} sessionId=${sessionId} cwd=${cwd ?? '(default)'} resume=${resume ?? false}`\n    )\n\n    const entry: CockpitSessionProcess = { sessionId, machineId, proc }\n    if (this.cockpit) {\n      const cockpit = this.cockpit\n      entry.parser = new CockpitStreamParser({\n        onCard: card => cockpit.onCard(machineId, sessionId, 'claude', card),\n        onSessionId: sid => cockpit.onSessionId(machineId, sessionId, sid),\n        onParseFail: (line, err) =>\n          log.warn(\n            `[cockpit] parse fail session=${sessionId}: ${(err as Error).message} line=${line.slice(0, 120)}`\n          ),\n      })\n    }\n    this.sessions.set(sessionId, entry)\n\n    proc.stdout?.setEncoding('utf-8')\n    proc.stdout?.on('data', (chunk: string) => {\n      // 장수명은 raw agent.output 을 fan-out 하지 않고 파서만 feed (데스크탑 전용 카드 경로).\n      entry.parser?.feed(chunk)\n    })\n    proc.stderr?.setEncoding('utf-8')\n    proc.stderr?.on('data', (chunk: string) => {\n      log.warn(\n        `[agent-runner] session stderr sessionId=${sessionId}: ${chunk.trimEnd()}`\n      )\n    })\n    proc.on('error', (err: Error) => {\n      // spawn 후 비동기 오류(예: 파이프 끊김). exit 가 안 따라올 수 있어 여기서 정리 보장.\n      log.warn(\n        `[agent-runner] session error sessionId=${sessionId}: ${err.message}`\n      )\n      if (this.sessions.has(sessionId)) {\n        this.sessions.delete(sessionId)\n        this.emit(machineId, sessionId, 'exit', { exitCode: 1 })\n      }\n    })\n    proc.on('exit', (code: number | null) => {\n      if (!this.sessions.has(sessionId)) return\n      this.sessions.delete(sessionId)\n      entry.parser?.close()\n      log.info(\n        `[agent-runner] session exit machine=${machineId} sessionId=${sessionId} code=${code ?? -1}`\n      )\n      this.emit(machineId, sessionId, 'exit', { exitCode: code ?? -1 })\n    })\n  }\n\n  /**\n   * [Cockpit v2 장수명] 열린 세션 stdin 으로 user 메시지를 push (프롬프트 / approval 응답).\n   *\n   * stream-json user 봉투로 직렬화해 한 줄(JSON + '\\n')로 write 한다. 세션이 없으면(미오픈/\n   * 종료됨) 무시 — 호출자(ws)가 open 을 보장한다. 콕핏 user_input 카드는 렌더러가 직접 그린다\n   * (파서가 user 텍스트로 카드를 만들지 않음).\n   */\n  sendToSession(sessionId: string, text: string): void {\n    const entry = this.sessions.get(sessionId)\n    if (!entry) {\n      log.warn(`[agent-runner] sendToSession: no session sessionId=${sessionId}`)\n      return\n    }\n    const envelope = JSON.stringify({\n      type: 'user',\n      message: { role: 'user', content: [{ type: 'text', text }] },\n    })\n    entry.proc.stdin?.write(`${envelope}\\n`)\n  }\n\n  /**\n   * [Cockpit v2 장수명] 세션 종료(SIGTERM). 실제 정리(sessions 제거 + 'exit' emit)는 proc\n   * 'exit' 핸들러가 수행 — [kill] 과 동일 패턴(중복 close 안전).\n   *\n   * `sessionId` 지정 시 그 세션만. 미지정 + `machineId` 지정 시 **그 머신의 콕핏 세션 전부**를\n   * 닫는다(Phase C: 콕핏이 resume 으로 연 실제 id 를 렌더러가 몰라도 머신 단위로 닫게 — 단일 세션이라\n   * 보통 1개). 둘 다 없으면 no-op.\n   */\n  closeSession(sessionId?: string, machineId?: string): void {\n    if (sessionId) {\n      const entry = this.sessions.get(sessionId)\n      if (entry) {\n        log.info(`[agent-runner] closeSession sessionId=${sessionId}`)\n        entry.proc.kill('SIGTERM')\n      }\n      return\n    }\n    if (machineId) {\n      for (const entry of this.sessions.values()) {\n        if (entry.machineId === machineId) {\n          log.info(\n            `[agent-runner] closeSession (by machine) machine=${machineId} sessionId=${entry.sessionId}`\n          )\n          entry.proc.kill('SIGTERM')\n        }\n      }\n    }\n  }\n\n  /**\n   * [Cockpit 신선도 20260614] 한 머신에서 데스크탑이 보유한 running 콕핏 세션 id 목록.\n   *\n   * 방금 [openSession] 한 장수명 세션은 `.jsonl` flush 전이라 디스크 스캔에서 누락된다.\n   * ssh-manager 가 이 in-memory 진실을 mergeRunningSessions 로 합류시켜 picker 가 \"현재\n   * 세션\"을 즉시 보이게 한다(ws-manager 가 `setActiveSessionProvider` 로 후주입).\n   */\n  getActiveSessionIds(machineId: string): string[] {\n    const ids: string[] = []\n    for (const entry of this.sessions.values()) {\n      if (entry.machineId === machineId) ids.push(entry.sessionId)\n    }\n    return ids\n  }\n\n  /** 앱 종료 시 모든 프로세스·세션 정리. */\n  cleanup(): void {\n    for (const entry of this.processes.values()) {\n      try {\n        entry.proc.kill('SIGTERM')\n      } catch {\n        // already dead\n      }\n    }\n    this.processes.clear()\n    for (const entry of this.sessions.values()) {\n      try {\n        entry.proc.kill('SIGTERM')\n      } catch {\n        // already dead\n      }\n    }\n    this.sessions.clear()\n  }\n}\n\n/**\n * [Cockpit] argv 검증 + OS 분기 + spawn 을 한 곳에 모은 헬퍼.\n *\n * one-shot([AgentRunner.run]) 과 장수명([AgentRunner.openSession]) 이 공유한다 — 둘의 유일한\n * 차이는 stdin 모드뿐이다(one-shot 은 prompt 를 argv 로 받아 'ignore', 장수명은 stdin 으로\n * user 메시지를 push 하므로 'pipe'). 검증/spawn 실패는 throw 하지 않고 `{ ok:false, stage,\n * reason }` 로 돌려, 호출자가 emit 형식(stderr 메시지·종료 코드)을 자기 맥락에 맞게 정한다.\n *\n * - Unix: argv 그대로 spawn(shell:false). PATH 가 claude 를 찾는다.\n * - Windows: claude 는 보통 `claude.cmd`(npm shim) 라 shell:false 직접 spawn 시 ENOENT/EINVAL.\n *   그래서 `cmd.exe /d /s /c claude ...` 로 감싸되 **인자는 배열로** 넘긴다 — Node 가 각 인자를\n *   Windows 규칙으로 자동 escape(공백/한글은 \"...\" 로 감쌈)하므로, 모바일이 보낸 작은따옴표가\n *   prompt 를 자르던 문제가 사라진다(shell:true 문자열 방식과 달리 따옴표 파싱을 우리가 안 함).\n * - env.PATH 는 [augmentedPath] 로 보강(macOS GUI 앱은 login shell PATH 미상속 → claude ENOENT).\n */\nfunction spawnAgentProcess(\n  argv: string[],\n  cwd: string | undefined,\n  stdin: 'ignore' | 'pipe'\n):\n  | { ok: true; proc: ChildProcess }\n  | { ok: false; stage: 'validate' | 'spawn'; reason: string } {\n  const validation = validateAgentArgv(argv)\n  if (!validation.ok) {\n    return {\n      ok: false,\n      stage: 'validate',\n      reason: validation.reason ?? 'invalid argv',\n    }\n  }\n  const isWin = process.platform === 'win32'\n  const command = isWin ? (process.env.COMSPEC ?? 'cmd.exe') : argv[0]\n  const args = isWin ? ['/d', '/s', '/c', ...argv] : argv.slice(1)\n  try {\n    const proc = spawn(command, args, {\n      // shell:false (기본) — 배열 인자가 곧 인자 경계. Node 가 Windows escape 적용.\n      cwd,\n      env: { ...process.env, TERM: 'dumb', PATH: augmentedPath() },\n      stdio: [stdin, 'pipe', 'pipe'],\n      windowsHide: true,\n      // cmd.exe 인자에 직접 따옴표를 넣지 않고 Node 자동 escape 에 맡김(작은따옴표 버그 방지).\n      windowsVerbatimArguments: false,\n    })\n    return { ok: true, proc }\n  } catch (err) {\n    return { ok: false, stage: 'spawn', reason: (err as Error).message }\n  }\n}\n\n/**\n * [Cockpit] macOS GUI 앱 PATH 보강.\n *\n * macOS 에서 Electron 앱은 login shell(.zshrc 등)의 PATH 를 물려받지 않아\n * Homebrew(`/opt/homebrew/bin`, `/usr/local/bin`), nvm/asdf, 사용자 로컬\n * (`~/.local/bin`) 에 설치된 claude 를 ENOENT 로 못 찾는다. 흔한 설치 경로를\n * 기존 PATH 에 append 한다 (중복은 무해 — 앞쪽 우선이라 시스템 경로 우선순위 유지).\n *\n * Windows/Linux 에서는 기존 PATH 가 보통 충분 — 추가 경로가 없거나 무해.\n */\nfunction augmentedPath(): string {\n  const sep = process.platform === 'win32' ? ';' : ':'\n  const current = process.env.PATH ?? process.env.Path ?? ''\n  if (process.platform !== 'darwin') return current\n  const home = process.env.HOME ?? ''\n  const extra = [\n    '/opt/homebrew/bin',\n    '/usr/local/bin',\n    '/usr/bin',\n    '/bin',\n    home ? `${home}/.local/bin` : '',\n    home ? `${home}/.npm-global/bin` : '',\n  ].filter(Boolean)\n  const have = new Set(current.split(sep))\n  const missing = extra.filter(p => !have.has(p))\n  return missing.length > 0 ? `${current}${sep}${missing.join(sep)}` : current\n}\n\n/**\n * [Cockpit] `agent.run` argv 1차 검증 (데스크탑 신뢰 경계).\n *\n * mobile 측이 P3a 에서 prompt newline 거부 + sessionId 정규식 검사를 이미 하지만,\n * relay 를 거치는 임의 페이로드를 데스크탑이 무조건 신뢰하면 안 된다. argv 방식은\n * 셸을 안 거쳐(shell:false) 인젝션 표면이 작지만, executable(argv[0]) allowlist 로\n * \"cockpit 이 띄울 법한 AI CLI 인가\" 를 보수적으로 검증한다.\n *\n * - argv[0] 가 알려진 agent CLI (claude/codex/gemini) 인가\n * - argv 원소 개수/총 길이 상한 (메모리/오용 방어)\n *\n * 본 검사는 \"사고 방지\" 수준 — 진짜 격리는 후속(전용 user / 컨테이너) 과제.\n */\nexport function validateAgentArgv(argv: string[]): {\n  ok: boolean\n  reason?: string\n} {\n  if (!Array.isArray(argv) || argv.length === 0) {\n    return { ok: false, reason: 'empty argv' }\n  }\n  if (argv.length > 64) {\n    return { ok: false, reason: 'too many args' }\n  }\n  const totalLen = argv.reduce((n, a) => n + (a?.length ?? 0), 0)\n  if (totalLen > 16384) {\n    return { ok: false, reason: 'argv too long' }\n  }\n  for (const a of argv) {\n    if (typeof a !== 'string') {\n      return { ok: false, reason: 'non-string arg' }\n    }\n  }\n  const ALLOWED_AGENTS = new Set(['claude', 'codex', 'gemini'])\n  if (!ALLOWED_AGENTS.has(argv[0])) {\n    return { ok: false, reason: `unrecognized executable: \"${argv[0]}\"` }\n  }\n  return { ok: true }\n}\n","/**\r\n * [Cockpit] stream-json 파서의 **무상태** 헬퍼 — 포맷팅/매칭만.\r\n *\r\n * 파서 본체(`cockpit-parser.ts`)에서 분리한 이유: stateful 이벤트 핸들러(세션/턴 상태)와\r\n * 무상태 변환을 갈라 파일당 10-함수 한도(.claude/rules)를 지키고, 헬퍼만 단독 단위 테스트한다.\r\n * 본 파일의 함수들은 모바일 `stream_json_parser.dart` 의 동명 private 헬퍼를 1:1 포팅한 것 —\r\n * 동작이 모바일과 동일해야 같은 fixture(F1-F4)로 양쪽 회귀를 가드할 수 있다.\r\n */\r\n\r\n/**\r\n * tool args(객체) → display 문자열.\r\n *\r\n * 단일 인자면 값만(`Read(\"path\")` → `path`), 다중이면 `k=v` 결합, 빈 객체면 `(no args)`.\r\n */\r\nexport function formatToolArgs(args: Record<string, unknown>): string {\r\n  const keys = Object.keys(args)\r\n  if (keys.length === 0) return '(no args)'\r\n  if (keys.length === 1) return `${args[keys[0]]}`\r\n  return keys.map(k => `${k}=${args[k]}`).join(', ')\r\n}\r\n\r\n/**\r\n * tool_result 의 `content`(문자열 또는 블록 배열)를 평탄화한 텍스트.\r\n *\r\n * 배열이면 `{type:'text', text}` 블록은 text 만, 그 외 블록은 JSON 직렬화로 합친다.\r\n * null/그 외는 빈 문자열 또는 `String(...)`.\r\n */\r\nexport function extractToolResultOutput(content: unknown): string {\r\n  if (content == null) return ''\r\n  if (typeof content === 'string') return content\r\n  if (Array.isArray(content)) {\r\n    const parts: string[] = []\r\n    for (const item of content) {\r\n      if (item && typeof item === 'object') {\r\n        const t = (item as { type?: unknown }).type\r\n        if (t === 'text') {\r\n          const text = (item as { text?: unknown }).text\r\n          parts.push(typeof text === 'string' ? text : '')\r\n        } else {\r\n          parts.push(JSON.stringify(item))\r\n        }\r\n      } else {\r\n        parts.push(String(item))\r\n      }\r\n    }\r\n    return parts.join('\\n')\r\n  }\r\n  return String(content)\r\n}\r\n\r\n/**\r\n * 권한 거부 매칭 — claude-code 표준 문구(\"Claude requested permissions to use X, but you\r\n * haven't granted it yet.\") 검출.\r\n *\r\n * §8-A.0 fallback: explicit `permission_request` 이벤트가 없어, `is_error` tool_result 의 이\r\n * 문구로 `approval` 카드를 합성한다. 다국어/변형은 미지원(spike minimum) — V2.1+ 다국어 매칭.\r\n */\r\nexport function isPermissionDenial(output: string): boolean {\r\n  return (\r\n    output.includes('requested permissions to use') &&\r\n    output.includes(\"haven't granted\")\r\n  )\r\n}\r\n\r\n/**\r\n * tool_use_id → tool name. spike minimum: 매핑 미보관 — placeholder(`'tool'`/`'unknown'`).\r\n *\r\n * ToolCall 의 id 와 ToolResult 의 tool_use_id 페어링은 호출자(콕핏 provider) 책임.\r\n */\r\nexport function toolNameFromId(toolUseId: string): string {\r\n  return toolUseId.length === 0 ? 'unknown' : 'tool'\r\n}\r\n","import type { CockpitCard } from '@arva/shared/types'\r\n\r\nimport {\r\n  extractToolResultOutput,\r\n  formatToolArgs,\r\n  isPermissionDenial,\r\n  toolNameFromId,\r\n} from './cockpit-parser-utils'\r\n\r\n/**\r\n * [Cockpit] 파서 콜백 묶음. emit 봉투(`requestId`/`ts`/`agent`)는 호출자(agent-runner)가 부착.\r\n */\r\nexport interface CockpitParserCallbacks {\r\n  /** 카드 1건 emit. */\r\n  onCard: (card: CockpitCard) => void\r\n  /** `system/init` 의 session_id — `--resume` 영속 / 현재 세션 표시용. */\r\n  onSessionId?: (sessionId: string) => void\r\n  /** JSON 파싱 실패 — raw line 보존(디버그). 다음 라인 처리 계속(fail-safe, RC2). */\r\n  onParseFail?: (rawLine: string, error: unknown) => void\r\n  /** 미지원 type/event — skip 로그용 (production 무시 OK). */\r\n  onUnknownType?: (type: string) => void\r\n}\r\n\r\n/**\r\n * [Cockpit V2 — desktop source of truth] claude-code `--output-format stream-json` stdout 을\r\n * sealed [CockpitCard] 로 변환하는 **stateful** 파서.\r\n *\r\n * 모바일 `stream_json_parser.dart`(StreamJsonParser)의 **충실한 1:1 포트** — 데스크탑이\r\n * source of truth 가 되며 파싱을 한 곳으로 모은다(모바일 파서는 Phase 2 에서 폐기). 동작/회귀가\r\n * 모바일과 동일해야 하므로(같은 fixture F1-F4 로 가드), 핸들러 분기·dedup 규칙·flush 타이밍을\r\n * 원본 그대로 유지한다. 무상태 포맷 헬퍼는 `cockpit-parser-utils.ts` 로 분리(10-함수 한도).\r\n *\r\n * 핵심 규칙(원본 주석 보존):\r\n * - `--include-partial-messages` 시 같은 텍스트가 (1) stream_event delta→flush, (2)\r\n *   `type:\"assistant\"` full message 두 경로로 와 **중복 카드**가 된다. `emittedAssistantTextThisTurn`\r\n *   가드로 full message 의 text item 을 skip(tool_use 는 유지). turn 종료(`result`)에서 리셋.\r\n * - 라이브 스트리밍: delta 마다 `isStreaming:true` emit → 호출자가 직전 streaming 카드를 replace\r\n *   (카드 누적 방지). `flushActiveAssistant` 가 `isStreaming:false` 로 최종 확정.\r\n * - `turn_complete` 는 `result` 이벤트에서만 emit(message_stop 아님) — \"응답 완료\" 유일 신호.\r\n * - 권한 거부(§8-A.0): explicit permission 이벤트가 없어, is_error tool_result 의 표준 문구를\r\n *   `approval`(options=[], risk=high) 로 합성.\r\n */\r\nexport class CockpitStreamParser {\r\n  private readonly cb: CockpitParserCallbacks\r\n  /** 부분 라인 버퍼 — chunk 가 newline 중간에 끊겨도 안전. */\r\n  private lineBuffer = ''\r\n  /** 진행 중 assistant text 누적 — text_delta progressive. content_block_stop 시 final. */\r\n  private activeAssistantText: string | null = null\r\n  /** 이번 turn 에서 stream_event 경로가 assistant text 를 이미 emit 했는지(중복 차단). */\r\n  private emittedAssistantTextThisTurn = false\r\n  /** 진행 중 tool_use 의 input JSON 누적 — input_json_delta progressive. */\r\n  private activeToolInputJson: string | null = null\r\n  private activeToolName: string | null = null\r\n\r\n  constructor(cb: CockpitParserCallbacks) {\r\n    this.cb = cb\r\n  }\r\n\r\n  /**\r\n   * stdout chunk 수신 — 라인 단위 buffering 후 parse. 미완 라인(newline 미종료)은 buffer 보존,\r\n   * 다음 chunk 와 이어 처리.\r\n   */\r\n  feed(chunk: string): void {\r\n    this.lineBuffer += chunk\r\n    const lines = this.lineBuffer.split('\\n')\r\n    // 마지막 element 가 unterminated line → 다시 buffer 에 보존.\r\n    this.lineBuffer = lines.pop() ?? ''\r\n    for (const line of lines) {\r\n      const trimmed = line.trim()\r\n      if (trimmed.length === 0) continue\r\n      this.parseLine(trimmed)\r\n    }\r\n  }\r\n\r\n  /** 프로세스 종료 시 — buffer 잔여 처리 + 진행 중 assistant text final flush. close 후 feed 금지. */\r\n  close(): void {\r\n    const remaining = this.lineBuffer.trim()\r\n    this.lineBuffer = ''\r\n    if (remaining.length > 0) this.parseLine(remaining)\r\n    this.flushActiveAssistant()\r\n  }\r\n\r\n  /** 단일 라인 parse — top-level type 분기. `system`(작음)은 inline 처리. */\r\n  private parseLine(line: string): void {\r\n    let raw: Record<string, unknown>\r\n    try {\r\n      raw = JSON.parse(line) as Record<string, unknown>\r\n    } catch (e) {\r\n      this.cb.onParseFail?.(line, e)\r\n      return\r\n    }\r\n    const type = typeof raw.type === 'string' ? raw.type : null\r\n    if (type === null) {\r\n      this.cb.onUnknownType?.('(missing type)')\r\n      return\r\n    }\r\n    try {\r\n      switch (type) {\r\n        case 'system': {\r\n          // system/init 에서 session_id 추출(카드 emit 없음). 그 외 subtype 은 V2.1+ carry-over.\r\n          const subtype =\r\n            typeof raw.subtype === 'string' ? raw.subtype : '(none)'\r\n          if (subtype === 'init') {\r\n            const sid =\r\n              typeof raw.session_id === 'string' ? raw.session_id : null\r\n            if (sid) this.cb.onSessionId?.(sid)\r\n          } else {\r\n            this.cb.onUnknownType?.(`system/${subtype}`)\r\n          }\r\n          break\r\n        }\r\n        case 'stream_event':\r\n          this.handleStreamEvent(raw)\r\n          break\r\n        case 'assistant':\r\n          this.handleAssistantFinal(raw)\r\n          break\r\n        case 'user':\r\n          this.handleUserMessage(raw)\r\n          break\r\n        case 'result':\r\n          this.handleResult(raw)\r\n          break\r\n        default:\r\n          this.cb.onUnknownType?.(type)\r\n      }\r\n    } catch (e) {\r\n      this.cb.onParseFail?.(line, e)\r\n    }\r\n  }\r\n\r\n  /** stream_event 분기 — message_stop=flush, content_block_*=[handleContentBlock] 위임. */\r\n  private handleStreamEvent(raw: Record<string, unknown>): void {\r\n    const event = raw.event\r\n    if (!event || typeof event !== 'object') return\r\n    const ev = event as Record<string, unknown>\r\n    const eventType = typeof ev.type === 'string' ? ev.type : null\r\n    switch (eventType) {\r\n      case 'message_start':\r\n      case 'message_delta':\r\n        break // usage metadata — skip (V2.1+)\r\n      case 'message_stop':\r\n        // assistant turn 종료 — 진행 중 assistant text final emit.\r\n        this.flushActiveAssistant()\r\n        break\r\n      case 'content_block_start':\r\n      case 'content_block_delta':\r\n      case 'content_block_stop':\r\n        this.handleContentBlock(eventType, ev)\r\n        break\r\n      default:\r\n        this.cb.onUnknownType?.(`stream_event/${eventType ?? '(none)'}`)\r\n    }\r\n  }\r\n\r\n  /**\r\n   * content_block start/delta/stop 통합 — 진행 중 assistant text / tool_use input 누적 + emit.\r\n   *\r\n   * (모바일은 3 메서드로 나뉘지만 본 포트는 10-함수 한도로 1 메서드 통합 — 분기 로직은 동일.)\r\n   */\r\n  private handleContentBlock(\r\n    eventType: string,\r\n    event: Record<string, unknown>\r\n  ): void {\r\n    if (eventType === 'content_block_start') {\r\n      const block = event.content_block\r\n      if (block && typeof block === 'object') {\r\n        const blockType = (block as { type?: unknown }).type\r\n        if (blockType === 'tool_use') {\r\n          // tool_use 시작 — input_json_delta 누적 준비. id 페어링은 호출자 책임.\r\n          const name = (block as { name?: unknown }).name\r\n          this.activeToolName = typeof name === 'string' ? name : null\r\n          this.activeToolInputJson = ''\r\n        } else if (blockType === 'text') {\r\n          this.activeAssistantText = '' // text block 시작 — 누적 준비.\r\n        }\r\n        // 'thinking'/'tool_reference' 은 무시.\r\n      }\r\n      return\r\n    }\r\n    if (eventType === 'content_block_delta') {\r\n      const delta = event.delta\r\n      if (!delta || typeof delta !== 'object') return\r\n      const d = delta as Record<string, unknown>\r\n      const deltaType = typeof d.type === 'string' ? d.type : null\r\n      if (deltaType === 'text_delta') {\r\n        const text = typeof d.text === 'string' ? d.text : null\r\n        if (text !== null && this.activeAssistantText !== null) {\r\n          this.activeAssistantText += text\r\n          // 라이브 스트리밍 — 매 delta 마다 누적 텍스트를 isStreaming=true 로 emit. 호출자가\r\n          // \"직전이 streaming 카드면 replace\" 하므로 카드가 누적되지 않고 한 카드가 실시간 갱신.\r\n          this.cb.onCard({\r\n            kind: 'assistant',\r\n            text: this.activeAssistantText,\r\n            isStreaming: true,\r\n          })\r\n          this.emittedAssistantTextThisTurn = true\r\n        }\r\n      } else if (deltaType === 'input_json_delta') {\r\n        const partial =\r\n          typeof d.partial_json === 'string' ? d.partial_json : null\r\n        if (partial !== null && this.activeToolInputJson !== null) {\r\n          this.activeToolInputJson += partial\r\n        }\r\n      }\r\n      // thinking_delta/signature_delta 무시.\r\n      return\r\n    }\r\n    // content_block_stop — tool_use input 완성 → tool_call emit; text block → flush.\r\n    if (this.activeToolName !== null && this.activeToolInputJson !== null) {\r\n      let args: Record<string, unknown>\r\n      try {\r\n        args =\r\n          this.activeToolInputJson.length === 0\r\n            ? {}\r\n            : (JSON.parse(this.activeToolInputJson) as Record<string, unknown>)\r\n      } catch {\r\n        args = { '(raw)': this.activeToolInputJson }\r\n      }\r\n      this.cb.onCard({\r\n        kind: 'tool_call',\r\n        toolName: this.activeToolName,\r\n        args: formatToolArgs(args),\r\n      })\r\n      this.activeToolName = null\r\n      this.activeToolInputJson = null\r\n    }\r\n    // text block 종료 — content_block_stop 에서도 1회 emit 보장(assistant full 이 별도로 안 오는 경우).\r\n    this.flushActiveAssistant()\r\n  }\r\n\r\n  /**\r\n   * assistant full message — content[] 의 각 item 처리.\r\n   *\r\n   * stream_event delta 가 이미 text 를 emit 했으면([emittedAssistantTextThisTurn]) 중복이라\r\n   * text item 을 skip. tool_use 는 stream_event 로 안 온 경우가 있어 항상 emit.\r\n   */\r\n  private handleAssistantFinal(raw: Record<string, unknown>): void {\r\n    const message = raw.message\r\n    if (!message || typeof message !== 'object') return\r\n    const content = (message as { content?: unknown }).content\r\n    if (!Array.isArray(content)) return\r\n    for (const item of content) {\r\n      if (!item || typeof item !== 'object') continue\r\n      const itemType = (item as { type?: unknown }).type\r\n      if (itemType === 'text') {\r\n        if (this.emittedAssistantTextThisTurn) continue // stream_event 가 이미 emit — 중복 skip.\r\n        const text = (item as { text?: unknown }).text\r\n        if (typeof text === 'string' && text.length > 0) {\r\n          this.cb.onCard({ kind: 'assistant', text, isStreaming: false })\r\n        }\r\n      } else if (itemType === 'tool_use') {\r\n        const name = (item as { name?: unknown }).name\r\n        const input = (item as { input?: unknown }).input\r\n        this.cb.onCard({\r\n          kind: 'tool_call',\r\n          toolName: typeof name === 'string' ? name : 'unknown',\r\n          args:\r\n            input && typeof input === 'object'\r\n              ? formatToolArgs(input as Record<string, unknown>)\r\n              : `${input}`,\r\n        })\r\n      }\r\n      // 'thinking' content 는 무시.\r\n    }\r\n  }\r\n\r\n  /**\r\n   * user message — tool_result 가 content 안에 wrap. is_error + 권한 문구 매칭 시\r\n   * `approval`(§8-A.0 fallback), 그 외 `tool_result`.\r\n   */\r\n  private handleUserMessage(raw: Record<string, unknown>): void {\r\n    const message = raw.message\r\n    if (!message || typeof message !== 'object') return\r\n    const content = (message as { content?: unknown }).content\r\n    if (!Array.isArray(content)) return\r\n    for (const item of content) {\r\n      if (!item || typeof item !== 'object') continue\r\n      if ((item as { type?: unknown }).type !== 'tool_result') continue\r\n      const tid = (item as { tool_use_id?: unknown }).tool_use_id\r\n      const toolUseId = typeof tid === 'string' ? tid : ''\r\n      const isError = (item as { is_error?: unknown }).is_error === true\r\n      const output = extractToolResultOutput(\r\n        (item as { content?: unknown }).content\r\n      )\r\n\r\n      // §8-A.0 fallback — permission 거부 매칭 → approval(자유 응답 없음, risk=high).\r\n      if (isError && isPermissionDenial(output)) {\r\n        this.cb.onCard({\r\n          kind: 'approval',\r\n          prompt: output,\r\n          options: [],\r\n          risk: 'high',\r\n          allowsFreeText: false,\r\n        })\r\n        continue\r\n      }\r\n      this.cb.onCard({\r\n        kind: 'tool_result',\r\n        toolName: toolNameFromId(toolUseId),\r\n        output,\r\n        truncated: false, // V2 spike minimum — truncation 처리 V2.1+\r\n      })\r\n    }\r\n  }\r\n\r\n  /** `result` — turn 종료. 진행 중 text 먼저 flush 해 `turn_complete` 가 마지막 카드가 되게. */\r\n  private handleResult(raw: Record<string, unknown>): void {\r\n    this.flushActiveAssistant()\r\n    this.cb.onCard({\r\n      kind: 'turn_complete',\r\n      subtype: typeof raw.subtype === 'string' ? raw.subtype : undefined,\r\n      stopReason:\r\n        typeof raw.stop_reason === 'string' ? raw.stop_reason : undefined,\r\n      numTurns: typeof raw.num_turns === 'number' ? raw.num_turns : undefined,\r\n      totalCostUsd:\r\n        typeof raw.total_cost_usd === 'number' ? raw.total_cost_usd : undefined,\r\n      isError: raw.is_error === true,\r\n    })\r\n    // turn 경계 — 다음 turn 의 dedup 플래그 리셋(멀티턴 시 새 turn text 정상 emit).\r\n    this.emittedAssistantTextThisTurn = false\r\n  }\r\n\r\n  /** 진행 중 assistant text 가 있으면 final(isStreaming=false) emit + clear. idempotent. */\r\n  private flushActiveAssistant(): void {\r\n    const text = this.activeAssistantText\r\n    if (text !== null && text.length > 0) {\r\n      this.cb.onCard({ kind: 'assistant', text, isStreaming: false })\r\n      // stream_event 경로가 이번 turn 의 text 를 emit 했음을 표시 — full message 중복 차단.\r\n      this.emittedAssistantTextThisTurn = true\r\n    }\r\n    this.activeAssistantText = null\r\n  }\r\n}\r\n","import fs from 'fs'\r\nimport path from 'path'\r\n\r\n/**\r\n * [file-upload 20260614] 모바일 첨부 업로드의 기본 크기 상한 (25MB).\r\n *\r\n * in-memory 누적(streaming 아님)이라 한 업로드가 메모리를 무한히 점유하지 못하게 한다.\r\n * begin 단계의 선언 size 와 chunk 누적 size 양쪽에 적용 — 모바일이 거짓 size 를 선언해도\r\n * 실제 바이트가 상한을 넘으면 drop(B). 25MB = 모바일 첨부(이미지/로그/작은 코드)에 충분.\r\n */\r\nexport const DEFAULT_MAX_UPLOAD_BYTES = 25 * 1024 * 1024\r\n\r\n/** Windows 에서 파일명에 못 쓰는 문자 — `_` 로 치환한다(cross-platform 안전 파일명). */\r\nconst WINDOWS_ILLEGAL_CHARS = /[<>:\"/\\\\|?*]/g\r\n\r\n/** 파일명에서 제거하는 제어문자 (NUL~US, DEL) — 로그/FS 손상 방지. */\r\n// biome-ignore lint/suspicious/noControlCharactersInRegex: 제어문자 strip 이 목적 — 의도적 매칭\r\nconst CONTROL_CHARS = /[\\x00-\\x1f\\x7f]/g\r\n\r\n/**\r\n * Windows 예약 device 이름(확장자 무관) — 이 이름으로 쓰면 파일이 아니라 콘솔/포트\r\n * device 로 흘러가 예측 불가 동작/행을 일으킨다. `_` prefix 로 무력화한다.\r\n */\r\nconst WINDOWS_RESERVED_NAMES = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\\.|$)/i\r\n\r\n/** 동시 진행 업로드 상한 — 미완성(begin 후 end 미수신) 누적으로 인한 메모리 DoS 방어. */\r\nconst MAX_CONCURRENT_UPLOADS = 8\r\n\r\n/** 업로드 idle TTL(ms) — 마지막 활동 후 이 시간이 지나면 begin 시 lazy sweep 으로 회수. */\r\nconst UPLOAD_TTL_MS = 120_000\r\n\r\n/**\r\n * 모바일이 보낸 untrusted 파일명을 데스크탑 FS 에 안전한 단일 segment 로 정화한다.\r\n *\r\n * WHY: 모바일은 cross-platform(POSIX/Windows) 경로를 보낼 수 있고 path traversal\r\n * (`../../etc/passwd`)이나 제어문자/illegal char 를 포함할 수 있다. cwd 밖으로 쓰는 것을\r\n * 막기 위해 **마지막 segment 만** 취하고(POSIX `/` 와 Windows `\\` 양쪽 분리), 제어문자\r\n * strip + Windows illegal char 치환을 적용한다. 빈 문자열 또는 `.`/`..` 은 호출자가\r\n * bad_name 으로 거절하도록 `''` 를 반환한다(파일명으로 부적합).\r\n *\r\n * [name] 모바일이 선언한 파일명(경로 포함 가능). 반환은 정화된 basename 또는 `''`.\r\n */\r\nexport function sanitizeUploadName(name: string): string {\r\n  // 양쪽 구분자로 분리 후 마지막 비어있지 않은 segment 채택 (traversal/디렉터리 제거).\r\n  const segments = String(name ?? '').split(/[/\\\\]/)\r\n  const last = segments[segments.length - 1] ?? ''\r\n  let cleaned = last\r\n    .replace(CONTROL_CHARS, '')\r\n    .replace(WINDOWS_ILLEGAL_CHARS, '_')\r\n    .trim()\r\n  // 후행 dot/space 제거 — Windows 가 조용히 잘라 확장자 혼동(`x.txt.` → `x.txt`)을 막는다.\r\n  cleaned = cleaned.replace(/[. ]+$/, '')\r\n  if (cleaned === '' || cleaned === '.' || cleaned === '..') return ''\r\n  // 예약 device 이름(CON/NUL/COM1…)은 `_` prefix 로 무력화(device 로 쓰기 방지).\r\n  if (WINDOWS_RESERVED_NAMES.test(cleaned)) cleaned = `_${cleaned}`\r\n  return cleaned\r\n}\r\n\r\n/**\r\n * `dir/name` 이 비어있으면 그 경로를, 충돌하면 ` (1)`, ` (2)`… 를 확장자 앞에 끼워 빈 경로를 찾는다.\r\n *\r\n * WHY: 같은 이름 파일이 이미 있으면 덮어쓰지 않고 `report (1).pdf` 식으로 회피해 데이터\r\n * 손실을 막는다(브라우저 다운로드 관행). `exists` 를 주입받아 fs 없이 단위 테스트 가능.\r\n * 1000회까지 충돌하면(비현실적) `Date.now()` suffix 로 fall back 해 무한 루프를 방지한다.\r\n *\r\n * [dir] 대상 디렉터리(절대 경로). [name] 이미 정화된 파일명. [exists] 경로 존재 여부 판정.\r\n */\r\nexport function resolveCollisionPath(\r\n  dir: string,\r\n  name: string,\r\n  exists: (p: string) => boolean\r\n): string {\r\n  const first = path.join(dir, name)\r\n  if (!exists(first)) return first\r\n\r\n  const ext = path.extname(name)\r\n  const stem = name.slice(0, name.length - ext.length)\r\n  for (let i = 1; i <= 1000; i++) {\r\n    const candidate = path.join(dir, `${stem} (${i})${ext}`)\r\n    if (!exists(candidate)) return candidate\r\n  }\r\n  // 비현실적 fallback — 타임스탬프 suffix 로 충돌 회피 (정상 코드라 Date.now 허용).\r\n  return path.join(dir, `${stem} (${Date.now()})${ext}`)\r\n}\r\n\r\n/** 한 업로드의 in-memory 누적 상태. `end` 까지 디스크에 아무것도 쓰지 않는다. */\r\ninterface UploadEntry {\r\n  /** 머신 식별자 — end 시 cwd 해석에 사용. */\r\n  machineId: string\r\n  /** 정화 전 원본 파일명(end 에서 다시 sanitize). */\r\n  name: string\r\n  /** begin 이 선언한 size — end 에서 실제 누적과 일치하는지 검증(truncation 탐지). */\r\n  size: number\r\n  /** 예상 청크 수 — end 에서 모든 index 가 채워졌는지 검증. */\r\n  totalChunks: number\r\n  /** 지금까지 누적된 바이트 수(maxBytes 가드 + size 일치 검증). */\r\n  received: number\r\n  /**\r\n   * index → raw 청크. **arrival 순서가 아니라 index 로 보관**해 relay/네트워크가 재배열·중복\r\n   * 해도 end 에서 0..totalChunks-1 순서로 정확히 재조립한다(silent corruption 방어).\r\n   */\r\n  chunks: Map<number, Buffer>\r\n  /** maxBytes 초과 / 잘못된 index 등으로 손상되면 true — end 가 write_failed 로 거절. */\r\n  errored: boolean\r\n  /** 마지막 활동 시각(ms) — idle TTL sweep 기준. */\r\n  lastTouchMs: number\r\n}\r\n\r\n/**\r\n * 모바일→데스크탑 첨부 파일 업로드를 in-memory 로 누적해 머신 cwd 에 기록하는 수신기.\r\n *\r\n * WHY: ws-manager 의 begin/chunk/end 3-메시지 핸들러에서 fs/ws 의존성을 분리해 단위\r\n * 테스트 가능하게 한다. streaming 이 아니라 25MB 상한 안에서 메모리에 모은 뒤 `end` 에서\r\n * atomic write(`.part` → rename)한다 — 부분 쓰기 파일이 cwd 에 남지 않게 한다. 파일명/크기는\r\n * 모두 untrusted 라 sanitize·cap 한다. cwd 해석은 주입된 `resolveCwd` 에 위임(머신별 OSC7 cwd).\r\n */\r\nexport class FileUploadReceiver {\r\n  private readonly resolveCwd: (machineId: string) => string\r\n  private readonly maxBytes: number\r\n  private readonly uploads = new Map<string, UploadEntry>()\r\n\r\n  /**\r\n   * [resolveCwd] machineId → 기록 대상 디렉터리(절대 경로) 해석기(ssh.resolveLocalCwd).\r\n   * [maxBytes] 업로드 1건의 최대 바이트(기본 25MB). begin size 와 chunk 누적 양쪽에 적용.\r\n   */\r\n  constructor(opts: {\r\n    resolveCwd: (machineId: string) => string\r\n    maxBytes?: number\r\n  }) {\r\n    this.resolveCwd = opts.resolveCwd\r\n    this.maxBytes = opts.maxBytes ?? DEFAULT_MAX_UPLOAD_BYTES\r\n  }\r\n\r\n  /**\r\n   * 업로드 시작 — 파일명/크기를 검증하고 누적 entry 를 만든다.\r\n   *\r\n   * 검증 실패 시 즉시 회신할 에러 객체를 반환한다(호출자가 file.upload.done 으로 전달).\r\n   * 성공 시 entry 를 등록하고 `null` 을 반환한다(아직 응답 없음 — end 에서 응답).\r\n   * 동일 uploadId 가 재시작되면 이전 entry 를 덮어쓴다(중복 begin 방어).\r\n   *\r\n   * 반환: 거절 시 `{ ok:false, error }`(bad_name/too_large/too_many), 정상 시 `null`.\r\n   */\r\n  begin(\r\n    machineId: string,\r\n    uploadId: string,\r\n    name: string,\r\n    size: number,\r\n    totalChunks: number\r\n  ): { ok: false; error: string } | null {\r\n    this.sweepExpired()\r\n    if (sanitizeUploadName(name) === '') return { ok: false, error: 'bad_name' }\r\n    // 음수/소수/NaN size 거부 — `> maxBytes` 만으로는 `-1` 같은 값이 통과한다.\r\n    if (!Number.isInteger(size) || size < 0 || size > this.maxBytes) {\r\n      return { ok: false, error: 'too_large' }\r\n    }\r\n    if (!Number.isInteger(totalChunks) || totalChunks < 1) {\r\n      return { ok: false, error: 'protocol' }\r\n    }\r\n    // 동시 업로드 수 상한 — 새 uploadId 일 때만 카운트(같은 uploadId 재시작은 허용).\r\n    if (\r\n      !this.uploads.has(uploadId) &&\r\n      this.uploads.size >= MAX_CONCURRENT_UPLOADS\r\n    ) {\r\n      return { ok: false, error: 'too_many' }\r\n    }\r\n    this.uploads.set(uploadId, {\r\n      machineId,\r\n      name,\r\n      size,\r\n      totalChunks,\r\n      received: 0,\r\n      chunks: new Map(),\r\n      errored: false,\r\n      lastTouchMs: Date.now(),\r\n    })\r\n    return null\r\n  }\r\n\r\n  /**\r\n   * 청크 1개 수신 — base64 decode 해 누적한다. 알 수 없는 uploadId 는 무시(stale/취소 방어).\r\n   *\r\n   * 누적 바이트가 maxBytes 를 넘으면 entry 를 errored 로 표시하고 청크를 비운다(메모리 보호) —\r\n   * end 가 write_failed 로 거절한다. [index] 는 begin 의 totalChunks 범위로 검증하고 index 위치에\r\n   * 보관한다(arrival 순서 무관) — 범위 밖/중복 index 는 entry 를 errored 로 만든다(무결성 가드).\r\n   */\r\n  chunk(uploadId: string, index: number, dataB64: string): void {\r\n    const entry = this.uploads.get(uploadId)\r\n    if (!entry || entry.errored) return\r\n    // index 범위/중복 검증 — 손상되면 즉시 errored 로 마킹(silent corruption 방어).\r\n    if (\r\n      !Number.isInteger(index) ||\r\n      index < 0 ||\r\n      index >= entry.totalChunks ||\r\n      entry.chunks.has(index)\r\n    ) {\r\n      entry.errored = true\r\n      entry.chunks.clear()\r\n      return\r\n    }\r\n    const buf = Buffer.from(String(dataB64 ?? ''), 'base64')\r\n    if (entry.received + buf.length > this.maxBytes) {\r\n      entry.errored = true\r\n      entry.chunks.clear()\r\n      return\r\n    }\r\n    entry.chunks.set(index, buf)\r\n    entry.received += buf.length\r\n    entry.lastTouchMs = Date.now()\r\n  }\r\n\r\n  /**\r\n   * 업로드 종료 — 청크를 concat 해 머신 cwd 에 atomic write 하고 결과를 반환한다.\r\n   *\r\n   * `<final>.part` 로 먼저 쓴 뒤 final 로 rename 해 부분 파일 노출을 막는다. 충돌 파일명은\r\n   * resolveCollisionPath 로 회피한다. entry 는 성공/실패 무관 finally 에서 제거한다(누수 방지).\r\n   *\r\n   * 반환: 알 수 없는 uploadId 면 `{ ok:false, error:'protocol' }`, 손상이면 write_failed,\r\n   * 정상 시 `{ ok:true, path }`(기록된 절대 경로), 기록 실패 시 `{ ok:false, error:'write_failed' }`.\r\n   */\r\n  async end(\r\n    uploadId: string\r\n  ): Promise<{ ok: boolean; path?: string; error?: string }> {\r\n    const entry = this.uploads.get(uploadId)\r\n    if (!entry) return { ok: false, error: 'protocol' }\r\n    try {\r\n      if (entry.errored) return { ok: false, error: 'write_failed' }\r\n      // 무결성 가드: 모든 청크가 도착했고(구멍 없음) 누적 바이트가 선언 size 와 일치해야\r\n      // 한다. 불일치면 truncated/재배열 → ok:true 로 깨진 파일을 넘기지 않는다.\r\n      if (\r\n        entry.chunks.size !== entry.totalChunks ||\r\n        entry.received !== entry.size\r\n      ) {\r\n        return { ok: false, error: 'write_failed' }\r\n      }\r\n      const name = sanitizeUploadName(entry.name)\r\n      if (name === '') return { ok: false, error: 'bad_name' }\r\n\r\n      const dir = this.resolveCwd(entry.machineId)\r\n      // 세션 cwd 가 stale(리네임/삭제된 옛 경로)이면 부모 디렉토리 부재로 open 이 ENOENT 를\r\n      // 던져 write_failed 가 됐다(2026-06-20: 머신 cwd=…\\claude_code_mobile 부재 → 100% 실패).\r\n      // 기록 전 대상 폴더를 보장해 stale cwd 여도 업로드가 성공하게 한다(cross-platform).\r\n      await fs.promises.mkdir(dir, { recursive: true })\r\n      const finalPath = resolveCollisionPath(dir, name, p => fs.existsSync(p))\r\n      // .part 는 uploadId 로 고유화 — 같은 이름 동시 업로드의 temp 파일 충돌 방지.\r\n      const partPath = `${finalPath}.${uploadId}.part`\r\n      try {\r\n        // index 순서로 파일 핸들에 직접 스트리밍 — Buffer.concat 의 전체 2차 복사(최대 25MB)를\r\n        // 피하고 async write 로 메인 프로세스 이벤트 루프를 양보한다(relay/PTY 펌프 stall 방지).\r\n        const fh = await fs.promises.open(partPath, 'w')\r\n        try {\r\n          for (let i = 0; i < entry.totalChunks; i++) {\r\n            await fh.write(entry.chunks.get(i) as Buffer)\r\n          }\r\n        } finally {\r\n          await fh.close()\r\n        }\r\n        await fs.promises.rename(partPath, finalPath)\r\n      } catch {\r\n        // 부분 파일 정리 (best-effort) — 실패해도 무시.\r\n        try {\r\n          await fs.promises.rm(partPath, { force: true })\r\n        } catch {\r\n          /* noop */\r\n        }\r\n        return { ok: false, error: 'write_failed' }\r\n      }\r\n      return { ok: true, path: finalPath }\r\n    } finally {\r\n      this.uploads.delete(uploadId)\r\n    }\r\n  }\r\n\r\n  /**\r\n   * 진행 중인 업로드 1건을 버린다(클라이언트 disconnect 시 best-effort).\r\n   * end 전까지 디스크에 아무것도 쓰지 않으므로 fs 부수효과 없음.\r\n   */\r\n  abort(uploadId: string): void {\r\n    this.uploads.delete(uploadId)\r\n  }\r\n\r\n  /** 진행 중인 모든 업로드를 버린다(전체 shutdown 등). fs 부수효과 없음. */\r\n  abortAll(): void {\r\n    this.uploads.clear()\r\n  }\r\n\r\n  /**\r\n   * idle TTL 초과 entry 회수 — relay 경로는 per-client `close` 신호가 없어(단일 소켓)\r\n   * begin/chunk 만 보내고 end 를 안 보내는 미완성 업로드가 메모리에 영구 잔존할 수 있다.\r\n   * begin 진입마다 lazy sweep 해 [UPLOAD_TTL_MS] 무활동 entry 를 제거한다(별도 타이머 없이).\r\n   */\r\n  private sweepExpired(): void {\r\n    const now = Date.now()\r\n    for (const [id, entry] of this.uploads) {\r\n      if (now - entry.lastTouchMs > UPLOAD_TTL_MS) this.uploads.delete(id)\r\n    }\r\n  }\r\n}\r\n","import type { AgentConfigStore, FcmTokenEntry } from '../config/agent-config'\r\n\r\n/** FcmTokenStore 가 영속에 필요한 ConfigStore 부분만 추린 의존(테스트 주입 용이). */\r\ntype FcmTokenPersistence = Pick<AgentConfigStore, 'getFcmTokens' | 'setFcmTokens'>\r\n\r\n/**\r\n * 이 데스크탑에 등록된 폰 FCM 토큰을 메모리 + 영속 백엔드(암호화)에 보관한다(FCM Phase 3).\r\n *\r\n * `device.fcm_register`/`device.fcm_unregister` 를 ws-manager 가 받아 위임한다. machineId\r\n * 별 구분 없이 데스크탑(=relay 세션) 단위 목록으로 둔다 — device.fcm_register 에 machineId\r\n * 가 없고(폰-데스크탑 연결 단위 등록), 발송 대상 머신은 claude_state 가 결정해 payload 에\r\n * 싣기 때문이다. 변경 시 즉시 영속해 앱 재시작/killed 후에도 살아남는다.\r\n *\r\n * 영속 백엔드(electron-store/JSON)는 {@link AgentConfigStore} 로 **주입**받는다 — 코어가\r\n * 호스트 전용 저장소(electron-store)를 직접 import 하지 않게 하는 DI 경계(Phase A3).\r\n */\r\nexport class FcmTokenStore {\r\n  private entries: FcmTokenEntry[]\r\n\r\n  constructor(private readonly config: FcmTokenPersistence) {\r\n    this.entries = config.getFcmTokens()\r\n  }\r\n\r\n  /** 토큰 등록(동일 token 이면 platform/locale 갱신). 빈 token 은 무시. 즉시 영속. */\r\n  register(token: string, platform: string, locale: string): void {\r\n    if (!token) return\r\n    const idx = this.entries.findIndex((e) => e.token === token)\r\n    if (idx >= 0) this.entries[idx] = { token, platform, locale }\r\n    else this.entries.push({ token, platform, locale })\r\n    this.config.setFcmTokens(this.entries)\r\n  }\r\n\r\n  /** 토큰 해제(로그아웃). 등록돼 있을 때만 제거 후 영속. */\r\n  unregister(token: string): void {\r\n    const next = this.entries.filter((e) => e.token !== token)\r\n    if (next.length === this.entries.length) return\r\n    this.entries = next\r\n    this.config.setFcmTokens(this.entries)\r\n  }\r\n\r\n  /** 현재 등록된 토큰 문자열 목록(발송용). */\r\n  tokens(): string[] {\r\n    return this.entries.map((e) => e.token)\r\n  }\r\n}\r\n","import type {\n  BroadcastFn,\n  BroadcastRecipient,\n  ServerMessage,\n} from '@arva/shared/types'\n\nimport type { AgentConfigStore, AgentIdentity } from './config/agent-config'\nimport { configureLogDir } from './lib/logger'\nimport { SSHManager } from './managers/ssh-manager'\nimport { WsManager } from './managers/ws-manager'\n\n/**\n * {@link createAgentCore} 입력. host(데스크탑/헤드리스)가 자기 설정·식별·로그경로·메시지 sink 를\n * 주입한다 — 코어는 호스트 전용 의존(electron-store/렌더러)을 알지 못한다.\n */\nexport interface AgentCoreOptions {\n  config: AgentConfigStore\n  identity: AgentIdentity\n  /** scoped 로그 파일 디렉터리. 헤드리스는 `~/.ai_remote_vibe_agent/logs`. */\n  logDir: string\n  /**\n   * host 측 메시지 sink — 데스크탑은 렌더러 IPC, 헤드리스는 Ink 상태 갱신. mobile fan-out 은\n   * 코어가 내부에서 `ws.broadcastToMobile` 로 처리하므로 host 는 자기 화면 갱신만 신경쓰면 된다.\n   */\n  onHostMessage: (msg: ServerMessage) => void\n}\n\n/**\n * createAgentCore 반환 — 기동된 SSH/Ws 매니저 + 합성 broadcast + cleanup.\n */\nexport interface AgentCore {\n  ssh: SSHManager\n  ws: WsManager\n  /** host + mobile fan-out 합성 broadcast(매니저들이 내부적으로 이미 보유, 외부 트리거용 노출). */\n  broadcast: BroadcastFn\n  /** 프로세스 종료 훅(SIGINT 등)에서 호출 — 모든 PTY/WS 정리. */\n  cleanup(): void\n}\n\n/**\n * 호스트 비의존 에이전트 코어를 와이어링해 **즉시 기동**한다.\n *\n * 부수효과(생성자에서 발생): `SSHManager` 가 `local` 머신을 자동 등록하고, `WsManager` 가\n * 로컬 WS 서버를 띄운 뒤 relay host 에 자동 접속한다. 따라서 호출 직후부터 폰이 페어링 가능하다.\n * 로컬 셸 PTY 는 폰 `session_attach` 시점에 on-demand 로 spawn 되므로 추가 코드가 필요 없다.\n *\n * broadcast 의 host 측은 `onHostMessage` 로, mobile 측은 `ws.broadcastToMobile` 로 분기한다\n * (데스크탑 index.ts 의 broadcast 클로저와 동일한 skipRenderer/skipMobile/perRecipient 규약).\n */\nexport function createAgentCore(opts: AgentCoreOptions): AgentCore {\n  // 첫 로그 write 전에 scoped 로그 디렉터리 확정.\n  configureLogDir(opts.logDir)\n\n  // ws 는 broadcast 클로저가 참조하지만 broadcast 보다 늦게 생성된다 — late-bound(`let`)로 해소\n  // (데스크탑 index.ts 와 동일 패턴). 첫 broadcast 는 ssh/ws 기동 이후라 항상 정의돼 있다.\n  let ws: WsManager | undefined\n\n  const broadcast: BroadcastFn = (msg, bopts) => {\n    if (!bopts?.skipRenderer) {\n      const recipient: BroadcastRecipient = { kind: 'renderer' }\n      const hostMsg = bopts?.perRecipient\n        ? ({ ...msg, ...bopts.perRecipient(recipient) } as ServerMessage)\n        : msg\n      opts.onHostMessage(hostMsg)\n    }\n    if (!bopts?.skipMobile) ws?.broadcastToMobile(msg, bopts?.perRecipient)\n  }\n\n  const ssh = new SSHManager(broadcast)\n  ws = new WsManager(ssh, broadcast, opts.config, opts.identity)\n\n  // 종료 경로가 여럿(Ink exit / SIGTERM / waitUntilExit)이라 cleanup 가 2~3회 호출될 수 있다.\n  // 매니저 cleanup 이 현재는 멱등이지만 가드로 다중 호출을 명시적으로 무해화(미래 회귀 방어).\n  let cleanedUp = false\n\n  return {\n    ssh,\n    ws,\n    broadcast,\n    cleanup() {\n      if (cleanedUp) return\n      cleanedUp = true\n      ssh.cleanup()\n      ws?.cleanup()\n    },\n  }\n}\n","import crypto from 'crypto'\nimport http from 'http'\nimport type { IncomingMessage, Server, ServerResponse } from 'http'\n\nimport { createLogger } from '../lib/logger'\n\nimport type {\n  BroadcastFn,\n  ClaudeSessionState,\n  ClaudeStateSummary,\n} from '@arva/shared/types'\nimport { readTranscriptTailByPath } from './agent-sessions'\nimport type { SSHManager } from './ssh-manager'\n\nconst log = createLogger('hook-receiver')\n\n/** 알림 본문 미리보기 최대 길이 (시크릿 마스킹 후 절단). */\nconst PREVIEW_CAP = 280\n\n/**\n * 미리보기 문자열을 만든다 — **시크릿 마스킹 후 절단**.\n *\n * transcript/프롬프트에는 토큰·키가 섞일 수 있어 원문을 알림에 실으면 안 된다(보안).\n * Bearer/`sk-`류 prefix/`key=value` 패턴을 best-effort 로 가린다. 완전한 DLP 가 아니라\n * \"알림 본문에 명백한 시크릿이 노출되지 않게\" 하는 1차 방어선이다.\n */\nfunction toPreview(text: string): string {\n  if (!text) return ''\n  const masked = text\n    .replace(/(Bearer\\s+)[A-Za-z0-9._-]+/gi, '$1***')\n    .replace(/\\b(sk|pk|rk|ghp|gho|ghs|xox[baprs])[-_][A-Za-z0-9_-]{6,}\\b/g, '$1-***')\n    .replace(\n      /((?:api[_-]?key|token|secret|password|passwd|pwd)\\s*[:=]\\s*)\\S+/gi,\n      '$1***'\n    )\n  return masked.length > PREVIEW_CAP ? `${masked.slice(0, PREVIEW_CAP)}…` : masked\n}\n\n/** assistant 텍스트가 코드 블록을 포함하면 'code', 아니면 'text'. */\nfunction classifyKind(text: string): 'text' | 'code' {\n  return text.includes('```') ? 'code' : 'text'\n}\n\n/**\n * [A2/A9 20260614] 고위험 파괴적 동작 denylist (소문자 비교).\n *\n * 모바일 `agent_notification_event.isHighRiskApproval`(`../ai_remote_vibe_mobile`)의 패턴을\n * 데스크탑으로 이전한 것 — **권위 single-source 는 데스크탑**, 모바일 측은 advisory(2차)로\n * 격하된다. drift 방지를 위해 모바일 denylist 와 동기 유지할 것. 패턴 추가/변경 시 양쪽 갱신.\n */\nconst RISK_PATTERNS = [\n  'rm -rf',\n  'rm -fr',\n  'sudo ',\n  'push --force',\n  'push -f',\n  'force-with-lease',\n  'drop table',\n  'drop database',\n  'truncate ',\n  'mkfs',\n  'dd if=',\n  'chmod 777',\n  ':(){',\n  'reset --hard',\n  'git clean -',\n]\n\n/**\n * [A2/A9 20260614] 권한 승인 텍스트가 **고위험**인지 desktop-side 권위 판정.\n *\n * 입력은 permission_prompt 의 결합 문자열(`\"<도구명>: <input 미리보기>\"` 또는 fallback\n * message) — `tool_use` 의 도구명·구조화 input 이 녹아 있어 모바일의 최종 preview 추정보다\n * 정확하다. [toPreview] 의 시크릿 마스킹 **이전** raw 로 분류해 패턴이 가려지지 않게 한다.\n * 미일치 = 비고위험(false). denylist 는 [RISK_PATTERNS] (모바일과 동기).\n */\nexport function classifyRisk(text: string): boolean {\n  const t = text.toLowerCase()\n  return RISK_PATTERNS.some(p => t.includes(p))\n}\n\n/**\n * [Push-Notif Phase 1] Claude 훅이 POST 하는 상태 이벤트를 받아 `claude_state` 로 emit 하는\n * **loopback HTTP 수신부**.\n *\n * 왜 별도 HTTP 서버인가: LAN WS 는 `/mobile` 에서 바이너리+HMAC 라 text 프레임을 거부하므로\n * `node` 훅 러너가 도달할 수 없다. 그래서 127.0.0.1 전용 평문 HTTP 를 따로 연다(per-launch\n * 랜덤 토큰 인증, 비-loopback 차단). 받은 이벤트는 `cwd→machineId` 역매핑으로 무관 세션을\n * 걸러낸 뒤 `broadcast()`(renderer + 모든 모바일 + relay) 로 fan-out 한다.\n *\n * 부수효과: `broadcast` 호출(IPC/WS 송신). `currentOpenPrompt` 에 needs_input 의 eventId 를\n * 기록(Phase 2 quick_reply idempotency 가 참조).\n */\nexport class HookReceiver {\n  /** 훅 러너 인증용 per-launch 토큰. installClaudeHooks 로 전달돼 config 에 기록된다. */\n  readonly token = crypto.randomBytes(24).toString('hex')\n  private server: Server | null = null\n  /** machineId → 단조 증가 seq (eventId 생성). */\n  private seq = new Map<string, number>()\n  /** machineId → 현재 열린 prompt 의 eventId (needs_input 시 set, complete 시 clear). */\n  private currentOpenPrompt = new Map<string, string>()\n  /**\n   * [FCM Phase 3] awaiting-approval edge 시 호출 — push-dispatcher 로 연결(index 에서 set).\n   * 앱이 killed 된 폰에 승인 푸시를 보내는 트리거. 미설정(훅 비활성/실패)이면 no-op.\n   */\n  onApprovalPending?: (\n    machineId: string,\n    eventId: string,\n    summary?: ClaudeStateSummary\n  ) => void\n  /**\n   * [Reclaim guard 20260606] machineId → 마지막 claude_state. AuthorityResolver 의 handed_off\n   * watchdog 가 `isAgentBusy` 로 조회 — 에이전트 턴 중(working/awaiting-approval)이면 출력이\n   * 잠시 없어도 회수를 보류한다(권한 프롬프트 응시 등). `emit` 이 단일 갱신 지점.\n   *\n   * 주의: 머신 제거/disconnect 시 정리되지 않는다(`seq`/`currentOpenPrompt` 와 동일). 에이전트가\n   * Stop 없이 죽으면(Ctrl-C/크래시) 'working' 이 잔류해 busy=true 가 유지될 수 있으나,\n   * AuthorityResolver 의 busy 절대 상한 `HANDOFF_BUSY_HARD_CEILING_MS`(30min, H6 fix 20260607)가\n   * 회수를 보장하므로 영구 보호로 이어지지 않는다(최악 30min 지연, map 증가는 로컬 머신 수로 bound).\n   */\n  private lastState = new Map<string, ClaudeSessionState>()\n\n  constructor(\n    private broadcast: BroadcastFn,\n    private ssh: SSHManager\n  ) {}\n\n  /**\n   * 127.0.0.1 의 임의 포트로 수신부를 연다. 반환된 `{port, token}` 을 installClaudeHooks 에\n   * 넘겨 훅 러너가 찾아올 수 있게 한다.\n   */\n  start(): Promise<{ port: number; token: string }> {\n    return new Promise((resolve, reject) => {\n      const server = http.createServer((req, res) => this.handleRequest(req, res))\n      server.on('error', err => {\n        log.warn(`hook-receiver listen 실패: ${err.message}`)\n        reject(err)\n      })\n      server.listen(0, '127.0.0.1', () => {\n        this.server = server\n        const addr = server.address()\n        const port = typeof addr === 'object' && addr ? addr.port : 0\n        log.info(`hook-receiver 수신 시작 127.0.0.1:${port}`)\n        resolve({ port, token: this.token })\n      })\n    })\n  }\n\n  /** 수신부 종료 (데스크탑 종료 시). */\n  stop(): void {\n    try {\n      this.server?.close()\n    } catch {\n      // 무시\n    }\n    this.server = null\n  }\n\n  /**\n   * 머신의 현재 열린 prompt eventId (Phase 2 quick_reply 의 stale-응답 idempotency 용).\n   * 없으면 null.\n   */\n  currentOpenPromptId(machineId: string): string | null {\n    return this.currentOpenPrompt.get(machineId) ?? null\n  }\n\n  /**\n   * [Reclaim guard 20260606] 머신의 에이전트가 턴 진행 중인지 — handed_off watchdog 가\n   * 회수 보류 판단에 사용. working/awaiting-approval=true, complete/미수신=false.\n   */\n  isAgentBusy(machineId: string): boolean {\n    const s = this.lastState.get(machineId)\n    return s === 'working' || s === 'awaiting-approval'\n  }\n\n  private handleRequest(req: IncomingMessage, res: ServerResponse): void {\n    if (req.method !== 'POST' || req.url !== '/claude-state') {\n      res.writeHead(404).end()\n      return\n    }\n    // 토큰 검증 (constant-time). 길이 불일치/부재는 즉시 거부.\n    const raw = req.headers['x-hook-token']\n    const got = Array.isArray(raw) ? raw[0] : raw\n    if (\n      typeof got !== 'string' ||\n      got.length !== this.token.length ||\n      !crypto.timingSafeEqual(Buffer.from(got), Buffer.from(this.token))\n    ) {\n      res.writeHead(401).end()\n      return\n    }\n    let body = ''\n    req.on('data', chunk => {\n      body += chunk\n      if (body.length > 1_000_000) req.destroy() // 1MB 가드\n    })\n    req.on('end', () => {\n      res.writeHead(204).end()\n      try {\n        const parsed = JSON.parse(body) as {\n          event?: unknown\n          payload?: unknown\n        }\n        if (\n          typeof parsed.event === 'string' &&\n          parsed.payload &&\n          typeof parsed.payload === 'object'\n        ) {\n          this.processEvent(\n            parsed.event,\n            parsed.payload as Record<string, unknown>\n          )\n        }\n      } catch {\n        // malformed body 무시\n      }\n    })\n    req.on('error', () => {\n      try {\n        res.writeHead(400).end()\n      } catch {\n        // 무시\n      }\n    })\n  }\n\n  /**\n   * 훅 이벤트 1건을 `claude_state` 로 변환·emit. cwd 가 로컬 머신과 매칭 안 되면 drop.\n   *\n   * - Stop → `complete` (요약은 payload 의 `last_assistant_message` 인라인 사용, transcript\n   *   read 0). `stop_hook_active===true` 는 재귀 가드로 무시.\n   * - Notification `permission_prompt` → `awaiting-approval`(kind permission, 본문은\n   *   `transcript_path` tail 의 pending tool_use). `elicitation_*` → `awaiting-approval`(question).\n   *   `idle_prompt` 등은 Phase 1 에서 무시(Stop 과 중복 — Phase 2 정교화).\n   * - UserPromptSubmit → `working` (요약 없음, 진행 표시).\n   */\n  private processEvent(event: string, payload: Record<string, unknown>): void {\n    const cwd = typeof payload.cwd === 'string' ? payload.cwd : ''\n    if (!cwd) return\n    const machineId = this.ssh.findLocalMachineIdByCwd(cwd)\n    if (!machineId) return // 무관 세션 — drop\n\n    if (event === 'Stop') {\n      if (payload.stop_hook_active === true) return\n      const text =\n        typeof payload.last_assistant_message === 'string'\n          ? payload.last_assistant_message\n          : ''\n      const eventId = this.nextEventId(machineId)\n      this.currentOpenPrompt.delete(machineId)\n      this.emit(\n        machineId,\n        eventId,\n        'complete',\n        text ? { kind: classifyKind(text), preview: toPreview(text) } : undefined\n      )\n      return\n    }\n\n    if (event === 'Notification') {\n      const nt =\n        typeof payload.notification_type === 'string'\n          ? payload.notification_type\n          : ''\n      const message = typeof payload.message === 'string' ? payload.message : ''\n      if (nt === 'permission_prompt') {\n        const tail = readTranscriptTailByPath(\n          typeof payload.transcript_path === 'string'\n            ? payload.transcript_path\n            : ''\n        )\n        const raw = tail.lastToolUse\n          ? `${tail.lastToolUse.name}: ${tail.lastToolUse.inputPreview}`\n          : message\n        const eventId = this.nextEventId(machineId)\n        this.currentOpenPrompt.set(machineId, eventId)\n        // [A2/A9] risk 는 마스킹 전 raw 로 권위 분류 — preview 는 마스킹 후(보안), 분류는 정확도 우선.\n        this.emit(machineId, eventId, 'awaiting-approval', {\n          kind: 'permission',\n          preview: toPreview(raw),\n          isHighRisk: classifyRisk(raw),\n        })\n        return\n      }\n      if (nt.startsWith('elicitation')) {\n        const eventId = this.nextEventId(machineId)\n        this.currentOpenPrompt.set(machineId, eventId)\n        this.emit(machineId, eventId, 'awaiting-approval', {\n          kind: 'question',\n          preview: toPreview(message),\n        })\n      }\n      return\n    }\n\n    if (event === 'UserPromptSubmit') {\n      this.emit(machineId, this.nextEventId(machineId), 'working')\n    }\n  }\n\n  private nextEventId(machineId: string): string {\n    const n = (this.seq.get(machineId) ?? 0) + 1\n    this.seq.set(machineId, n)\n    return `${machineId}#${n}`\n  }\n\n  /** claude_state 를 로깅 + fan-out — 4개 분기의 broadcast 를 한 곳으로 모아 관측성 확보. */\n  private emit(\n    machineId: string,\n    eventId: string,\n    state: ClaudeSessionState,\n    summary?: ClaudeStateSummary\n  ): void {\n    log.info(\n      `claude_state machine=${machineId} event=${eventId} state=${state}${summary ? ` kind=${summary.kind}` : ''}`\n    )\n    // [Reclaim guard 20260606] busy 판단 source 갱신 — broadcast 와 동일 funnel 에서.\n    this.lastState.set(machineId, state)\n    this.broadcast({ type: 'claude_state', machineId, eventId, state, summary })\n    // [FCM Phase 3] 앱 killed 폰에 승인 푸시 트리거(push-dispatcher 위임). presence·토큰\n    // 게이트는 dispatcher 내부에서 처리한다.\n    if (state === 'awaiting-approval') {\n      this.onApprovalPending?.(machineId, eventId, summary)\n    }\n  }\n}\n","import fs from 'fs'\nimport os from 'os'\nimport path from 'path'\n\nimport { createLogger } from '../lib/logger'\n\nconst log = createLogger('claude-hooks')\n\n/**\n * [Push-Notif Phase 1] Claude 훅이 발화 시 실행하는 **번들 러너**의 소스.\n *\n * 데스크탑이 런타임에 `<userData>/claude-state-hook.js` 로 기록한다(asar/버전 경로 churn\n * 회피 — settings.json 은 이 안정 경로만 참조). 이 스크립트는:\n *   1. 같은 폴더의 `claude-hook.json` 에서 `{port, token}` 을 읽고(없으면 즉시 exit 0 —\n *      데스크탑이 꺼져 있으면 네트워크 시도조차 안 함),\n *   2. stdin 의 훅 JSON 을 받아,\n *   3. 데스크탑 loopback 수신부(`127.0.0.1:<port>/claude-state`)로 `x-hook-token` 과 함께 POST.\n * 모든 실패는 swallow + exit 0 — **Claude TUI 를 절대 막거나 깨뜨리지 않는다**.\n *\n * 순수 Node stdlib(http/fs/path)만 사용 — jq/curl/bash 비의존(크로스 플랫폼).\n */\nconst HOOK_SCRIPT = `// AUTO-GENERATED by ai_remote_vibe_agent (Push-Notif Phase 1). Safe to delete.\nconst fs = require('fs')\nconst http = require('http')\nconst path = require('path')\nconst event = process.argv[2] || 'unknown'\nlet cfg\ntry {\n  cfg = JSON.parse(fs.readFileSync(path.join(__dirname, 'claude-hook.json'), 'utf8'))\n} catch (_) {\n  process.exit(0)\n}\nlet buf = ''\nprocess.stdin.setEncoding('utf8')\nprocess.stdin.on('data', c => { buf += c })\nprocess.stdin.on('error', () => process.exit(0))\nprocess.stdin.on('end', () => {\n  let payload = {}\n  try { payload = JSON.parse(buf) } catch (_) {}\n  const body = JSON.stringify({ event, payload })\n  const req = http.request(\n    {\n      host: '127.0.0.1',\n      port: cfg.port,\n      path: '/claude-state',\n      method: 'POST',\n      timeout: 2000,\n      headers: {\n        'content-type': 'application/json',\n        'x-hook-token': cfg.token,\n        'content-length': Buffer.byteLength(body),\n      },\n    },\n    res => { res.resume(); res.on('end', () => process.exit(0)) }\n  )\n  req.on('error', () => process.exit(0))\n  req.on('timeout', () => { req.destroy(); process.exit(0) })\n  req.write(body)\n  req.end()\n})\n`\n\n/** 설치 대상 훅 이벤트. Stop=complete, Notification=needs_input, UserPromptSubmit=working. */\nconst HOOK_EVENTS = ['Stop', 'Notification', 'UserPromptSubmit'] as const\n\ninterface HookCommand {\n  type: 'command'\n  command: string\n  timeout?: number\n}\ninterface HookEntry {\n  matcher?: string\n  hooks: HookCommand[]\n}\n\n/** `~/.claude/settings.json` 경로 (크로스 플랫폼, 홈 하드코딩 금지). */\nfunction settingsPath(): string {\n  return path.join(os.homedir(), '.claude', 'settings.json')\n}\n\n/** 번들 훅 러너의 안정 경로 (`<userData>/claude-state-hook.js`). */\nfunction hookScriptPath(userDataDir: string): string {\n  return path.join(userDataDir, 'claude-state-hook.js')\n}\n\n/** 런타임 설정(port/token) 경로 (`<userData>/claude-hook.json`). */\nfunction hookConfigPath(userDataDir: string): string {\n  return path.join(userDataDir, 'claude-hook.json')\n}\n\n/**\n * 우리가 주입할 hooks 블록을 만든다. command 는 백슬래시 대신 forward-slash 경로를\n * 써서(Node 가 수용) Git Bash/PowerShell 양쪽의 따옴표·이스케이프 차이를 회피한다.\n */\nfunction buildHooksBlock(scriptPath: string): Record<string, HookEntry[]> {\n  const sp = scriptPath.replace(/\\\\/g, '/')\n  const cmd = (event: string): HookCommand => ({\n    type: 'command',\n    command: `node \"${sp}\" ${event}`,\n    timeout: 10,\n  })\n  return {\n    Stop: [{ hooks: [cmd('Stop')] }],\n    Notification: [\n      {\n        // 권한/질문/유휴만 — auth_success 등 무관 타입은 받지 않는다.\n        matcher:\n          'permission_prompt|idle_prompt|elicitation_dialog|elicitation_complete|elicitation_response',\n        hooks: [cmd('Notification')],\n      },\n    ],\n    UserPromptSubmit: [{ hooks: [cmd('UserPromptSubmit')] }],\n  }\n}\n\n/** 어떤 hooks 엔트리가 **우리 것**인지(= 우리 스크립트 파일명을 command 에 포함) 판별. */\nfunction isOurHookEntry(entry: unknown, scriptFileName: string): boolean {\n  const e = entry as HookEntry | undefined\n  return Array.isArray(e?.hooks)\n    ? e.hooks.some(\n        h => typeof h?.command === 'string' && h.command.includes(scriptFileName)\n      )\n    : false\n}\n\n/**\n * [Push-Notif Phase 1] Claude 훅을 설치한다 — **idempotent + additive**.\n *\n * 부수효과: (1) `<userData>/claude-state-hook.js` 기록, (2) `<userData>/claude-hook.json`\n * 에 `{port, token}` 기록, (3) `~/.claude/settings.json` 의 `hooks` 에 우리 엔트리 merge.\n * 사용자/타 도구의 기존 훅은 보존하고, **우리 이전 엔트리는 제거 후 새로 추가**(중복 방지 +\n * 경로/포트 갱신). settings.json 파싱 실패 시 **clobber 하지 않고 중단**(false 반환).\n *\n * @returns 성공 여부. 실패해도 throw 하지 않음 — 알림 기능은 best-effort.\n */\nexport function installClaudeHooks(\n  userDataDir: string,\n  port: number,\n  token: string\n): boolean {\n  try {\n    const scriptPath = hookScriptPath(userDataDir)\n    fs.writeFileSync(scriptPath, HOOK_SCRIPT, 'utf-8')\n    fs.writeFileSync(\n      hookConfigPath(userDataDir),\n      JSON.stringify({ port, token }),\n      'utf-8'\n    )\n\n    const settingsFile = settingsPath()\n    let settings: Record<string, unknown> = {}\n    if (fs.existsSync(settingsFile)) {\n      try {\n        settings = JSON.parse(fs.readFileSync(settingsFile, 'utf-8'))\n      } catch (err) {\n        // 손상/비표준 settings.json 을 덮어쓰면 사용자 설정이 날아간다 — 중단.\n        log.warn(\n          `settings.json 파싱 실패 → 훅 설치 중단: ${(err as Error).message}`\n        )\n        return false\n      }\n    } else {\n      fs.mkdirSync(path.dirname(settingsFile), { recursive: true })\n    }\n\n    const scriptFile = path.basename(scriptPath)\n    const fresh = buildHooksBlock(scriptPath)\n    const hooks =\n      settings.hooks && typeof settings.hooks === 'object'\n        ? (settings.hooks as Record<string, unknown[]>)\n        : {}\n    for (const event of HOOK_EVENTS) {\n      const existing = Array.isArray(hooks[event]) ? hooks[event] : []\n      const others = existing.filter(e => !isOurHookEntry(e, scriptFile))\n      hooks[event] = [...others, ...fresh[event]]\n    }\n    settings.hooks = hooks\n    fs.writeFileSync(\n      settingsFile,\n      `${JSON.stringify(settings, null, 2)}\\n`,\n      'utf-8'\n    )\n    log.info(`Claude 훅 설치 완료 (port=${port}) → ${settingsFile}`)\n    return true\n  } catch (err) {\n    log.warn(`훅 설치 실패: ${(err as Error).message}`)\n    return false\n  }\n}\n\n/**\n * 런타임 설정(`claude-hook.json`)만 삭제한다 — 데스크탑 종료 시 호출.\n *\n * settings.json 의 훅 엔트리는 그대로 두되(다음 실행 시 재사용), 설정 파일을 지우면\n * 훅 러너가 \"설정 없음 → 즉시 exit 0\"로 **네트워크 시도조차 안 한다**(stale 포트로 엉뚱한\n * 프로세스를 POST 하는 일 방지).\n */\nexport function removeRuntimeConfig(userDataDir: string): void {\n  try {\n    fs.rmSync(hookConfigPath(userDataDir), { force: true })\n  } catch {\n    // 무시 — 종료 흐름 막지 않음.\n  }\n}\n\n/**\n * Claude 훅을 **완전 제거**한다 — settings.json 의 우리 엔트리 + 스크립트/설정 파일.\n *\n * 사용자/타 도구의 다른 훅은 보존. (Phase 1 에서는 자동 호출 안 함 — 향후 설정 토글/\n * 언인스톨러용 export.)\n */\nexport function uninstallClaudeHooks(userDataDir: string): void {\n  try {\n    const scriptFile = path.basename(hookScriptPath(userDataDir))\n    const settingsFile = settingsPath()\n    if (fs.existsSync(settingsFile)) {\n      const settings = JSON.parse(fs.readFileSync(settingsFile, 'utf-8')) as Record<\n        string,\n        unknown\n      >\n      const hooks = settings.hooks as Record<string, unknown[]> | undefined\n      if (hooks) {\n        for (const event of HOOK_EVENTS) {\n          if (!Array.isArray(hooks[event])) continue\n          hooks[event] = hooks[event].filter(e => !isOurHookEntry(e, scriptFile))\n          if (hooks[event].length === 0) delete hooks[event]\n        }\n        if (Object.keys(hooks).length === 0) delete settings.hooks\n        fs.writeFileSync(\n          settingsFile,\n          `${JSON.stringify(settings, null, 2)}\\n`,\n          'utf-8'\n        )\n      }\n    }\n    fs.rmSync(hookScriptPath(userDataDir), { force: true })\n    fs.rmSync(hookConfigPath(userDataDir), { force: true })\n    log.info('Claude 훅 제거 완료')\n  } catch (err) {\n    log.warn(`훅 제거 실패: ${(err as Error).message}`)\n  }\n}\n","import { randomUUID } from 'node:crypto'\nimport os from 'node:os'\nimport path from 'node:path'\n\nimport type { AgentConfigStore, FcmTokenEntry } from '@arva/core'\nimport {\n  AUTH_TIMEOUT_MS_DEFAULT,\n  HEARTBEAT_INTERVAL_MS_DEFAULT,\n  PONG_TIMEOUT_MS_DEFAULT,\n  WS_PORT_DEFAULT,\n  resolveRelayUrls,\n} from '@arva/shared/relay-config'\n\nimport { type HeadlessStoreShape, readStore, writeStore } from './json-file'\n\nconst STORE_FILE = path.join(os.homedir(), '.ai_remote_vibe_agent', 'headless-store.json')\n\n/**\n * 헤드리스용 {@link AgentConfigStore} 구현 — JSON 파일 백엔드.\n *\n * 데스크탑 electron-store(safeStorage) 대신 `~/.ai_remote_vibe_agent/headless-store.json` 평문을\n * 쓴다(헤드리스엔 OS 암호화 API 가 없음). install_id 는 최초 1회 생성·영속(GA4 distinct 키).\n * relay 시크릿/URL 은 파일에 두지 않고 **런타임 env** 로 받되, 없으면 빌드 시 박은 기본값\n * (`__REINS_DEFAULT_RELAY_*__`, tsup `define`)으로 fallback 한다 — `npm i -g @junyoung-kim/reins` 사용자가 별도\n * 설정 없이 클라우드 relay 에 곧장 붙도록(데스크탑이 같은 값을 바이너리에 임베드하는 것과 동일).\n * 우선순위: **셸 env > `.env` > 빌드 기본값**. relay URL 은 추가로 stored(TUI 편집)가 최우선.\n */\nexport class HeadlessConfigStore implements AgentConfigStore {\n  private data: HeadlessStoreShape\n\n  constructor(private readonly file: string = STORE_FILE) {\n    this.data = readStore(file)\n    if (!this.data.install_id) {\n      this.data.install_id = randomUUID()\n      writeStore(file, this.data)\n    }\n  }\n\n  getRelayUrls(): string[] {\n    // stored(JSON) > env(MAIN_VITE_RELAY_URL/VIBE_RELAY_URL) > 빌드 기본값. 해석 로직은 데스크탑과\n    // 공유(@arva/shared/relay-config) — env 접근만 헤드리스(런타임 process.env)가 책임진다.\n    // 빌드 기본값(__REINS_DEFAULT_RELAY_URL__)은 env 둘 다 없을 때만 쓰여, npm 설치 직후에도 relay 가 잡힌다.\n    return resolveRelayUrls(\n      this.data.relay_urls,\n      process.env.MAIN_VITE_RELAY_URL ?? process.env.VIBE_RELAY_URL ?? __REINS_DEFAULT_RELAY_URL__\n    )\n  }\n\n  getWsPort(): number {\n    return this.data.ws_port ?? WS_PORT_DEFAULT\n  }\n\n  getHeartbeatIntervalMs(): number {\n    return this.data.heartbeat_interval_ms ?? HEARTBEAT_INTERVAL_MS_DEFAULT\n  }\n\n  getPongTimeoutMs(): number {\n    return this.data.pong_timeout_ms ?? PONG_TIMEOUT_MS_DEFAULT\n  }\n\n  getAuthTimeoutMs(): number {\n    return this.data.auth_timeout_ms ?? AUTH_TIMEOUT_MS_DEFAULT\n  }\n\n  /**\n   * relay URL 목록을 영속화한다(index 0=primary). TUI 옵션 화면에서 편집.\n   *\n   * **적용 시점**: `getRelayUrls()` 는 코어가 relay 연결마다 fresh read 하므로,\n   * 저장 후 `core.ws.reconnect()` 로 즉시 반영된다(재시작 불필요). 검증(ws/wss 스킴)은\n   * 호출 측(UI)이 {@link ../config/validate} 로 수행 — 이 setter 는 영속만 책임진다.\n   */\n  setRelayUrls(urls: string[]): void {\n    this.data.relay_urls = urls\n    writeStore(this.file, this.data)\n  }\n\n  /**\n   * 로컬 WS 서버 포트를 영속화한다.\n   *\n   * **적용 시점**: 코어가 부팅 시 1회 read 후 서버를 bind 하므로 **재시작 필요**(라이브 리바인드 없음).\n   */\n  setWsPort(port: number): void {\n    this.data.ws_port = port\n    writeStore(this.file, this.data)\n  }\n\n  /** heartbeat 간격(ms) 영속화. 코어가 부팅 시 캐시 → **재시작 후 반영**. */\n  setHeartbeatIntervalMs(ms: number): void {\n    this.data.heartbeat_interval_ms = ms\n    writeStore(this.file, this.data)\n  }\n\n  /** pong 타임아웃(ms) 영속화. **재시작 후 반영**. */\n  setPongTimeoutMs(ms: number): void {\n    this.data.pong_timeout_ms = ms\n    writeStore(this.file, this.data)\n  }\n\n  /** auth 타임아웃(ms) 영속화. **재시작 후 반영**. */\n  setAuthTimeoutMs(ms: number): void {\n    this.data.auth_timeout_ms = ms\n    writeStore(this.file, this.data)\n  }\n\n  /**\n   * 옵션 화면 일괄 저장 — 제공된 키만 병합하고 `writeStore` 를 **1회만** 호출한다.\n   * 필드별 setter 를 연달아 부르면 같은 파일을 N회 쓰므로(한 저장 = 5 fsync), 옵션 화면은 이걸 쓴다.\n   * 적용 시맨틱(relay=reconnect 즉시, ws/timeout=재시작)은 개별 setter 의 TSDoc 참조.\n   */\n  setOptions(\n    patch: Partial<\n      Pick<\n        HeadlessStoreShape,\n        'relay_urls' | 'ws_port' | 'heartbeat_interval_ms' | 'pong_timeout_ms' | 'auth_timeout_ms'\n      >\n    >\n  ): void {\n    Object.assign(this.data, patch)\n    writeStore(this.file, this.data)\n  }\n\n  getInstallId(): string {\n    // 생성자에서 보장되므로 항상 존재.\n    return this.data.install_id as string\n  }\n\n  getFcmTokens(): FcmTokenEntry[] {\n    return this.data.fcm_tokens ?? []\n  }\n\n  setFcmTokens(entries: FcmTokenEntry[]): void {\n    this.data.fcm_tokens = entries\n    writeStore(this.file, this.data)\n  }\n\n  getRelayAgentSecret(): string {\n    // 셸 env > `.env` > 빌드 기본값(__REINS_DEFAULT_RELAY_SECRET__, tsup define). 비로깅.\n    // 데스크탑 바이너리가 같은 시크릿을 임베드하므로 npm 배포본에 박아도 새 노출은 아니다(공유 부트스트랩 시크릿).\n    return (\n      process.env.MAIN_VITE_RELAY_AGENT_SECRET ??\n      process.env.VIBE_RELAY_AGENT_SECRET ??\n      __REINS_DEFAULT_RELAY_SECRET__\n    )\n  }\n}\n","import fs from 'node:fs'\nimport path from 'node:path'\n\nimport type { FcmTokenEntry } from '@arva/core'\n\n/** 헤드리스 설정 파일 스키마(`~/.ai_remote_vibe_agent/headless-store.json`). */\nexport interface HeadlessStoreShape {\n  relay_urls?: string[]\n  ws_port?: number\n  /** relay ping 간격(ms). 미설정 시 {@link HEARTBEAT_INTERVAL_MS_DEFAULT}. 코어가 부팅 시 1회 read → 변경 적용엔 재시작 필요. */\n  heartbeat_interval_ms?: number\n  /** pong 대기 타임아웃(ms). 미설정 시 기본값. 재시작 시 반영. */\n  pong_timeout_ms?: number\n  /** LAN 인증 핸드셰이크 타임아웃(ms). 미설정 시 기본값. 재시작 시 반영. */\n  auth_timeout_ms?: number\n  install_id?: string\n  fcm_tokens?: FcmTokenEntry[]\n}\n\n/**\n * 설정 파일을 읽는다. 없거나 손상 시 빈 객체 — 데스크탑 electron-store 와 달리 평문 JSON\n * (safeStorage 부재). FCM 토큰은 PII 라 향후 OS 키체인 연동 여지를 남긴다(현재 MVP 는 평문).\n */\nexport function readStore(file: string): HeadlessStoreShape {\n  try {\n    return JSON.parse(fs.readFileSync(file, 'utf8')) as HeadlessStoreShape\n  } catch {\n    return {}\n  }\n}\n\n/**\n * 설정 파일을 best-effort 로 저장한다(디렉터리 보장 후 write).\n *\n * FCM 토큰 등 PII 를 평문으로 담으므로 멀티유저 호스트에서 동거 사용자 노출을 줄이려 파일/디렉터리\n * 권한을 0600/0700 으로 제한한다(POSIX). Windows 는 mode 가 무시되어 무해.\n */\nexport function writeStore(file: string, data: HeadlessStoreShape): void {\n  fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 })\n  fs.writeFileSync(file, JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 })\n}\n","import fs from 'node:fs'\r\nimport os from 'node:os'\r\nimport path from 'node:path'\r\n\r\n/**\r\n * reins 가 실제 소비하는 env 키 — `.env` 에선 이 키들만 `process.env` 로 주입한다.\r\n *\r\n * **WHY(보안)**: `.env` 는 cwd 상위에서 자동 발견되므로(신뢰 못 하는 ancestor 가능), 거기 적힌\r\n * 임의 `KEY=VALUE` 를 전부 주입하면 `NODE_OPTIONS`/`PATH` 등으로 자식 프로세스 코드 실행을 유발할\r\n * 수 있다. reins 가 쓰는 relay 키만 통과시켜 그 표면을 없앤다. (셸 env 는 이 필터를 거치지 않음.)\r\n */\r\nexport const ALLOWED_ENV_KEYS: ReadonlySet<string> = new Set([\r\n  'MAIN_VITE_RELAY_AGENT_SECRET',\r\n  'VIBE_RELAY_AGENT_SECRET',\r\n  'MAIN_VITE_RELAY_URL',\r\n  'VIBE_RELAY_URL',\r\n])\r\n\r\n/**\r\n * cwd 에서 위로 거슬러 올라가며 가장 가까운 `.env` 를 찾는다(없으면 null).\r\n *\r\n * **WHY**: `pnpm --filter @junyoung-kim/reins start` 의 cwd 는 `apps/headless` 라, 모노리포 루트의 `.env`\r\n * 를 찾으려면 상위 탐색이 필요하다(사용자가 패키지 폴더에 `.env` 를 복제하지 않도록). `npx @junyoung-kim/reins`\r\n * 도 프로젝트 하위 어디서 실행하든 그 프로젝트 `.env` 를 찾는다. 로드한 경로는 부팅 로그에 찍혀\r\n * 어떤 `.env` 가 쓰였는지 투명하다.\r\n *\r\n * @param start 탐색 시작 디렉터리(보통 `process.cwd()`).\r\n */\r\nexport function findUpwardEnvFile(start: string): string | null {\r\n  let dir = path.resolve(start)\r\n  for (;;) {\r\n    const candidate = path.join(dir, '.env')\r\n    try {\r\n      fs.accessSync(candidate)\r\n      return candidate\r\n    } catch {\r\n      // 이 레벨엔 없음 — 상위로\r\n    }\r\n    const parent = path.dirname(dir)\r\n    if (parent === dir) return null // 파일시스템 루트 도달\r\n    dir = parent\r\n  }\r\n}\r\n\r\n/**\r\n * `.env` 자동 탐색 경로(앞쪽=가까운 것이 우선). cwd 상위에서 가장 가까운 `.env`(모노리포 루트 포함)\r\n * + 고정 위치 `~/.ai_remote_vibe_agent/.env`(어디서 실행하든 동일).\r\n */\r\nfunction defaultEnvFiles(): string[] {\r\n  const files: string[] = []\r\n  const nearest = findUpwardEnvFile(process.cwd())\r\n  if (nearest) files.push(nearest)\r\n  files.push(path.join(os.homedir(), '.ai_remote_vibe_agent', '.env'))\r\n  return files\r\n}\r\n\r\n/**\r\n * `.env` 파일들을 `process.env` 로 로드한다(의존성 없는 최소 파서).\r\n *\r\n * reins 도 빌드 시 relay 기본값을 박지만(tsup define), 그건 어디까지나 **최종 fallback** 이라 사용자가\r\n * 자기 시크릿/URL 로 오버라이드하려면 env 가 필요하다. 매 실행마다 셸 export 를 치지 않도록 `.env` 를\r\n * 읽되, **이미 설정된 실제 env 는 절대 덮어쓰지 않는다**\r\n * (우선순위: 셸 env > 먼저 로드한 파일 > 나중 파일). 시크릿 값은 로깅하지 않으며, 검증 없이 그대로 주입.\r\n * `allowed` 에 든 키만 주입한다({@link ALLOWED_ENV_KEYS}) — ancestor `.env` 의 임의 env 주입 차단.\r\n *\r\n * @param files 테스트용 경로 주입(생략 시 {@link defaultEnvFiles}).\r\n * @param allowed 주입 허용 키 집합(생략 시 {@link ALLOWED_ENV_KEYS}).\r\n * @returns 실제로 읽어 적용한 파일 경로 목록(부팅 로그용 — 값은 포함하지 않음).\r\n */\r\nexport function loadDotEnv(\r\n  files: string[] = defaultEnvFiles(),\r\n  allowed: ReadonlySet<string> = ALLOWED_ENV_KEYS\r\n): string[] {\r\n  const loaded: string[] = []\r\n  for (const file of files) {\r\n    let text: string\r\n    try {\r\n      text = fs.readFileSync(file, 'utf8')\r\n    } catch {\r\n      continue // 파일 없음 — 무시\r\n    }\r\n    for (const raw of text.split(/\\r?\\n/)) {\r\n      const line = raw.trim().replace(/^export\\s+/, '')\r\n      if (!line || line.startsWith('#')) continue\r\n      const eq = line.indexOf('=')\r\n      if (eq <= 0) continue\r\n      const key = line.slice(0, eq).trim()\r\n      if (!allowed.has(key)) continue // reins 가 소비하는 키만 주입(임의 env 주입 차단)\r\n      if (key in process.env) continue // 셸 env / 먼저 로드한 파일이 우선 — 덮어쓰지 않음\r\n      let val = line.slice(eq + 1).trim()\r\n      // length>=2 가드: 값이 따옴표 1글자(`\"`)뿐일 때 slice(1,-1)가 빈 문자열로 만드는 것 방지.\r\n      if (\r\n        val.length >= 2 &&\r\n        ((val.startsWith('\"') && val.endsWith('\"')) ||\r\n          (val.startsWith(\"'\") && val.endsWith(\"'\")))\r\n      ) {\r\n        val = val.slice(1, -1)\r\n      }\r\n      process.env[key] = val\r\n    }\r\n    loaded.push(file)\r\n  }\r\n  return loaded\r\n}\r\n","/**\n * `reins` 엔트리 라우팅 결정 — 부수효과 없는 순수 함수(cli.tsx 가 import 시 실행하는 부수효과와\n * 분리해 단위테스트 가능하게 한다). \"서비스 자신인가\" 판정은 service 계층의 `isServiceProcess` 가\n * 소유하고, 여기서는 그 결과(boolean)만 소비한다.\n */\n\n/**\n * 진입 모드. cli.tsx 가 이 값으로 데몬/거절/관리/풀TUI 중 하나를 분기한다.\n *\n * - `daemon` — TUI 없이 코어만(systemd 서비스의 주 경로, 또는 서비스 없는 비대화형 실행).\n * - `serviceBusy` — 서비스 가동 중인데 끼어든 다른 non-TTY 호출. 두 번째 코어를 띄우지 않는다.\n * - `manager` — 서비스 가동 중 인터랙티브 재접속(관리/상태 화면, 코어 미기동).\n * - `tui` — 기본 풀스크린 TUI(코어 기동).\n */\nexport type EntryMode = 'daemon' | 'serviceBusy' | 'manager' | 'tui'\n\n/** {@link resolveEntryMode} 입력. */\nexport interface EntryInputs {\n  /** `process.stdout.isTTY` — 서비스/파이프는 항상 false. */\n  isTTY: boolean\n  /** 이 프로세스가 systemd 서비스 자신인지(service 계층 `isServiceProcess` 의 결과). */\n  isServiceProcess: boolean\n  /**\n   * 서비스가 가동 중인지(`systemctl is-active`)를 **지연 평가**하는 thunk.\n   * 마커 보유 데몬(서비스 주 경로)에서는 호출하지 않아 불필요한 spawn·자기참조 조회를 피한다.\n   */\n  isServiceActive: () => boolean\n}\n\n/**\n * TTY 유무를 1차, \"서비스 프로세스 여부\"를 2차 판별자로 진입 모드를 결정한다.\n *\n * non-TTY 가드의 핵심: 서비스 자신도 non-TTY 라 `isServiceActive()` 가 자기 자신을 active 로\n * 오판한다(자기참조). 그래서 \"서비스 자신\"({@link EntryInputs.isServiceProcess})을 먼저 걸러\n * `daemon` 으로 보낸 뒤에야 active 판정을 적용해, 끼어든 다른 non-TTY 호출만 `serviceBusy` 로 막는다.\n */\nexport function resolveEntryMode(i: EntryInputs): EntryMode {\n  if (!i.isTTY) {\n    if (i.isServiceProcess) return 'daemon'\n    return i.isServiceActive() ? 'serviceBusy' : 'daemon'\n  }\n  return i.isServiceActive() ? 'manager' : 'tui'\n}\n","{\n  \"name\": \"@junyoung-kim/reins\",\n  \"version\": \"0.1.4\",\n  \"type\": \"module\",\n  \"description\": \"Keep your AI coding agent on a leash, from your phone — a headless TUI (npx @junyoung-kim/reins)\",\n  \"bin\": {\n    \"reins\": \"dist/cli.mjs\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"engines\": {\n    \"node\": \">=18\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://gitlab.com/juny-glre/ai_remote_vibe_agent.git\",\n    \"directory\": \"apps/headless\"\n  },\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"start\": \"node ./dist/cli.mjs\",\n    \"dev\": \"tsx src/cli.tsx\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"test\": \"vitest run\",\n    \"prepack\": \"tsup\"\n  },\n  \"dependencies\": {\n    \"@xterm/headless\": \"^6.0.0\",\n    \"electron-log\": \"^5.4.3\",\n    \"ink\": \"^5.0.1\",\n    \"ink-select-input\": \"^6.2.0\",\n    \"ink-text-input\": \"^6.0.0\",\n    \"node-pty\": \"^1.1.0\",\n    \"p-limit\": \"^7.3.0\",\n    \"qrcode-terminal\": \"^0.12.0\",\n    \"react\": \"^18.3.1\",\n    \"simple-git\": \"^3.36.0\",\n    \"ssh2\": \"^1.17.0\",\n    \"ws\": \"^8.20.0\"\n  },\n  \"devDependencies\": {\n    \"@arva/core\": \"workspace:*\",\n    \"@arva/shared\": \"workspace:*\",\n    \"@types/node\": \"^24.10.1\",\n    \"@types/react\": \"^18.3.12\",\n    \"ink-testing-library\": \"^4.0.0\",\n    \"tsup\": \"^8.3.5\",\n    \"tsx\": \"^4.21.0\",\n    \"typescript\": \"^5.9.3\",\n    \"vite-tsconfig-paths\": \"^5.1.4\",\n    \"vitest\": \"^4.1.4\"\n  }\n}\n","import type { AgentIdentity } from '@arva/core'\n\nimport pkg from '../package.json'\n\n/**\n * 헤드리스 앱 식별 — cap.ack `agent_version` 에 실린다. 데스크탑(@arva/desktop)과 별개 버전이라\n * 별도로 주입한다(코어는 `~/package.json` 을 직접 읽지 않는다, A3).\n */\nexport const identity: AgentIdentity = { version: pkg.version }\n","import fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\n\nimport type { HeadlessConfigStore } from '../config/json-store'\nimport { computePairUrl } from '../pairing'\nimport { resolveExec } from './platform'\nimport { getServiceBackend } from './resolver'\nimport type { ServiceBackend } from './types'\n\n/**\n * `reins service <action>` 진입점 — 코어/TUI 없이 자동시작 등록만 수행하고 종료코드를 반환한다.\n *\n * 백엔드는 resolver 가 플랫폼별로 고른다(linux=systemd / mac=launchd / win=schtasks / 그 외=none).\n * 미지원 플랫폼은 깨지지 않고 안내 후 1 을 반환한다. 출력은 `console.log`(TUI 렌더 전이라 안전).\n *\n * @returns process exit code.\n */\nexport function runServiceCommand(\n  action: string | undefined,\n  config: HeadlessConfigStore\n): number {\n  const backend = getServiceBackend()\n  if (!backend.isSupported()) {\n    console.log(`reins service: ${backend.supportHint()}`)\n    console.log(`(current platform: ${process.platform})`)\n    return 1\n  }\n  switch (action) {\n    case 'install':\n      return doInstall(backend)\n    case 'uninstall':\n      return doUninstall(backend)\n    case 'status':\n      return doStatus(backend, config)\n    default:\n      console.log('usage: reins service <install | uninstall | status>')\n      return 1\n  }\n}\n\n/** 등록 + 시작(restart) + persists/시크릿 안내. */\nfunction doInstall(backend: ServiceBackend): number {\n  const exec = resolveExec()\n  if (exec.isNpxCache) {\n    console.log('⚠ running from an npx cache path — too ephemeral to use as a service.')\n    console.log('  install globally and try again:')\n    console.log('    npm i -g @junyoung-kim/reins')\n    console.log('    reins service install')\n    return 1\n  }\n\n  const res = backend.install(exec)\n  for (const m of res.messages) console.log(m)\n  if (!res.ok) return 1\n\n  // install 직후 restart 로 시작한다. start 가 아니라 restart 인 이유: 재설치(버전 업)로 실행 경로가\n  // 바뀌어도 이미 active 인 구버전이 새 정의로 교체되도록(단순 start 는 active 면 no-op).\n  if (backend.restart()) console.log('service started')\n  else console.log('warning: service start failed (retry: reins service install)')\n\n  console.log('')\n  if (res.persists) {\n    console.log('✅ auto-start registered — survives reboot and logout.')\n  } else if (res.persistCommand) {\n    console.log('⚠ to keep it running after you disconnect, run this once:')\n    console.log(`    ${res.persistCommand}`)\n  }\n\n  if (!serviceHasSecretOverride()) {\n    console.log('')\n    console.log('ℹ relay secret: connecting with the built-in default (no setup needed).')\n    console.log('  to use your own secret, add it to ~/.ai_remote_vibe_agent/.env:')\n    console.log('  e.g. MAIN_VITE_RELAY_AGENT_SECRET=<your-secret>')\n    console.log('  then restart the reins service.')\n  }\n  return 0\n}\n\n/** 등록 해제. */\nfunction doUninstall(backend: ServiceBackend): number {\n  const res = backend.uninstall()\n  for (const m of res.messages) console.log(m)\n  console.log('auto-start registration removed.')\n  return 0\n}\n\n/** 상태 출력 + (가능하면) 페어링 URL 재계산 노출. */\nfunction doStatus(backend: ServiceBackend, config: HeadlessConfigStore): number {\n  const s = backend.status()\n  console.log(`platform   : ${process.platform} (backend: ${s.backend})`)\n  console.log(`installed  : ${s.installed}`)\n  console.log(`active     : ${s.active}`)\n  console.log(`enabled    : ${s.enabled}`)\n  console.log(`persists   : ${s.persists} (user: ${s.user})`)\n\n  const pairUrl = computePairUrl(config.getRelayUrls())\n  if (pairUrl) console.log(`pair url   : ${pairUrl}`)\n\n  if (s.installed && !s.persists && s.persistCommand) {\n    console.log('')\n    console.log(`⚠ to keep running after logout: ${s.persistCommand}`)\n  }\n  return 0\n}\n\n/**\n * 서비스가 **사용자 지정** relay 시크릿 오버라이드를 갖는지 점검.\n *\n * 시크릿이 비어도 빌드 기본값으로 접속되므로 실패 경고가 아니라 \"기본값으로 동작 + 바꾸는 법\"\n * 안내용. 서비스는 cwd 상위 탐색 `.env` 가 안 잡히고 고정 경로 `~/.ai_remote_vibe_agent/.env`\n * 만 로드하므로(load-env 의 defaultEnvFiles) 이 파일만 본다.\n */\nfunction serviceHasSecretOverride(): boolean {\n  const envFile = path.join(os.homedir(), '.ai_remote_vibe_agent', '.env')\n  try {\n    const text = fs.readFileSync(envFile, 'utf-8')\n    // `=\\s*\\S`: 키만 있고 값이 빈(`KEY=`) 경우를 \"있음\"으로 오판하지 않도록 비어있지 않은 값 요구.\n    return /^(export\\s+)?(MAIN_VITE_RELAY_AGENT_SECRET|VIBE_RELAY_AGENT_SECRET)\\s*=\\s*\\S/m.test(text)\n  } catch {\n    return false\n  }\n}\n","import fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\n\nimport { toPairUrl } from '@arva/core'\n\n/**\n * 영속 세션 토큰 파일 — 코어(`ws-manager`)가 `loadOrCreateSessionToken` 으로 생성·소유한다.\n * 데스크탑·헤드리스가 공유하는 고정 경로(`~/.ai_remote_vibe_agent/session-token.txt`).\n */\nconst SESSION_TOKEN_FILE = path.join(os.homedir(), '.ai_remote_vibe_agent', 'session-token.txt')\n\n/**\n * 영속 세션 토큰을 **읽기만** 한다(없으면 null — 생성하지 않음).\n *\n * 관리 화면은 라이브 코어 없이 동작하므로 코어가 만들어 둔 토큰을 그대로 읽는다. 새로 만들면\n * 서비스가 쓰는 토큰과 달라져 QR 이 어긋나므로, 토큰이 없으면(=서비스 미기동 이력) null 을 반환한다.\n * UUID(36자) 검증으로 깨진 파일을 거른다.\n */\nexport function readSessionToken(): string | null {\n  try {\n    const t = fs.readFileSync(SESSION_TOKEN_FILE, 'utf-8').trim()\n    return t.length === 36 ? t : null\n  } catch {\n    return null\n  }\n}\n\n/**\n * 페어링 URL 을 조립하는 **순수** 부분(단위테스트 대상) — IO 없는 변환만.\n *\n * 코어 `connectToRelay` 와 동일한 변환을 재현한다: primary relay base 의 trailing slash 정규화 →\n * `${base}/mobile/${token}` → {@link toPairUrl}. token/relayUrl 이 없으면 null.\n *\n * @param relayUrls index 0 = primary.\n * @param token     영속 세션 토큰(없으면 null).\n */\nexport function buildPairUrl(relayUrls: string[], token: string | null): string | null {\n  const rawBase = relayUrls[0]\n  if (!token || !rawBase) return null\n  const relayBase = rawBase.replace(/\\/+$/, '')\n  return toPairUrl(`${relayBase}/mobile/${token}`)\n}\n\n/**\n * 라이브 코어 없이 페어링 URL(HTTPS 딥링크)을 **재계산**한다 — 관리/상태 화면용.\n *\n * 토큰은 디스크 영속이라 서비스가 실제 서빙 중인 QR 과 **동일**하다. 토큰 미존재 또는 relay URL\n * 없음 시 null(호출부가 \"준비 안 됨\" 표시).\n *\n * **한계(다중 relay)**: 항상 primary(index 0)를 가정한다. 코어가 fallback relay 로 failover 한\n * 상태면 이 URL 은 죽은 primary 를 가리킬 수 있다 — 실제 활성 relay 는 서비스 프로세스만 안다.\n * 호출부는 relay 가 2개 이상이면 \"primary 가정\" 라벨로 이 가정을 사용자에게 노출한다.\n *\n * @param relayUrls `config.getRelayUrls()` (index 0 = primary).\n */\nexport function computePairUrl(relayUrls: string[]): string | null {\n  return buildPairUrl(relayUrls, readSessionToken())\n}\n","import { spawnSync } from 'node:child_process'\nimport fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\n\n/**\n * 자동 시작 서비스 등록을 지원하는 플랫폼인지.\n *\n * 현재 **리눅스(systemd) 우선만** 지원한다. macOS(launchd)/Windows(Task Scheduler)는 후속 작업이라,\n * 비-linux 에서는 깨지지 않고 \"미지원\" 안내만 하도록 호출부가 이 값을 먼저 확인한다.\n */\nexport function isServiceSupported(): boolean {\n  return process.platform === 'linux'\n}\n\n/** systemctl 가용성 캐시 — 프로세스 수명 동안 불변이라 1회만 spawn 한다(관리 화면 폴링의 중복 제거). */\nlet systemctlUserAvailable: boolean | undefined\n\n/**\n * `systemctl --user` 가용 여부. systemd 미설치(컨테이너·일부 배포판)면 false 라,\n * install 이 깨지지 않고 안내 후 종료할 수 있다. 바이너리 미존재(ENOENT)도 false 로 흡수한다.\n *\n * 결과를 모듈 스코프에 캐시한다 — `serviceStatus()` 가드가 매 호출(관리 화면 폴링 포함)마다\n * `systemctl --version` 을 spawn 하던 것을 첫 1회로 줄인다.\n */\nexport function hasSystemctlUser(): boolean {\n  if (systemctlUserAvailable === undefined) {\n    const r = spawnSync('systemctl', ['--user', '--version'], { stdio: 'ignore' })\n    systemctlUserAvailable = !r.error && r.status === 0\n  }\n  return systemctlUserAvailable\n}\n\n/** linger 안내(`loginctl enable-linger <user>`)에 쓸 현재 사용자명. */\nexport function currentUser(): string {\n  return os.userInfo().username\n}\n\n/** {@link resolveExec} 결과. */\nexport interface ExecResolution {\n  /** node 절대경로. */\n  nodePath: string\n  /** cli 스크립트 절대경로(심링크 해소). */\n  scriptPath: string\n  /** `npx`(npm 캐시) 경로로 실행 중인지 — 서비스 ExecStart 로는 부적합. */\n  isNpxCache: boolean\n}\n\n/**\n * 유닛 `ExecStart` 에 박을 실행 경로를 resolve 한다.\n *\n * `process.execPath`(node) + `process.argv[1]`(cli) 절대경로를 쓴다. 전역 설치 시 bin 심링크를\n * 가리킬 수 있어 `realpath` 로 실제 파일을 고정한다. `npx @junyoung-kim/reins`(npm 캐시 `_npx`)로 실행하면 그\n * 경로가 임시·휘발성이라 서비스로 부적합 → `isNpxCache=true` 로 호출부가 경고하게 한다.\n */\nexport function resolveExec(): ExecResolution {\n  const nodePath = process.execPath\n  const rawScript = process.argv[1] ?? ''\n  let scriptPath = rawScript\n  try {\n    scriptPath = fs.realpathSync(path.resolve(rawScript))\n  } catch {\n    scriptPath = path.resolve(rawScript)\n  }\n  // npm 의 npx 실행은 `.../_npx/<hash>/node_modules/...` 캐시에 풀린다(OS 무관 `_npx` 세그먼트).\n  const isNpxCache = scriptPath.split(path.sep).includes('_npx')\n  return { nodePath, scriptPath, isNpxCache }\n}\n","import fs from 'node:fs'\n\nimport { run } from './exec'\nimport {\n  domainTarget,\n  launchAgentDir,\n  plistPath,\n  renderPlist,\n  serviceTarget,\n} from './launchd-plist'\nimport { currentUser, type ExecResolution } from './platform'\nimport type { InstallResult, ServiceBackend, ServiceStatus, UninstallResult } from './types'\n\n/** `launchctl print` stdout 이 \"실행 중\"을 가리키는지 — status·isActive 공유 판정. */\nfunction launchdIsRunning(printStdout: string): boolean {\n  return /\\bstate = running\\b/.test(printStdout) || /\\bpid = \\d+/.test(printStdout)\n}\n\n/** 에이전트가 launchd 런타임에 로드돼 있는지(KeepAlive 상주 대상). */\nfunction isLoaded(): boolean {\n  return run('launchctl', ['print', serviceTarget()]).ok\n}\n\n/** 지금 실행 중(PID 보유)인지 — cli 단일 인스턴스 가드가 쓰는 단발 조회. */\nexport function launchdActive(): boolean {\n  const r = run('launchctl', ['print', serviceTarget()])\n  return r.ok && launchdIsRunning(r.stdout)\n}\n\n/** 서비스 종합 상태 조회 — `launchctl print` 1회로 active/persists 를 함께 도출(틱당 spawn 절약). */\nexport function launchdStatus(): ServiceStatus {\n  const installed = fs.existsSync(plistPath())\n  const printed = run('launchctl', ['print', serviceTarget()])\n  // LaunchAgent 는 Aqua(로그인) 세션에 속해 SSH 끊김을 견딘다(KeepAlive). 완전 로그아웃/헤드리스\n  // (무 GUI)까지 버티려면 LaunchDaemon(root)이 필요(미자동화). persists=loaded: 로드면 상주로 본다.\n  return {\n    installed,\n    active: printed.ok && launchdIsRunning(printed.stdout),\n    enabled: installed,\n    persists: printed.ok,\n    persistCommand: '',\n    backend: 'launchd',\n    user: currentUser(),\n  }\n}\n\n/**\n * plist 작성 + `enable`(로그인 자동 로드) — **시작은 하지 않는다**(systemd 와 동일: 포그라운드\n * 코어와 같은 포트로 충돌하는 footgun 방지). 즉시 기동은 호출부가 {@link launchdStart}/restart 로.\n */\nexport function launchdInstall(exec: ExecResolution): InstallResult {\n  const messages: string[] = []\n  const file = plistPath()\n  fs.mkdirSync(launchAgentDir(), { recursive: true })\n  fs.writeFileSync(file, renderPlist({ nodePath: exec.nodePath, scriptPath: exec.scriptPath }), {\n    mode: 0o644,\n  })\n  messages.push(`plist written: ${file}`)\n\n  // disabled 오버라이드를 풀어 로그인 시 자동 로드되게 한다(이미 enabled 면 no-op).\n  const enable = run('launchctl', ['enable', serviceTarget()])\n  if (enable.ok) messages.push('agent enabled (login)')\n  messages.push('runs while logged into macOS (survives SSH disconnect).')\n  messages.push('headless Mac with no GUI login needs a system LaunchDaemon (sudo) — not yet automated.')\n\n  // persists=false 로 두어 install 직후엔 \"상주 보장\"을 주장하지 않는다(restart 로 로드되면 status 가\n  // persists=true 로 보고). persistCommand 는 빈 문자열(자동화된 1줄 명령이 없음 — 위 안내로 대체).\n  return { ok: true, persists: false, persistCommand: '', messages }\n}\n\n/** bootout(언로드) + plist 삭제. 이미 없는 상태에도 안전(멱등). */\nexport function launchdUninstall(): UninstallResult {\n  const messages: string[] = []\n  const out = run('launchctl', ['bootout', serviceTarget()])\n  if (!out.ok && out.stderr) messages.push(`warning: bootout: ${out.stderr}`)\n  try {\n    fs.rmSync(plistPath())\n    messages.push('plist removed')\n  } catch {\n    // 이미 없음 — 무시\n  }\n  return { ok: true, messages }\n}\n\n/** 로드 상태면 kickstart(`-k` 면 kill 후 재기동), 아니면 bootstrap(RunAtLoad 로 기동). start/restart 공유. */\nfunction launchdKick(kill: boolean): boolean {\n  if (isLoaded()) {\n    return run('launchctl', ['kickstart', ...(kill ? ['-k'] : []), serviceTarget()]).ok\n  }\n  return run('launchctl', ['bootstrap', domainTarget(), plistPath()]).ok\n}\n\n/** 시작. */\nexport function launchdStart(): boolean {\n  return launchdKick(false)\n}\n\n/** 재시작(kill 후 재기동). */\nexport function launchdRestart(): boolean {\n  return launchdKick(true)\n}\n\n/** 중지 — bootout(언로드). plist 는 남아 다음 로그인에 다시 로드된다. */\nexport function launchdStop(): boolean {\n  return run('launchctl', ['bootout', serviceTarget()]).ok\n}\n\n/**\n * macOS launchd LaunchAgent 백엔드 — resolver 가 darwin 에서 반환한다.\n *\n * LaunchAgent(`~/Library/LaunchAgents`)는 user Aqua 세션에 속해 SSH 끊김을 견딘다. 완전 헤드리스\n * (무 GUI 로그인)는 LaunchDaemon(root)이 필요하나 본 단계 범위 밖(안내만).\n */\nexport const launchdBackend: ServiceBackend = {\n  kind: 'launchd',\n  isSupported: () => process.platform === 'darwin',\n  supportHint: () => 'launchd is only available on macOS.',\n  status: launchdStatus,\n  isActive: launchdActive,\n  install: launchdInstall,\n  uninstall: launchdUninstall,\n  start: launchdStart,\n  stop: launchdStop,\n  restart: launchdRestart,\n}\n","import { spawnSync } from 'node:child_process'\n\n/** 자식 프로세스 1회 실행 결과(systemctl/launchctl/schtasks/loginctl/powershell 공유). */\nexport interface ExecResult {\n  /** spawn 실패 없음 + 종료코드 0. */\n  ok: boolean\n  stdout: string\n  stderr: string\n}\n\n/**\n * 서비스 백엔드가 쓰는 단일 `spawnSync` 래퍼 — **배열 인자·`shell:false`**(셸 escape 불필요).\n *\n * 세 백엔드가 같은 \"성공 판정(`!error && status===0`) + stdout/stderr trim\" 규칙을 한곳에서 쓰게\n * 한다. 향후 timeout·PATH 보강(CLAUDE.md: GUI 앱 spawn 시 login PATH 누락) 같은 cross-cutting\n * 변경이 한 군데에만 들어가도록 모은다. 바이너리 미존재(ENOENT)·비정상 종료는 `ok=false` 로 흡수.\n */\nexport function run(cmd: string, args: string[]): ExecResult {\n  const r = spawnSync(cmd, args, { encoding: 'utf-8' })\n  return {\n    ok: !r.error && r.status === 0,\n    stdout: (r.stdout ?? '').trim(),\n    stderr: (r.stderr ?? '').trim(),\n  }\n}\n","import os from 'node:os'\nimport path from 'node:path'\n\nimport { SERVICE_ENV_KEY, SERVICE_ENV_VALUE } from './service-process'\n\n/**\n * launchd 서비스 라벨(reverse-DNS). plist 파일명·`launchctl` 서비스 타깃·자기식별이 공유한다.\n * npm 스코프(@junyoung-kim)를 반영해 충돌을 피한다.\n */\nexport const LAUNCHD_LABEL = 'com.junyoung-kim.reins'\n\n/** user LaunchAgent 디렉터리(`~/Library/LaunchAgents`). */\nexport function launchAgentDir(): string {\n  return path.join(os.homedir(), 'Library', 'LaunchAgents')\n}\n\n/** plist 파일 절대경로. */\nexport function plistPath(): string {\n  return path.join(launchAgentDir(), `${LAUNCHD_LABEL}.plist`)\n}\n\n/** 현재 uid — launchd gui 도메인 타깃에 필요. mac 외엔 os.userInfo() fallback. */\nexport function currentUid(): number {\n  return process.getuid?.() ?? os.userInfo().uid\n}\n\n/** launchctl 도메인 타깃(`gui/<uid>`) — bootstrap 대상. */\nexport function domainTarget(): string {\n  return `gui/${currentUid()}`\n}\n\n/** launchctl 서비스 타깃(`gui/<uid>/<label>`) — print/kickstart/bootout 대상. */\nexport function serviceTarget(): string {\n  return `${domainTarget()}/${LAUNCHD_LABEL}`\n}\n\n/** plist 문자열 값에 들어갈 경로의 XML 특수문자 escape(경로에 `&`·공백 등이 와도 안전). */\nfunction escapeXml(s: string): string {\n  return s\n    .replace(/&/g, '&amp;')\n    .replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;')\n    .replace(/\"/g, '&quot;')\n    .replace(/'/g, '&apos;')\n}\n\n/**\n * LaunchAgent plist 텍스트를 생성한다(부수효과 없는 순수 함수 — 단위테스트 대상).\n *\n * - `RunAtLoad`+`KeepAlive`=true: 로그인 시 자동 시작 + 크래시/종료 후 자동 복구(systemd\n *   `Restart=always` 등가).\n * - `EnvironmentVariables` 에 {@link SERVICE_ENV_KEY} 마커를 박는다 — cli 의 non-TTY 가드가\n *   \"이 프로세스가 서비스 자신\"임을 식별해 자기참조(플래핑)를 피한다(systemd 유닛 `Environment=`\n *   와 동일 계약, 단일 출처는 service-process).\n * - 로그는 `~/.ai_remote_vibe_agent/logs/launchd.{out,err}.log` 로(데몬은 TTY 가 없으므로 파일로).\n *\n * 경로는 XML escape 한다(공백/특수문자 안전). relay 시크릿은 plist 에 박지 않는다(앱이\n * `~/.ai_remote_vibe_agent/.env` 자동 로드).\n */\nexport function renderPlist(p: { nodePath: string; scriptPath: string }): string {\n  const logDir = path.join(os.homedir(), '.ai_remote_vibe_agent', 'logs')\n  const outLog = path.join(logDir, 'launchd.out.log')\n  const errLog = path.join(logDir, 'launchd.err.log')\n  const args = [p.nodePath, p.scriptPath]\n    .map(a => `    <string>${escapeXml(a)}</string>`)\n    .join('\\n')\n  return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n  <key>Label</key>\n  <string>${LAUNCHD_LABEL}</string>\n  <key>ProgramArguments</key>\n  <array>\n${args}\n  </array>\n  <key>EnvironmentVariables</key>\n  <dict>\n    <key>${SERVICE_ENV_KEY}</key>\n    <string>${SERVICE_ENV_VALUE}</string>\n  </dict>\n  <key>RunAtLoad</key>\n  <true/>\n  <key>KeepAlive</key>\n  <true/>\n  <key>StandardOutPath</key>\n  <string>${escapeXml(outLog)}</string>\n  <key>StandardErrorPath</key>\n  <string>${escapeXml(errLog)}</string>\n</dict>\n</plist>\n`\n}\n","/**\n * \"이 프로세스가 자동시작 서비스 자신인가\" 판정 + 서비스 정의에 박는 환경 마커(전 백엔드 공통).\n *\n * 자기식별 마커 계약을 한곳(service/)에 둔다: 마커를 **쓰는 쪽** 세 곳(systemd `unit-template` 의\n * `Environment=`, launchd `launchd-plist` 의 `EnvironmentVariables`, win `schtasks-args` 의 `.cmd`\n * `set`)과 **읽는 쪽**({@link isServiceProcess})이 같은 KEY/VALUE 상수를 공유해, 이름·값을 바꿔도\n * 한 곳만 고치면 되고 컴파일러가 양쪽을 묶어준다(드리프트 시 가드가 조용히 깨지는 것을 방지 —\n * 드리프트는 서비스 자기참조 → 플래핑으로 이어진다). 새 백엔드는 반드시 이 마커를 주입해야 한다.\n */\n\n/** 서비스 마커 env 키. 세 백엔드의 서비스 정의와 {@link isServiceProcess} 가 공유. */\nexport const SERVICE_ENV_KEY = 'REINS_RUN_AS_SERVICE'\n\n/** 서비스 마커 env 값. */\nexport const SERVICE_ENV_VALUE = '1'\n\n/** 유닛 `Environment=` 한 줄로 박는 `KEY=VALUE` 문자열(unit-template 가 사용). */\nexport const SERVICE_ENV_MARKER = `${SERVICE_ENV_KEY}=${SERVICE_ENV_VALUE}`\n\n/**\n * 이 프로세스가 자동시작 서비스로 실행됐는지 — cli 진입 가드의 자기참조 방지 신호(전 백엔드 공통).\n *\n * - `REINS_RUN_AS_SERVICE=1`({@link SERVICE_ENV_KEY}): 서비스 정의(systemd 유닛/launchd plist/win .cmd)\n *   가 주입하는 명시 마커(신규 설치의 1차 신호, 플랫폼 무관).\n * - `INVOCATION_ID`: systemd 가 모든 유닛 프로세스에 주입하는 표준 env(**linux 전용**). **전이기(transitional)\n *   fallback** — 마커 없는 구 유닛이 재설치 없이 새 코드로 돌아도 서비스 자신을 식별해 자가치유한다.\n *   구 유닛을 `reins service install` 로 재설치해 명시 마커를 받으면 더는 의존하지 않는다. 마커보다\n *   느슨한 신호(\"어떤 systemd 유닛 프로세스\")라 호출부는 **non-TTY 분기에서만** 사용한다 —\n *   인터랙티브 셸(TTY)이 `INVOCATION_ID` 를 물려받아도 TTY 분기로 가므로 데몬 오라우팅이 불가능하다.\n */\nexport function isServiceProcess(env: NodeJS.ProcessEnv): boolean {\n  return env[SERVICE_ENV_KEY] === SERVICE_ENV_VALUE || !!env.INVOCATION_ID\n}\n","import { currentUser } from './platform'\nimport type { ServiceBackend, ServiceStatus } from './types'\n\n/** 미지원 플랫폼에서 status() 가 돌려줄 고정 상태(전부 false). */\nfunction noneStatus(): ServiceStatus {\n  return {\n    installed: false,\n    active: false,\n    enabled: false,\n    persists: false,\n    persistCommand: '',\n    backend: 'none',\n    user: currentUser(),\n  }\n}\n\n/**\n * 미지원 플랫폼용 no-op 백엔드 — resolver 가 아직 백엔드가 없는 OS 에 매핑한다.\n *\n * 모든 변경 작업(install/start/…)은 깨지지 않고 false 를 반환하고, 호출부(runServiceCommand·\n * MainScreen 토글)는 `isSupported()===false` 를 먼저 보고 {@link ServiceBackend.supportHint} 안내만\n * 출력한다(릴리즈 규칙: 플랫폼 차이는 명시적으로).\n */\nexport const noneBackend: ServiceBackend = {\n  kind: 'none',\n  isSupported: () => false,\n  supportHint: () => `auto-start service is not supported on ${process.platform} yet.`,\n  status: noneStatus,\n  isActive: () => false,\n  install: () => ({\n    ok: false,\n    persists: false,\n    persistCommand: '',\n    messages: ['unsupported platform'],\n  }),\n  uninstall: () => ({ ok: true, messages: [] }),\n  start: () => false,\n  stop: () => false,\n  restart: () => false,\n}\n","import fs from 'node:fs'\nimport path from 'node:path'\n\nimport { run } from './exec'\nimport { currentUser, type ExecResolution } from './platform'\nimport {\n  createArgs,\n  deleteArgs,\n  endArgs,\n  renderServiceCmd,\n  runArgs,\n  serviceCmdPath,\n  statePsArgs,\n} from './schtasks-args'\nimport type { InstallResult, ServiceBackend, ServiceStatus, UninstallResult } from './types'\n\n/** 작업 State enum(`Running`/`Ready`/`Disabled`) 조회 — 없으면 ''(로케일 독립, schtasks-args 참조). */\nfunction queryState(): string {\n  return run('powershell', statePsArgs()).stdout\n}\n\n/** 지금 실행 중인지 — State==='Running'. */\nexport function schtasksActive(): boolean {\n  return queryState() === 'Running'\n}\n\n/** 서비스 종합 상태 조회. */\nexport function schtasksStatus(): ServiceStatus {\n  const state = queryState()\n  const installed = state !== ''\n  // ONLOGON 작업은 사용자 세션에서 돌아 SSH 끊김을 견딘다. 완전 무 로그인(헤드리스)까지 버티려면\n  // /RU SYSTEM /SC ONSTART(관리자)가 필요하다(미자동화). persists=running: 떠 있으면 SSH 끊김 생존.\n  return {\n    installed,\n    active: state === 'Running',\n    enabled: installed && state !== 'Disabled',\n    persists: state === 'Running',\n    persistCommand: '',\n    backend: 'schtasks',\n    user: currentUser(),\n  }\n}\n\n/**\n * 래퍼 .cmd 작성 + `schtasks /Create`(ONLOGON) — **시작은 하지 않는다**(systemd/launchd 와 동일:\n * 포그라운드 코어와 포트 충돌 footgun 방지). 즉시 기동은 호출부가 {@link schtasksStart}/restart 로.\n */\nexport function schtasksInstall(exec: ExecResolution): InstallResult {\n  const messages: string[] = []\n  const cmdPath = serviceCmdPath()\n  fs.mkdirSync(path.dirname(cmdPath), { recursive: true })\n  fs.writeFileSync(cmdPath, renderServiceCmd({ nodePath: exec.nodePath, scriptPath: exec.scriptPath }))\n  messages.push(`wrapper written: ${cmdPath}`)\n\n  const create = run('schtasks', createArgs(cmdPath))\n  if (create.ok) messages.push('scheduled task created (at logon)')\n  else messages.push(`error: schtasks create failed: ${create.stderr}`)\n  messages.push('runs at logon (survives SSH disconnect while you are logged in).')\n  messages.push(\n    'headless (no interactive login) needs /RU SYSTEM /SC ONSTART in an elevated console — not yet automated.'\n  )\n\n  // persists=false 로 두어 install 직후엔 \"상주 보장\"을 주장하지 않는다(restart 로 기동되면 status 가\n  // persists=true 로 보고). persistCommand 는 빈 문자열(자동화된 1줄 명령 없음 — 위 안내로 대체).\n  return { ok: create.ok, persists: false, persistCommand: '', messages }\n}\n\n/** `schtasks /Delete` + 래퍼 .cmd 삭제. 이미 없는 상태에도 안전(멱등). */\nexport function schtasksUninstall(): UninstallResult {\n  const messages: string[] = []\n  const del = run('schtasks', deleteArgs())\n  if (!del.ok && del.stderr) messages.push(`warning: delete: ${del.stderr}`)\n  else if (del.ok) messages.push('scheduled task removed')\n  try {\n    fs.rmSync(serviceCmdPath())\n    messages.push('wrapper removed')\n  } catch {\n    // 이미 없음 — 무시\n  }\n  return { ok: true, messages }\n}\n\n/** 시작(`/Run`). */\nexport function schtasksStart(): boolean {\n  return run('schtasks', runArgs()).ok\n}\n\n/** 재시작 — 실행 중 인스턴스를 끝내고(/End, 실패 무시) 다시 시작(/Run). */\nexport function schtasksRestart(): boolean {\n  run('schtasks', endArgs())\n  return run('schtasks', runArgs()).ok\n}\n\n/** 중지(`/End`). 작업 등록은 유지돼 다음 로그인에 다시 시작된다. */\nexport function schtasksStop(): boolean {\n  return run('schtasks', endArgs()).ok\n}\n\n/**\n * Windows Task Scheduler 백엔드 — resolver 가 win32 에서 반환한다.\n *\n * ONLOGON 사용자 작업(관리자 불요)이라 사용자 세션에서 돌고 SSH 끊김을 견딘다. 완전 헤드리스\n * (무 로그인)와 크래시 자동 재시작(Restart=always 등가)은 본 단계 범위 밖(안내/후속).\n */\nexport const schtasksBackend: ServiceBackend = {\n  kind: 'schtasks',\n  isSupported: () => process.platform === 'win32',\n  supportHint: () => 'Task Scheduler is only available on Windows.',\n  status: schtasksStatus,\n  isActive: schtasksActive,\n  install: schtasksInstall,\n  uninstall: schtasksUninstall,\n  start: schtasksStart,\n  stop: schtasksStop,\n  restart: schtasksRestart,\n}\n","import os from 'node:os'\nimport path from 'node:path'\n\nimport { SERVICE_ENV_KEY, SERVICE_ENV_VALUE } from './service-process'\n\n/** 스케줄 작업명 — `schtasks` 변경/조회와 자기식별이 공유. */\nexport const TASK_NAME = 'reins'\n\n/**\n * 작업이 실행할 래퍼 `.cmd` 파일 경로(`~/.ai_remote_vibe_agent/reins-service.cmd`).\n *\n * 래퍼를 쓰는 이유: `schtasks /TR` 은 환경변수를 직접 박지 못한다. 자기식별 마커\n * ({@link SERVICE_ENV_KEY})를 .cmd 안에서 `set` 한 뒤 node 를 띄워, 인라인 cmd 따옴표 지옥과\n * 경로 공백 문제를 함께 피한다(.cmd 안에서 경로를 따옴표로 감싼다).\n */\nexport function serviceCmdPath(): string {\n  return path.join(os.homedir(), '.ai_remote_vibe_agent', 'reins-service.cmd')\n}\n\n/**\n * 래퍼 `.cmd` 텍스트를 생성한다(부수효과 없는 순수 함수 — 단위테스트 대상).\n *\n * `set \"KEY=VALUE\"` 따옴표 형태를 쓰는 이유: `set KEY=1 && ...` 는 값에 후행 공백이 섞여\n * `\"1 \"` 가 되는 cmd.exe 함정이 있어, 따옴표 형태가 정확히 `1` 만 설정한다. node/script 경로도\n * 따옴표로 감싸 공백을 견딘다(Windows 경로는 `\"` 를 포함할 수 없어 escape 불필요).\n */\nexport function renderServiceCmd(p: { nodePath: string; scriptPath: string }): string {\n  return [\n    '@echo off',\n    `set \"${SERVICE_ENV_KEY}=${SERVICE_ENV_VALUE}\"`,\n    `\"${p.nodePath}\" \"${p.scriptPath}\"`,\n    '',\n  ].join('\\r\\n')\n}\n\n/**\n * `schtasks /Create` 인자 배열(`shell:false` spawn 용).\n *\n * - `/SC ONLOGON`: 로그인 시 자동 시작(사용자 세션 — SSH 끊김과 무관하게 상주).\n * - `/F`: 재설치 시 기존 작업 덮어쓰기.\n * - `/RL LIMITED`: 관리자 권한 불요(ONLOGON 사용자 작업의 기본). HIGHEST 는 관리자 필요라 피한다.\n * - `/TR` 값은 **따옴표를 직접 붙이지 않는다** — `spawnSync(shell:false)` 가 공백 포함 경로를\n *   자동으로 인용한다. 수동 `\"...\"` 를 더하면 Node 가 그 따옴표를 backslash-escape 해서 작업의\n *   \"Task To Run\" 에 따옴표 문자가 박히는 취약한 형태가 된다(특히 사용자 폴더에 공백이 있을 때).\n */\nexport function createArgs(cmdPath: string): string[] {\n  return [\n    '/Create',\n    '/TN',\n    TASK_NAME,\n    '/TR',\n    cmdPath,\n    '/SC',\n    'ONLOGON',\n    '/F',\n    '/RL',\n    'LIMITED',\n  ]\n}\n\n/** `schtasks /Delete` 인자(멱등 — `/F` 로 확인 프롬프트 없음). */\nexport function deleteArgs(): string[] {\n  return ['/Delete', '/TN', TASK_NAME, '/F']\n}\n\n/** `schtasks /Run` 인자(지금 시작). */\nexport function runArgs(): string[] {\n  return ['/Run', '/TN', TASK_NAME]\n}\n\n/** `schtasks /End` 인자(실행 중 인스턴스 종료 — 작업 등록은 유지). */\nexport function endArgs(): string[] {\n  return ['/End', '/TN', TASK_NAME]\n}\n\n/**\n * 작업 상태를 **로케일 독립적으로** 얻는 PowerShell 인자.\n *\n * `schtasks /Query` 의 Status 텍스트는 OS 언어로 번역돼(예: 한국어 \"실행 중\") 파싱이 깨진다.\n * `Get-ScheduledTask` 의 `State` 는 enum(`Running`/`Ready`/`Disabled`)이라 번역되지 않아 안전하다.\n * 작업이 없으면 빈 출력.\n */\nexport function statePsArgs(): string[] {\n  return [\n    '-NoProfile',\n    '-NonInteractive',\n    '-Command',\n    `(Get-ScheduledTask -TaskName '${TASK_NAME}' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty State)`,\n  ]\n}\n","import fs from 'node:fs'\n\nimport { currentUser, type ExecResolution } from './platform'\nimport {\n  isServiceActive,\n  lingerCommand,\n  lingerEnabled,\n  systemctlUser,\n  systemdSupported,\n} from './systemd-cli'\nimport type { InstallResult, ServiceBackend, ServiceStatus, UninstallResult } from './types'\nimport { renderUnit, UNIT_FILE_NAME, userUnitDir, userUnitPath } from './unit-template'\n\n/** 서비스 종합 상태 조회(관리 화면·status 공유). 비지원 플랫폼은 supported=false 로 단락. */\nexport function serviceStatus(): ServiceStatus {\n  const user = currentUser()\n  const installed = fs.existsSync(userUnitPath())\n  if (!systemdSupported()) {\n    return {\n      installed,\n      active: false,\n      enabled: false,\n      persists: false,\n      persistCommand: lingerCommand(user),\n      backend: 'systemd',\n      user,\n    }\n  }\n  // systemd 의 \"세션 종료 후 유지\" = linger. off 면 1회 root 명령으로 켠다.\n  const { persists, persistCommand } = persistFields(user)\n  return {\n    installed,\n    active: isServiceActive(),\n    enabled: systemctlUser(['is-enabled', UNIT_FILE_NAME]).stdout === 'enabled',\n    persists,\n    persistCommand,\n    backend: 'systemd',\n    user,\n  }\n}\n\n/** linger 상태를 일반화 필드(persists/persistCommand)로 — status·install 반환점이 공유. */\nfunction persistFields(user: string): { persists: boolean; persistCommand: string } {\n  const persists = lingerEnabled(user)\n  return { persists, persistCommand: persists ? '' : lingerCommand(user) }\n}\n\n/**\n * user 유닛을 작성하고 부팅 자동 시작으로 `enable` 한다(**시작은 하지 않음** — {@link startService} 분리).\n *\n * 시작을 분리하는 이유: 포그라운드 TUI 에서 등록할 때 즉시 `--now` 로 띄우면 포그라운드 코어와\n * 서비스 코어가 같은 포트·같은 install_id 로 충돌한다(두 번째 인스턴스 footgun). 호출부가\n * 포그라운드 정리(`core.cleanup()`) 뒤에 {@link startService} 를 부르도록 시점을 분리한다.\n * CLI `reins service install` 은 포그라운드가 없으므로 install 직후 바로 start 한다.\n *\n * relay 시크릿은 유닛에 박지 않는다(앱이 `~/.ai_remote_vibe_agent/.env` 자동 로드).\n * linger 는 root 권한이 필요해 여기서 켜지 않고, 현재 상태만 담아 호출부가 안내하게 한다.\n */\nexport function installService(exec: ExecResolution): InstallResult {\n  const user = currentUser()\n  const unitPath = userUnitPath()\n  const messages: string[] = []\n\n  fs.mkdirSync(userUnitDir(), { recursive: true })\n  fs.writeFileSync(\n    unitPath,\n    renderUnit({ nodePath: exec.nodePath, scriptPath: exec.scriptPath }),\n    { mode: 0o644 }\n  )\n  messages.push(`unit written: ${unitPath}`)\n\n  const reload = systemctlUser(['daemon-reload'])\n  if (!reload.ok) {\n    // reload 실패 시 enable 이 (디스크의 새 유닛이 아닌) stale 정의를 잡을 수 있어, \"성공\"으로\n    // 보고하면 안 된다 — fatal 처리해 호출부가 재시도하게 한다.\n    messages.push(`error: daemon-reload failed: ${reload.stderr}`)\n    return { ok: false, ...persistFields(user), messages }\n  }\n\n  const enable = systemctlUser(['enable', UNIT_FILE_NAME])\n  if (enable.ok) messages.push('service enabled (boot)')\n  else messages.push(`error: enable failed: ${enable.stderr}`)\n\n  return { ok: enable.ok, ...persistFields(user), messages }\n}\n\n/** 서비스 시작(`start`). {@link installService} 로 enable 한 뒤 또는 핸드오프 종료 시 호출. */\nexport function startService(): boolean {\n  return systemctlUser(['start', UNIT_FILE_NAME]).ok\n}\n\n/** `disable --now` + 유닛 삭제 + reload. 이미 없는 상태에도 안전(멱등). */\nexport function uninstallService(): UninstallResult {\n  const messages: string[] = []\n  const disable = systemctlUser(['disable', '--now', UNIT_FILE_NAME])\n  if (!disable.ok && disable.stderr) messages.push(`warning: disable: ${disable.stderr}`)\n  try {\n    fs.rmSync(userUnitPath())\n    messages.push('unit removed')\n  } catch {\n    // 이미 없음 — 무시\n  }\n  systemctlUser(['daemon-reload'])\n  return { ok: true, messages }\n}\n\n/** 서비스 재시작(관리 화면 `r`). */\nexport function restartService(): boolean {\n  return systemctlUser(['restart', UNIT_FILE_NAME]).ok\n}\n\n/** 서비스 중지(관리 화면 `x`). */\nexport function stopService(): boolean {\n  return systemctlUser(['stop', UNIT_FILE_NAME]).ok\n}\n\n/**\n * linux systemd user 서비스 백엔드 — resolver 가 linux 에서 반환한다.\n *\n * 메서드는 위 함수들을 그대로 위임한다(인터페이스 어댑터). `restart` 가 install 직후 start 역할도\n * 겸하므로(버전 교체 보장), 호출부는 start/restart 를 구분해 쓴다.\n */\nexport const systemdBackend: ServiceBackend = {\n  kind: 'systemd',\n  isSupported: systemdSupported,\n  supportHint: () =>\n    '`systemctl --user` not found (Linux systemd user instance required).',\n  status: serviceStatus,\n  isActive: isServiceActive,\n  install: installService,\n  uninstall: uninstallService,\n  start: startService,\n  stop: stopService,\n  restart: restartService,\n}\n","import os from 'node:os'\nimport path from 'node:path'\n\nimport { SERVICE_ENV_MARKER } from './service-process'\n\n/**\n * systemd **user** 서비스 식별자. install 한 유닛명(`reins.service`)으로\n * `systemctl --user`·`cli.tsx` 단일 인스턴스 감지가 모두 이 상수를 공유한다.\n */\nexport const SERVICE_NAME = 'reins'\n\n/** 유닛 파일명(`reins.service`). */\nexport const UNIT_FILE_NAME = `${SERVICE_NAME}.service`\n\n/** {@link renderUnit} 입력 — install 시점에 resolve 한 실행 경로(둘 다 절대경로). */\nexport interface UnitParams {\n  /** node 절대경로(`process.execPath`). */\n  nodePath: string\n  /** 번들된 cli 절대경로(`dist/cli.mjs`의 realpath). */\n  scriptPath: string\n}\n\n/**\n * systemd user 유닛 파일 텍스트를 생성한다(부수효과 없는 순수 함수 — 단위테스트 대상).\n *\n * - `Restart=always`/`RestartSec=3`: 크래시·OOM 후 자동 복구(클라우드 상시 구동 목적).\n * - `After/Wants=network-online.target`: relay 접속이 네트워크 준비 후 시작되도록.\n * - `Environment=REINS_RUN_AS_SERVICE=1`: **시크릿이 아니라 \"이 프로세스가 서비스 자신\"** 임을\n *   표시하는 마커. cli.tsx 의 non-TTY 가드가 자기참조(서비스가 자신을 active 로 오판)를 피해\n *   다른 non-TTY 호출만 거절하도록 한다. relay 시크릿은 **유닛에 박지 않는다** — 앱이\n *   `~/.ai_remote_vibe_agent/.env` 를 자동 로드하고, user 서비스라 `HOME` 이 설정돼 경로가 해석된다.\n *\n * ExecStart 경로에 공백이 있으면 systemd 가 토큰을 잘못 분리하므로 각 경로를 `\"`로 감싼다.\n */\nexport function renderUnit(p: UnitParams): string {\n  const exec = `${quoteIfNeeded(p.nodePath)} ${quoteIfNeeded(p.scriptPath)}`\n  return `${[\n    '[Unit]',\n    'Description=reins — AI remote vibe agent (headless)',\n    'After=network-online.target',\n    'Wants=network-online.target',\n    '',\n    '[Service]',\n    'Type=simple',\n    `Environment=${SERVICE_ENV_MARKER}`,\n    `ExecStart=${exec}`,\n    'Restart=always',\n    'RestartSec=3',\n    '',\n    '[Install]',\n    'WantedBy=default.target',\n  ].join('\\n')}\\n`\n}\n\n/** 공백 포함 경로만 systemd 더블쿼트로 감싼다(공백 없으면 원본 — 일반 케이스 가독성). */\nfunction quoteIfNeeded(p: string): string {\n  return /\\s/.test(p) ? `\"${p}\"` : p\n}\n\n/** user 유닛 디렉터리(`~/.config/systemd/user`). */\nexport function userUnitDir(): string {\n  return path.join(os.homedir(), '.config', 'systemd', 'user')\n}\n\n/** user 유닛 파일 절대경로. */\nexport function userUnitPath(): string {\n  return path.join(userUnitDir(), UNIT_FILE_NAME)\n}\n","import { type ExecResult, run } from './exec'\nimport { hasSystemctlUser, isServiceSupported } from './platform'\nimport { UNIT_FILE_NAME } from './unit-template'\n\n/** `systemctl --user <args>` — 공유 {@link run} 에 `--user` 를 덧붙인 얇은 래퍼. */\nexport function systemctlUser(args: string[]): ExecResult {\n  return run('systemctl', ['--user', ...args])\n}\n\n/**\n * 서비스가 가동 중인지 — `cli.tsx` 단일 인스턴스 가드의 진입 판정.\n *\n * `is-active` 는 비활성 시 비정상 종료코드를 내므로 종료코드가 아닌 **stdout==='active'** 로 본다.\n * 비-linux·systemctl 미존재는 false(=가드 비활성, 풀 TUI 진행).\n */\nexport function isServiceActive(): boolean {\n  if (process.platform !== 'linux') return false\n  return systemctlUser(['is-active', UNIT_FILE_NAME]).stdout === 'active'\n}\n\n/** 해당 사용자 linger 활성 여부(`loginctl show-user`). loginctl 미존재 시 false. */\nexport function lingerEnabled(user: string): boolean {\n  return run('loginctl', ['show-user', user, '--property=Linger']).stdout.includes('Linger=yes')\n}\n\n/** SSH 로그아웃 후 유지를 위해 사용자가 1회 실행할 root 명령(순수 문자열). */\nexport function lingerCommand(user: string): string {\n  return `sudo loginctl enable-linger ${user}`\n}\n\n/** systemd 백엔드 가용성 — linux + `systemctl --user` 둘 다 있어야 한다. */\nexport function systemdSupported(): boolean {\n  return isServiceSupported() && hasSystemctlUser()\n}\n","import { launchdBackend } from './launchd'\nimport { noneBackend } from './none'\nimport { schtasksBackend } from './schtasks'\nimport { systemdBackend } from './systemd'\nimport type { ServiceBackend } from './types'\n\n/**\n * `process.platform` → 자동시작 백엔드 1개를 고른다(순수 — 단위테스트 대상).\n *\n * 지원: linux=systemd / darwin=launchd / win32=schtasks. 그 외는 {@link noneBackend} 로 떨어져\n * \"미지원\" 안내만 한다.\n */\nexport function resolveServiceBackend(platform: NodeJS.Platform): ServiceBackend {\n  switch (platform) {\n    case 'linux':\n      return systemdBackend\n    case 'darwin':\n      return launchdBackend\n    case 'win32':\n      return schtasksBackend\n    default:\n      return noneBackend\n  }\n}\n\n/** 프로세스 수명 동안 불변(플랫폼은 안 바뀜)이라 1회만 resolve 후 캐시. */\nlet cached: ServiceBackend | undefined\n\n/**\n * 현재 플랫폼의 자동시작 백엔드. cli/UI 의 편의 래퍼(index)가 이걸 통해 백엔드에 위임한다.\n */\nexport function getServiceBackend(): ServiceBackend {\n  if (!cached) cached = resolveServiceBackend(process.platform)\n  return cached\n}\n","import { EventEmitter } from 'node:events'\r\n\r\nimport type {\r\n  ConnectionDetails,\r\n  HostConflictReason,\r\n  Machine,\r\n  ServerMessage,\r\n} from '@arva/shared/types'\r\n\r\n/**\r\n * PTY 운영 상태 — 코어 `pty_status.status` 에 바인딩(폰 attach 전 미관측은 'idle' 로 표기).\r\n *\r\n * 손수 적은 리터럴 유니온이 아니라 프로토콜에서 추출 — shared 가 상태를 추가/변경하면 여기서도\r\n * 타입이 즉시 깨져 드리프트를 막는다.\r\n */\r\nexport type PtyState = Extract<ServerMessage, { type: 'pty_status' }>['status']\r\n\r\n/** 세션 카드용 머신 요약(이름 표시 + PTY 상태 오버레이 키). 프로토콜 `Machine` 에서 필드명 드리프트-락. */\r\nexport type MachineSummary = Pick<Machine, 'id' | 'name'>\r\n\r\n/** TUI 가 구독하는 헤드리스 UI 상태(불변 갱신 — Ink 가 참조 비교로 리렌더). */\r\nexport interface HeadlessUiState {\r\n  relayStatus: string\r\n  /** relay 재시도 횟수(0=정상). 연결 실패가 반복되는지 기본 화면 상태줄에 노출. */\r\n  relayRetry: number\r\n  /**\r\n   * 모바일 페어링 정보. `url` 은 폰이 연결할 페어링 URL(relay 모드=토큰 포함 HTTPS 딥링크,\r\n   * LAN 모드=`ws://<ip>:<port>/mobile`) — Info/Main 화면이 텍스트로 노출한다. `details` 는\r\n   * relayToken(8자)·localPort·lanIp 등 부가 진단 정보(`qr_code` 메시지 그대로 보존).\r\n   */\r\n  qr: { url: string; mode: string; details: ConnectionDetails } | null\r\n  /** 코어가 보유한 머신 목록(`machine_list` broadcast 로 갱신 — 폰이 머신 추가/삭제하면 라이브 반영). */\r\n  machines: MachineSummary[]\r\n  /** machineId → PTY 상태 오버레이. 머신 목록(이름)은 위 machines, 여기선 라이브 상태만 덮는다. */\r\n  sessions: Record<string, PtyState>\r\n  /**\r\n   * 호스트 중복 실행 경고. 이 에이전트와 다른 host(예: 데스크탑 앱)가 같은 relay 토큰으로\r\n   * 붙어 relay 가 핑퐁하는 상황 — `null`=정상, 값 있음=경고 배너 표시. 코어 `host_conflict`\r\n   * (active/reason)에 바인딩. 해제(active:false) 시 다시 null.\r\n   */\r\n  hostConflict: { reason: HostConflictReason } | null\r\n  /** 상세 로그(verbose 전용·스크롤). 기본 화면엔 미표시 — control-plane 이벤트만 누적. */\r\n  logs: string[]\r\n  /**\r\n   * npm 레지스트리에서 조회한 최신 버전. null=조회 중이거나 실패.\r\n   * 현재 버전보다 높으면 MainScreen 이 업데이트 배너를 표시한다.\r\n   */\r\n  latestVersion: string | null\r\n}\r\n\r\n/** verbose 로그 보관 한도. 스크롤백을 위해 기본 화면 표시량보다 넉넉히 잡는다. */\r\nconst MAX_LOGS = 500\r\n\r\n/**\r\n * 코어 broadcast(host 측)를 받아 TUI 상태로 환원하는 store. `apply(msg)` 가 상태를 불변 갱신하고\r\n * `'change'` 를 emit 하면 Ink 컴포넌트(useStore)가 리렌더한다. 데스크탑의 렌더러 IPC 수신부에\r\n * 대응하는 헤드리스판 sink.\r\n *\r\n * **데이터-플레인 제외**: `terminal_data`/`terminal_resize_ack`/`session_replay`/`ping` 은\r\n * 폰 사용 중 매 청크마다 쏟아져 로그를 뒤덮고 불필요한 리렌더를 유발한다. 실제 화면은 폰에 미러링되므로\r\n * 호스트 TUI 에선 무시한다(no log·no emit) — control-plane(relay/pty/conn/error/machine_list)만 추적해\r\n * verbose 로그의 신호 대 잡음을 유지한다.\r\n */\r\nexport class HeadlessStore extends EventEmitter {\r\n  private state: HeadlessUiState = {\r\n    relayStatus: 'connecting',\r\n    relayRetry: 0,\r\n    qr: null,\r\n    machines: [],\r\n    sessions: {},\r\n    hostConflict: null,\r\n    logs: [],\r\n    latestVersion: null,\r\n  }\r\n\r\n  getState(): HeadlessUiState {\r\n    return this.state\r\n  }\r\n\r\n  /** 코어 ServerMessage 1건을 상태에 반영. 구조화 상태(relay/qr/session)는 화면에, 그 외는 로그에. */\r\n  apply(msg: ServerMessage): void {\r\n    switch (msg.type) {\r\n      case 'relay_status':\r\n        this.state = { ...this.state, relayStatus: msg.status, relayRetry: msg.retryCount }\r\n        this.pushLog(`[relay] ${msg.status}${msg.retryCount ? ` (retry ${msg.retryCount})` : ''}`)\r\n        return\r\n      case 'qr_code':\r\n        this.state = { ...this.state, qr: { url: msg.url, mode: msg.mode, details: msg.details } }\r\n        this.pushLog(`[qr] ${msg.mode}`)\r\n        return\r\n      case 'pty_status':\r\n        this.state = {\r\n          ...this.state,\r\n          sessions: { ...this.state.sessions, [msg.machineId]: msg.status },\r\n        }\r\n        this.pushLog(`[pty] ${msg.machineId} → ${msg.status}${msg.pid ? ` pid=${msg.pid}` : ''}`)\r\n        return\r\n      case 'machine_list':\r\n        // 폰이 머신을 추가/삭제하면 코어가 broadcastMachines() → 여기로 흐른다. Sessions 카드를 라이브로.\r\n        this.state = {\r\n          ...this.state,\r\n          machines: msg.machines.map(m => ({ id: m.id, name: m.name })),\r\n        }\r\n        this.pushLog(`[machines] ${msg.machines.length}`)\r\n        return\r\n      case 'connection_status':\r\n        this.pushLog(`[conn] ${msg.machineId} → ${msg.status}`)\r\n        return\r\n      case 'host_conflict':\r\n        // 중복 host 경고 on/off — 배너 상태 갱신 + verbose 로그. active:false 면 해제(null).\r\n        this.state = {\r\n          ...this.state,\r\n          hostConflict: msg.active ? { reason: msg.reason } : null,\r\n        }\r\n        this.pushLog(`[conflict] ${msg.active ? `DETECTED (${msg.reason})` : 'resolved'}`)\r\n        return\r\n      case 'error':\r\n        this.pushLog(`[error] ${msg.message}`)\r\n        return\r\n      // 데이터-플레인(고빈도) — 무시. emit 도 하지 않아 리렌더 churn 을 막는다.\r\n      case 'terminal_data':\r\n      case 'terminal_resize_ack':\r\n      case 'session_replay':\r\n      case 'ping':\r\n        return\r\n      default:\r\n        this.pushLog(`· ${msg.type}`)\r\n    }\r\n  }\r\n\r\n  /** npm 레지스트리 조회 결과를 주입한다(부팅 직후 백그라운드 스레드에서 1회 호출). */\r\n  setLatestVersion(version: string): void {\r\n    this.state = { ...this.state, latestVersion: version }\r\n    this.emit('change')\r\n  }\r\n\r\n  /** 사람이 읽는 로그 라인을 직접 추가(cli 부팅 메시지 등). verbose 로그에만 보인다. */\r\n  log(line: string): void {\r\n    this.pushLog(line)\r\n  }\r\n\r\n  private pushLog(line: string): void {\r\n    const ts = new Date().toISOString().slice(11, 19)\r\n    const logs = [...this.state.logs, `${ts} ${line}`].slice(-MAX_LOGS)\r\n    this.state = { ...this.state, logs }\r\n    this.emit('change')\r\n  }\r\n}\r\n","import { spawn } from 'node:child_process'\r\nimport process from 'node:process'\r\n\r\n/** npm registry에서 받은 최신 버전 체크 결과. */\r\nexport interface VersionCheckResult {\r\n  latest: string\r\n  /** 현재 버전보다 최신인지. */\r\n  hasUpdate: boolean\r\n}\r\n\r\n/** 자동 업데이트 실행 결과. */\r\nexport interface UpdateResult {\r\n  ok: boolean\r\n  /**\r\n   * 실패 시 사용자에게 보여줄 메시지.\r\n   * EACCES → 권한 부족 안내 + 설치 명령어 포함.\r\n   */\r\n  errorMessage?: string\r\n}\r\n\r\n/**\r\n * semver 문자열을 [major, minor, patch] 숫자 배열로 파싱.\r\n * 파싱 실패 시 [0, 0, 0] 반환(비교 시 항상 \"최신 아님\"으로 처리).\r\n */\r\nfunction parseSemver(v: string): [number, number, number] {\r\n  const parts = v.replace(/^v/, '').split('.').map(Number)\r\n  return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0]\r\n}\r\n\r\n/**\r\n * a가 b보다 최신(greater)인지 비교.\r\n */\r\nfunction isNewer(a: string, b: string): boolean {\r\n  const [aMaj, aMin, aPat] = parseSemver(a)\r\n  const [bMaj, bMin, bPat] = parseSemver(b)\r\n  if (aMaj !== bMaj) return aMaj > bMaj\r\n  if (aMin !== bMin) return aMin > bMin\r\n  return aPat > bPat\r\n}\r\n\r\n/** npm 레지스트리 REST 엔드포인트 — 스코프 슬래시는 `%2F` 인코딩(리터럴 슬래시도 되나 명시). */\r\nconst REGISTRY_LATEST_URL = 'https://registry.npmjs.org/@junyoung-kim%2Freins/latest'\r\n\r\n/**\r\n * npm 레지스트리에서 `@junyoung-kim/reins` 최신 버전을 비동기로 조회한다.\r\n *\r\n * `npm view` 자식 프로세스 대신 registry `/latest` 엔드포인트를 직접 `fetch` 한다:\r\n * - **Windows 크래시 회피**: `npm` 은 `npm.cmd`(shim) 라, Node 18.20+/20.12+/24 는 `.cmd` 를\r\n *   `shell:false`(기본) 로 spawn 하면 CVE-2024-27980 패치가 `EINVAL` 을 **동기 throw** 한다.\r\n *   이 함수는 `void` fire-and-forget 로 호출되어 그 reject 이 unhandled → 부팅 크래시였다.\r\n *   HTTP 는 셸/플랫폼과 무관해 근본적으로 안전하다.\r\n * - npm 프로세스 기동(수 초) 없이 수백 ms 로 끝나 Ink 렌더 루프에 부담이 적다.\r\n * 네트워크 오류·비200·파싱 실패·타임아웃(8s)은 모두 null 로 흡수(업데이트 체크 실패는 조용히 무시).\r\n *\r\n * @param currentVersion  현재 설치 버전(`pkg.version`).\r\n * @returns 최신 버전 정보 또는 조회 실패 시 null.\r\n */\r\nexport async function checkLatestVersion(currentVersion: string): Promise<VersionCheckResult | null> {\r\n  try {\r\n    const res = await fetch(REGISTRY_LATEST_URL, {\r\n      signal: AbortSignal.timeout(8000),\r\n      headers: { accept: 'application/json' },\r\n    })\r\n    if (!res.ok) return null\r\n    const data = (await res.json()) as { version?: string }\r\n    const latest = data.version?.trim()\r\n    if (!latest) return null\r\n    return { latest, hasUpdate: isNewer(latest, currentVersion) }\r\n  } catch {\r\n    return null\r\n  }\r\n}\r\n\r\n/**\r\n * `npm install -g @junyoung-kim/reins@latest` 를 실행하고 완료 후 프로세스를 재시작한다.\r\n *\r\n * - 성공: `process.execPath`(node) + `process.argv[1]`(cli) 로 새 프로세스를 spawn 하고, **'spawn'\r\n *   이벤트(재시작 확정)에서만** `onBeforeRestart`(코어 cleanup) 를 호출해 relay·로컬 WS·PTY 를\r\n *   정리한 뒤 `process.exit(0)`. cleanup 을 생략하면 로컬 WS 포트가 안 풀린 채 child2 가 바인딩을\r\n *   시도해 EADDRINUSE 위험 + 모바일 클라이언트가 abrupt close(1006) 를 받는다.\r\n * - 재시작 실패('error', 예: 스크립트 경로 소실): 코어를 건드리지 않아 현재 인스턴스가 계속\r\n *   살아 있고, stderr 안내 + ok=false 를 반환한다.\r\n * - EACCES/EPERM(npm prefix 쓰기 권한 없음): ok=false + 안내 메시지 반환.\r\n * - 그 외 오류: ok=false + 일반 오류 메시지 반환.\r\n *\r\n * @param onBeforeRestart 재시작 확정('spawn') 직후 1회 호출(코어 cleanup). 실패 경로에서는 미호출.\r\n * @returns Promise<UpdateResult> — 재시작 성공 시 이 함수는 반환되지 않음(프로세스 교체).\r\n */\r\nexport async function runSelfUpdate(onBeforeRestart?: () => void): Promise<UpdateResult> {\r\n  return new Promise(resolve => {\r\n    // npm 은 Windows 에서 `npm.cmd`(shim) — Node 18.20+/24 는 `.cmd` 를 `shell:false` 로 직접\r\n    // spawn 하면 EINVAL(CVE-2024-27980 패치). CLAUDE.md 패턴대로 `cmd.exe /d /s /c npm ...` 배열로\r\n    // 감싼다(cmd.exe 가 PATHEXT 로 npm.cmd 해석, Node 가 각 인자를 Windows escape → shell:true 의\r\n    // DEP0190/인젝션 없이 안전). Unix 는 npm 을 직접 spawn(shell:false, agent-runner.ts 와 동일 패턴).\r\n    const isWin = process.platform === 'win32'\r\n    const npmArgv = ['npm', 'install', '-g', '@junyoung-kim/reins@latest']\r\n    const command = isWin ? (process.env.COMSPEC ?? 'cmd.exe') : npmArgv[0]\r\n    const args = isWin ? ['/d', '/s', '/c', ...npmArgv] : npmArgv.slice(1)\r\n    const child = spawn(command, args, {\r\n      shell: false,\r\n      stdio: ['ignore', 'pipe', 'pipe'],\r\n      windowsHide: true,\r\n    })\r\n\r\n    let stderr = ''\r\n    child.stderr?.on('data', (d: Buffer) => { stderr += d.toString() })\r\n\r\n    child.on('error', err => {\r\n      resolve({ ok: false, errorMessage: `Auto-update failed: ${err.message}` })\r\n    })\r\n\r\n    child.on('close', code => {\r\n      if (code === 0) {\r\n        // 업데이트 성공 → 현재 프로세스를 같은 인자로 재시작(spawn + process.exit 가 Node.js 에서\r\n        // execv 의 관용 대안, 자신을 교체).\r\n        //\r\n        // **exit 를 'spawn' 이벤트로 미룬다**: spawn 직후 동기 process.exit 를 하면 재시작 자식의\r\n        // 비동기 'error'(exec 실패: 스크립트 경로 소실·node 교체 등)가 실행될 기회를 잃어, 아무것도\r\n        // 안 뜬 채 안내도 못 받는 상태가 된다. 'spawn'(성공 확정) 에서만 cleanup 후 종료하고,\r\n        // 'error' 면 코어를 건드리지 않고 stderr 로 수동 복구를 안내한다.\r\n        const child2 = spawn(process.execPath, process.argv.slice(1), {\r\n          stdio: 'inherit',\r\n          detached: false,\r\n          shell: false,\r\n        })\r\n        child2.on('spawn', () => {\r\n          // 재시작이 확정된 뒤에만 코어 정리(포트/relay/PTY 해제) — 다음 인스턴스의 EADDRINUSE·\r\n          // 모바일 abrupt close 방지. 실패 경로에서는 코어가 그대로 살아 있어야 하므로 여기서 호출.\r\n          onBeforeRestart?.()\r\n          child2.unref()\r\n          process.exit(0)\r\n        })\r\n        child2.on('error', err => {\r\n          // 재시작 실패 — 코어는 아직 정리 전이라 현재 인스턴스가 계속 살아 있다. stderr 로 안내.\r\n          process.stderr.write(\r\n            `reins: restart after update failed (${err.message}) — run 'reins' manually\\n`\r\n          )\r\n          resolve({ ok: false, errorMessage: `Update installed but restart failed: ${err.message}` })\r\n        })\r\n        return\r\n      }\r\n\r\n      // 권한 오류 감지 — npm 은 EACCES/EPERM 을 stderr 에 출력한다.\r\n      const isPermError =\r\n        stderr.includes('EACCES') ||\r\n        stderr.includes('EPERM') ||\r\n        stderr.includes('permission denied') ||\r\n        stderr.includes('write permission')\r\n\r\n      if (isPermError) {\r\n        resolve({\r\n          ok: false,\r\n          errorMessage:\r\n            'Auto-update failed: no write permission to npm prefix  run: npm i -g @junyoung-kim/reins@latest',\r\n        })\r\n      } else {\r\n        const detail = stderr.split('\\n').find(l => l.includes('ERR')) ?? `exit ${code}`\r\n        resolve({ ok: false, errorMessage: `Auto-update failed: ${detail}` })\r\n      }\r\n    })\r\n  })\r\n}\r\n","import { Box, Text, useApp } from 'ink'\r\nimport React, { useEffect, useState } from 'react'\r\n\r\nimport type { AgentCore } from '@arva/core'\r\n\r\nimport type { HeadlessConfigStore } from '../config/json-store'\r\nimport { identity } from '../identity'\r\nimport type { HeadlessStore, HeadlessUiState } from '../state'\r\nimport { GetAppsScreen } from './screens/GetAppsScreen'\r\nimport { InfoScreen } from './screens/InfoScreen'\r\nimport { MachinesScreen } from './screens/MachinesScreen'\r\nimport { MainScreen } from './screens/MainScreen'\r\nimport { OptionsScreen } from './screens/OptionsScreen'\r\n\r\n/** 라우터 화면 식별자. machineForm 은 MachinesScreen 내부 sub-mode 로 처리(여기선 top-level 5개). */\r\nexport type Screen = 'main' | 'machines' | 'options' | 'info' | 'getApps'\r\n\r\n/** store 의 'change' 를 구독해 최신 상태를 반환하는 훅(참조 갱신으로 Ink 리렌더 유도). */\r\nfunction useStore(store: HeadlessStore): HeadlessUiState {\r\n  const [state, setState] = useState(store.getState())\r\n  useEffect(() => {\r\n    const onChange = () => setState({ ...store.getState() })\r\n    store.on('change', onChange)\r\n    return () => {\r\n      store.off('change', onChange)\r\n    }\r\n  }, [store])\r\n  return state\r\n}\r\n\r\n/** relay 상태별 도트 + 색. */\r\nconst STATUS: Record<string, { dot: string; color: string }> = {\r\n  connected: { dot: '●', color: 'green' },\r\n  connecting: { dot: '◌', color: 'yellow' },\r\n  disconnected: { dot: '○', color: 'red' },\r\n  error: { dot: '✗', color: 'red' },\r\n}\r\n\r\n/**\r\n * 헤드리스 메인 셸/라우터. 항상 보이는 헤더(reins·relay 상태·relay 타깃) + 활성 화면(main/machines/options).\r\n *\r\n * **키 소유권(중요)**: 각 화면이 자신의 `useInput` 을 `isActive`(=현재 화면) 로 게이팅한다. Ink 는\r\n * 핸들러를 stop-propagate 하지 않으므로, 게이팅 없이는 폼 타이핑 중 다른 화면 단축키가 동시 발동한다.\r\n * App 자체는 키를 잡지 않고 라우팅만 한다.\r\n *\r\n * relay 타깃 헤더는 `config.getRelayUrls()` 를 매 렌더 라이브로 읽어, 옵션 화면에서 URL 편집 후에도\r\n * (재시작 없이) 즉시 갱신된다.\r\n */\r\nexport function App({\r\n  store,\r\n  core,\r\n  config,\r\n}: {\r\n  store: HeadlessStore\r\n  core: AgentCore\r\n  config: HeadlessConfigStore\r\n}) {\r\n  const state = useStore(store)\r\n  const { exit } = useApp()\r\n  const [screen, setScreen] = useState<Screen>('main')\r\n\r\n  // relay 가 붙으면 relay QR 재요청(부팅 직후엔 LAN fallback QR 만 가능). 화면 무관 글로벌 lifecycle.\r\n  useEffect(() => {\r\n    if (state.relayStatus === 'connected') core.ws.requestQr()\r\n  }, [state.relayStatus, core])\r\n\r\n  const s = STATUS[state.relayStatus] ?? { dot: '·', color: 'gray' }\r\n  const retry = state.relayRetry ? ` (retry ${state.relayRetry})` : ''\r\n  const relayTarget = config.getRelayUrls().join(', ')\r\n\r\n  return (\r\n    <Box flexDirection=\"column\" borderStyle=\"round\" paddingX={1}>\r\n      <Box justifyContent=\"space-between\">\r\n        <Text bold>reins</Text>\r\n        <Text color=\"gray\">v{identity.version}</Text>\r\n      </Box>\r\n\r\n      <Box>\r\n        <Text color=\"gray\">Relay </Text>\r\n        <Text color={s.color}>\r\n          {s.dot} {state.relayStatus}\r\n          {retry}\r\n        </Text>\r\n        {state.qr ? <Text color=\"gray\">  ({state.qr.mode})</Text> : null}\r\n      </Box>\r\n      {/* relay URL 목록은 길어질 수 있어 truncate — 항상 보이는 헤더가 wrap 되면 아래 전체가 밀린다. */}\r\n      <Text color=\"gray\" wrap=\"truncate\">\r\n        {relayTarget}\r\n      </Text>\r\n\r\n      {state.hostConflict ? (\r\n        <Box flexDirection=\"column\" borderStyle=\"round\" borderColor=\"red\" paddingX={1} marginTop={1}>\r\n          <Text color=\"red\" bold>\r\n            ⚠ ANOTHER INSTANCE DETECTED\r\n          </Text>\r\n          <Text color=\"yellow\">\r\n            {state.hostConflict.reason === 'port_in_use'\r\n              ? 'The local port is already in use by another instance on this machine.'\r\n              : 'Relay keeps dropping — this agent and another host (e.g. the desktop app) share one token.'}\r\n          </Text>\r\n          <Text color=\"gray\">Close the desktop app or the other agent, then restart.</Text>\r\n        </Box>\r\n      ) : null}\r\n\r\n      {screen === 'main' ? (\r\n        <MainScreen\r\n          state={state}\r\n          core={core}\r\n          active\r\n          onNavigate={setScreen}\r\n          onExit={exit}\r\n        />\r\n      ) : null}\r\n      {screen === 'machines' ? (\r\n        <MachinesScreen\r\n          machines={core.ssh.getMachines()}\r\n          core={core}\r\n          active\r\n          onBack={() => setScreen('main')}\r\n        />\r\n      ) : null}\r\n      {screen === 'options' ? (\r\n        <OptionsScreen config={config} core={core} active onBack={() => setScreen('main')} />\r\n      ) : null}\r\n      {screen === 'info' ? (\r\n        <InfoScreen\r\n          state={state}\r\n          config={config}\r\n          core={core}\r\n          active\r\n          onBack={() => setScreen('main')}\r\n        />\r\n      ) : null}\r\n      {screen === 'getApps' ? (\r\n        <GetAppsScreen active onBack={() => setScreen('main')} />\r\n      ) : null}\r\n    </Box>\r\n  )\r\n}\r\n","import { spawn } from 'node:child_process'\r\n\r\nimport { Box, Text, useInput } from 'ink'\r\nimport React, { useEffect, useMemo, useRef, useState } from 'react'\r\n\r\nimport {\r\n  DESKTOP_OS_LABEL,\r\n  DESKTOP_OS_ORDER,\r\n  type DesktopOs,\r\n  detectDesktopOs,\r\n  fetchLatestInstaller,\r\n  type LatestInstaller,\r\n} from '../../download-feed'\r\nimport { Divider } from '../components/Divider'\r\nimport { PairQrCell } from '../components/PairQrCell'\r\nimport { UPDATES_FEED_URL } from '../constants'\r\nimport { renderQr } from '../qr'\r\nimport { useTerminalSize } from '../useTerminalSize'\r\n\r\n/**\r\n * 다운로드 URL 을 OS 기본 브라우저로 연다(best-effort).\r\n *\r\n * 플랫폼별 실행 파일이 다르다: macOS=`open`, Windows=`start`(cmd 내장 명령이라 `cmd /c` 경유 +\r\n * 빈 title 인자 `''` 로 URL 이 창 제목으로 오인되는 것 방지), Linux=`xdg-open`. 셸 escape 를 피하려\r\n * **배열 인자 spawn(shell:false)** 로 실행한다(CLAUDE.md 크로스플랫폼 규칙). detached+unref 로 TUI\r\n * 수명과 분리하고, 비동기 'error'(바이너리 부재 등)는 삼켜 TUI 가 죽지 않게 한다.\r\n *\r\n * **SSH/원격(무 디스플레이)에선 실패가 정상** — 호출자는 실패해도 화면의 URL 텍스트로 폴백한다.\r\n *\r\n * **신뢰 입력 가정**: `url` 은 업데이트 피드 manifest 에서 온 런타임 값이지만, 파일명이\r\n * `parseLatestYml` 의 `SAFE_FILE_NAME`(`[\\w.+-]`)으로 제한돼 Windows `cmd /c start` 경로의 cmd\r\n * 메타문자 인젝션 표면이 없다(base URL 은 컴파일타임 상수). 파일명 검증을 약화하면 이 경로를 재검토할 것.\r\n *\r\n * @returns spawn 이 동기 throw 없이 시작됐으면 true(브라우저 실제 오픈 보장은 아님).\r\n */\r\nfunction openUrl(url: string): boolean {\r\n  // 플랫폼별로 실행 파일·인자만 고르고, spawn 옵션과 호출은 한 곳으로 모은다(옵션 중복 제거).\r\n  let cmd: string\r\n  let args: string[]\r\n  if (process.platform === 'darwin') {\r\n    cmd = 'open'\r\n    args = [url]\r\n  } else if (process.platform === 'win32') {\r\n    cmd = 'cmd'\r\n    args = ['/c', 'start', '', url]\r\n  } else {\r\n    cmd = 'xdg-open'\r\n    args = [url]\r\n  }\r\n  try {\r\n    const child = spawn(cmd, args, { detached: true, stdio: 'ignore' })\r\n    child.on('error', () => {})\r\n    child.unref()\r\n    return true\r\n  } catch {\r\n    return false\r\n  }\r\n}\r\n\r\n/** 선택 OS 의 피드 조회 상태(화면 본문 분기). */\r\ntype FeedState =\r\n  | { status: 'loading' }\r\n  | { status: 'ok'; installer: LatestInstaller }\r\n  | { status: 'error' }\r\n\r\n/**\r\n * 'Get apps' 화면 — 데스크탑 GUI 앱을 **선택 OS 의 최신 버전**으로 다운로드 안내한다.\r\n *\r\n * desktop 의 generic 업데이트 피드(`UPDATES_FEED_URL`/`latest*.yml`)를 직접 읽어 설치파일 URL 을\r\n * 만든다(미구축 랜딩 페이지 비의존). OS 는 `process.platform` 으로 기본 선택하되 ←→ 로 변경 가능 —\r\n * SSH 원격에선 호스트 OS ≠ 설치 기기 OS 일 수 있어서다. OS 별 결과는 `cacheRef` 에 캐시해 토글 왕복 시\r\n * 재요청하지 않는다. fetch 실패는 폴백 메시지 + `r` 재시도로 흡수(TUI 크래시 없음).\r\n *\r\n * **키 소유권**: `useInput` 은 active(=현재 화면이 getApps)일 때만 동작(타 화면 단축키 오발동 방지).\r\n *\r\n * @param active 현재 라우터 화면이 getApps 인지(키 입력 게이팅).\r\n */\r\nexport function GetAppsScreen({\r\n  active,\r\n  onBack,\r\n}: {\r\n  active: boolean\r\n  onBack: () => void\r\n}) {\r\n  const { columns, rows } = useTerminalSize()\r\n  const detected = detectDesktopOs()\r\n  const [selectedOs, setSelectedOs] = useState<DesktopOs>(detected)\r\n  const [feed, setFeed] = useState<FeedState>({ status: 'loading' })\r\n  const [qrVisible, setQrVisible] = useState(false)\r\n  const [notice, setNotice] = useState<string | null>(null)\r\n  const [retry, setRetry] = useState(0)\r\n  // OS → 성공 결과 캐시(토글 왕복 시 재요청 방지). 표시 상태는 selectedOs 로 파생.\r\n  const cacheRef = useRef<Map<DesktopOs, LatestInstaller>>(new Map())\r\n\r\n  useEffect(() => {\r\n    const cached = cacheRef.current.get(selectedOs)\r\n    if (cached) {\r\n      setFeed({ status: 'ok', installer: cached })\r\n      return\r\n    }\r\n    const ctrl = new AbortController()\r\n    setFeed({ status: 'loading' })\r\n    fetchLatestInstaller(selectedOs, ctrl.signal)\r\n      .then(installer => {\r\n        cacheRef.current.set(selectedOs, installer)\r\n        setFeed({ status: 'ok', installer })\r\n      })\r\n      .catch(() => {\r\n        // 언마운트/OS 변경으로 인한 취소는 다음 effect 가 새 상태를 세팅하므로 무시.\r\n        if (!ctrl.signal.aborted) setFeed({ status: 'error' })\r\n      })\r\n    return () => ctrl.abort()\r\n  }, [selectedOs, retry])\r\n\r\n  useInput(\r\n    (input, key) => {\r\n      if (key.escape) {\r\n        onBack()\r\n        return\r\n      }\r\n      if (key.leftArrow || key.rightArrow) {\r\n        setSelectedOs(prev => {\r\n          const i = DESKTOP_OS_ORDER.indexOf(prev)\r\n          const len = DESKTOP_OS_ORDER.length\r\n          const next = key.leftArrow ? (i - 1 + len) % len : (i + 1) % len\r\n          return DESKTOP_OS_ORDER[next]\r\n        })\r\n        setNotice(null)\r\n        return\r\n      }\r\n      if (input === 'c') {\r\n        setQrVisible(v => !v)\r\n        return\r\n      }\r\n      if (input === 'r') {\r\n        // 에러 상태에서 재시도: 캐시 무효화 + retry nonce 증가로 effect 재실행.\r\n        cacheRef.current.delete(selectedOs)\r\n        setRetry(n => n + 1)\r\n        return\r\n      }\r\n      if (input === 'o' || key.return) {\r\n        if (feed.status !== 'ok') return\r\n        const ok = openUrl(feed.installer.url)\r\n        setNotice(\r\n          ok\r\n            ? 'Opening in browser… (local only — over SSH use the URL above)'\r\n            : 'Could not open browser — copy the URL above'\r\n        )\r\n      }\r\n    },\r\n    { isActive: active }\r\n  )\r\n\r\n  const downloadUrl = feed.status === 'ok' ? feed.installer.url : null\r\n  const qrText = useMemo(() => (downloadUrl ? renderQr(downloadUrl) : null), [downloadUrl])\r\n\r\n  return (\r\n    <Box flexDirection=\"column\">\r\n      <Divider />\r\n      <Text>Get apps</Text>\r\n\r\n      <Divider />\r\n      <Text color=\"gray\">DESKTOP APP (GUI host)</Text>\r\n      <Text color=\"gray\">{'  '}Get the latest desktop app for your OS.</Text>\r\n\r\n      <Box>\r\n        <Text color=\"gray\">{'  '}OS{'  '}</Text>\r\n        {DESKTOP_OS_ORDER.map(os => (\r\n          <Text key={os} color={os === selectedOs ? 'cyan' : 'gray'}>\r\n            {os === selectedOs ? '> ' : '  '}\r\n            {DESKTOP_OS_LABEL[os]}\r\n            {'  '}\r\n          </Text>\r\n        ))}\r\n      </Box>\r\n      <Text color=\"gray\">\r\n        {'      '}(detected: {DESKTOP_OS_LABEL[detected]})\r\n      </Text>\r\n\r\n      {feed.status === 'loading' ? (\r\n        <Text color=\"gray\">{'  '}Fetching latest version…</Text>\r\n      ) : null}\r\n      {feed.status === 'error' ? (\r\n        <Box flexDirection=\"column\">\r\n          <Text color=\"yellow\">{'  '}Could not read update feed — press r to retry</Text>\r\n          <Box>\r\n            <Text color=\"gray\">{'  '}Feed </Text>\r\n            <Text color=\"cyan\">{UPDATES_FEED_URL}/</Text>\r\n          </Box>\r\n        </Box>\r\n      ) : null}\r\n      {feed.status === 'ok' ? (\r\n        <Box flexDirection=\"column\">\r\n          <Text color=\"gray\">\r\n            {'  '}v{feed.installer.version}\r\n          </Text>\r\n          <Box>\r\n            <Text color=\"gray\">{'  '}URL </Text>\r\n            <Text color=\"cyan\">{feed.installer.url}</Text>\r\n          </Box>\r\n          <PairQrCell qrText={qrText} visible={qrVisible} columns={columns} rows={rows} />\r\n        </Box>\r\n      ) : null}\r\n\r\n      <Divider />\r\n      <Text color=\"gray\">mobile: scan the pairing QR on the main screen</Text>\r\n\r\n      <Divider />\r\n      {notice ? <Text color=\"yellow\">{notice}</Text> : null}\r\n      <Text color=\"cyan\">\r\n        ←→ os · o open in browser · c {qrVisible ? 'hide' : 'show'} qr · r retry · Esc back\r\n      </Text>\r\n    </Box>\r\n  )\r\n}\r\n","/** 화면 구분선(가로줄 40폭). 모든 헤드리스 TUI 화면이 공유하는 단일 정의. */\r\nexport const DIVIDER = '─'.repeat(40)\r\n\r\n/**\r\n * 데스크탑 GUI 앱(@arva/desktop) generic 업데이트 피드의 base URL — 'Get apps' 화면이 읽는다.\r\n *\r\n * `apps/desktop/electron-builder.ts` 의 `publish.url` 과 **반드시 동일**해야 한다(같은 서버의 같은\r\n * 디렉터리). 이 base 아래에 electron-builder 가 생성한 manifest(`latest.yml`/`latest-mac.yml`/\r\n * `latest-linux.yml`)와 설치파일이 CI(scp)로 올라간다. {@link fetchLatestInstaller} 가 manifest 를\r\n * 읽어 **최신 버전** 설치파일 경로를 만들므로, 버전이 본 npm 패키지에 박히지 않는다(desktop 릴리즈마다\r\n * reins 재배포 불필요). 미구축 랜딩 페이지(`apps/arv`)에 의존하지 않아 지금 바로 동작한다.\r\n */\r\nexport const UPDATES_FEED_URL = 'https://arv.juny-api.kr/updates'\r\n","import { UPDATES_FEED_URL } from './ui/constants'\n\n/**\n * desktop 빌드 타깃 OS — 'Get apps' 화면의 OS 토글 단위이자 manifest 선택 키.\n * (electron-builder 의 win/mac/linux 산출물에 대응)\n */\nexport type DesktopOs = 'mac' | 'win' | 'linux'\n\n/** ←→ 토글 순환 순서(화면이 이 순서로 좌/우 이동). */\nexport const DESKTOP_OS_ORDER: readonly DesktopOs[] = ['mac', 'win', 'linux']\n\n/** 사람이 읽는 OS 라벨(화면 표기 전용). */\nexport const DESKTOP_OS_LABEL: Record<DesktopOs, string> = {\n  mac: 'macOS',\n  win: 'Windows',\n  linux: 'Linux',\n}\n\n/** OS 별 electron-builder manifest 파일명(`electron-builder.ts` 산출 컨벤션). */\nconst MANIFEST_FILE: Record<DesktopOs, string> = {\n  win: 'latest.yml',\n  mac: 'latest-mac.yml',\n  linux: 'latest-linux.yml',\n}\n\n/**\n * OS 별 **사용자 설치용** 산출물 확장자(우선순위 순).\n *\n * mac 은 `target: ['dmg','zip']` 라 manifest 최상위 `path` 가 보통 **zip**(델타 자동업데이트용)을\n * 가리킨다 — 사용자 더블클릭 설치는 **dmg** 라 files[] 에서 dmg 를 먼저 고른다. win=exe, linux 는\n * 여러 포맷 중 가장 보편적인 AppImage 를 우선한다.\n */\nconst INSTALLER_EXT: Record<DesktopOs, readonly string[]> = {\n  win: ['.exe'],\n  mac: ['.dmg'],\n  linux: ['.AppImage'],\n}\n\n/** electron-builder 산출물 파일명 화이트리스트 — 셸/경로 메타문자 차단(아래 보안 주석 참조). */\nconst SAFE_FILE_NAME = /^[\\w.+-]+$/\n\n/** {@link fetchLatestInstaller} 의 결과 — 화면이 표시·다운로드에 쓴다. */\nexport interface LatestInstaller {\n  /** manifest `version` 필드(예: `1.8.0`). 화면 표기용. */\n  version: string\n  /** 다운로드 절대 URL(`<feed base>/<파일명>`). QR·브라우저 오픈 대상. */\n  url: string\n}\n\n/** 선택 OS 의 manifest 절대 URL. */\nexport function manifestUrl(target: DesktopOs): string {\n  return `${UPDATES_FEED_URL}/${MANIFEST_FILE[target]}`\n}\n\n/**\n * `process.platform` → {@link DesktopOs}. `darwin`=mac, `win32`=win, 그 외(linux 등)=linux.\n *\n * SSH 원격에선 reins 호스트 OS 와 GUI 앱을 설치할 기기 OS 가 다를 수 있어 어디까지나 **기본 선택값**일\n * 뿐, 사용자가 화면에서 ←→ 로 바꿀 수 있다.\n */\nexport function detectDesktopOs(platform: NodeJS.Platform = process.platform): DesktopOs {\n  if (platform === 'darwin') return 'mac'\n  if (platform === 'win32') return 'win'\n  return 'linux'\n}\n\n/**\n * electron-builder 의 `latest*.yml`(고정·단순 구조)에서 version 과 설치파일명을 뽑는 **미니 파서**.\n *\n * 풀 YAML 파서를 끌어오지 않으려는 의도(npx 배포 경량화). 인식 대상은 세 줄 패턴뿐:\n * ```yaml\n * version: 1.8.0\n * files:\n *   - url: <name-1>\n *   - url: <name-2>\n * path: <name>\n * ```\n * files[] 에서 {@link INSTALLER_EXT} 우선순위로 매칭되는 url 을 고르고, 없으면 top-level `path` 폴백.\n *\n * **보안(WHY)**: 반환 파일명은 호출부에서 base URL 과 결합돼 {@link manifestUrl} 와 함께 OS 기본\n * 브라우저로 열린다. Windows 는 `cmd /c start \"\" <url>`(cmd 내장) 경로라 url 에 cmd 메타문자(`&` 등)가\n * 있으면 명령 인젝션 표면이 된다 → 파일명을 {@link SAFE_FILE_NAME}(`[\\w.+-]`)로 제한해 메타문자·경로\n * 구분자(`/`)를 모두 차단한다(경로 탈출도 방지). version/file 누락 또는 불안전 파일명이면 null.\n *\n * @returns 파일명(상대) + version. URL 결합은 호출부({@link fetchLatestInstaller})가 담당.\n */\nexport function parseLatestYml(\n  text: string,\n  target: DesktopOs,\n): { version: string; file: string } | null {\n  const unquote = (s: string): string => s.trim().replace(/^['\"]|['\"]$/g, '')\n  const version = text.match(/^version:\\s*(.+)$/m)?.[1]\n  const pathField = text.match(/^path:\\s*(.+)$/m)?.[1]\n  const urls = [...text.matchAll(/^\\s*-\\s*url:\\s*(.+)$/gm)].map(m => unquote(m[1]))\n\n  let file: string | undefined\n  for (const ext of INSTALLER_EXT[target]) {\n    file = urls.find(u => u.toLowerCase().endsWith(ext.toLowerCase()))\n    if (file) break\n  }\n  if (!file && pathField) file = unquote(pathField)\n\n  const ver = version ? unquote(version) : undefined\n  if (!ver || !file || !SAFE_FILE_NAME.test(file)) return null\n  return { version: ver, file }\n}\n\n/** manifest fetch 타임아웃(ms) — 무응답 서버에서 화면이 영영 loading 에 머물지 않게 한다. */\nconst FETCH_TIMEOUT_MS = 5000\n\n/**\n * 선택 OS 의 최신 설치 파일 정보를 가져온다(manifest fetch → 파싱 → URL 결합).\n *\n * **부수효과**: 네트워크 GET({@link manifestUrl}). Node18+ 내장 `fetch` + `AbortController` 타임아웃.\n * 실패(네트워크/HTTP 비정상/파싱불가/타임아웃)는 모두 throw — 호출부(GetAppsScreen)가 폴백 UI 로 처리한다.\n *\n * @param target  대상 OS.\n * @param signal  호출부가 언마운트/OS 변경 시 취소하려는 외부 시그널(선택). 내부 타임아웃과 OR 로 합쳐진다.\n */\nexport async function fetchLatestInstaller(\n  target: DesktopOs,\n  signal?: AbortSignal,\n): Promise<LatestInstaller> {\n  const ctrl = new AbortController()\n  const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS)\n  // 외부 시그널(언마운트/OS 변경)도 내부 컨트롤러로 전파 — fetch 가 즉시 끊긴다.\n  if (signal) signal.addEventListener('abort', () => ctrl.abort(), { once: true })\n  try {\n    const res = await fetch(manifestUrl(target), { signal: ctrl.signal })\n    if (!res.ok) throw new Error(`FEED_HTTP_${res.status}`)\n    const parsed = parseLatestYml(await res.text(), target)\n    if (!parsed) throw new Error('FEED_PARSE_FAILED')\n    return { version: parsed.version, url: `${UPDATES_FEED_URL}/${parsed.file}` }\n  } finally {\n    clearTimeout(timer)\n  }\n}\n","import { Text } from 'ink'\r\nimport React from 'react'\r\n\r\nimport { DIVIDER } from '../constants'\r\n\r\n/**\r\n * 화면 구분선 한 줄.\r\n *\r\n * **`wrap=\"truncate\"` 가 핵심**: 고정폭 {@link DIVIDER}(40)가 좁은 터미널(SSH·모바일)에서 그대로\r\n * 렌더되면 Ink 가 다음 줄로 하드랩해 한 줄이 두 줄이 되고, 리스트 윈도잉 높이 계산(항목 수 기준)과\r\n * 라운드 보더 프레임이 밀려 화면이 깨진다. truncate 로 터미널 폭에서 잘라 **항상 정확히 한 줄**을 유지한다.\r\n */\r\nexport function Divider() {\r\n  return (\r\n    <Text color=\"gray\" wrap=\"truncate\">\r\n      {DIVIDER}\r\n    </Text>\r\n  )\r\n}\r\n","import { Text } from 'ink'\nimport React from 'react'\n\nimport { qrLayout } from '../qr-fit'\n\n/**\n * \"Scan to pair:\" 와 URL 사이에 들어가는 QR 영역 — 토글(`visible`)과 터미널 크기에 따라 네 상태를 그린다:\n *\n * - 펼침 + 들어감 → QR ASCII\n * - 펼침 + 안 들어감 → **잘린 QR 대신** 크기 경고(\"widen the window or use the URL\")\n * - 접힘 + 들어감 → \"(press c to show QR)\" 힌트\n * - 접힘 + 안 들어감 → \"(terminal too small for QR — use URL)\" 힌트\n *\n * 잘린 QR 은 스캔 불가라 무가치하므로 들어갈 때만 그린다(판정 근거는 {@link qrLayout}). URL 은 호출부가\n * 항상 노출하므로 어느 상태에서도 폰으로 직접 입력할 폴백이 남는다. `reserveRows` 는 화면별 chrome 높이.\n */\nexport function PairQrCell({\n  qrText,\n  visible,\n  columns,\n  rows,\n  reserveRows,\n}: {\n  qrText: string | null\n  visible: boolean\n  columns: number\n  rows: number\n  reserveRows?: number\n}) {\n  if (!qrText) return null\n  const { width, height, fits } = qrLayout(qrText, columns, rows, reserveRows)\n\n  if (!visible) {\n    return (\n      <Text color=\"gray\">\n        {fits ? '(press c to show QR)' : '(terminal too small for QR — use URL)'}\n      </Text>\n    )\n  }\n  if (fits) return <Text>{qrText}</Text>\n  return (\n    <Text color=\"yellow\">\n      ⚠ QR needs {width}x{height}, terminal is {columns}x{rows} — widen the window or use the URL\n      below.\n    </Text>\n  )\n}\n","/**\n * 페어링 QR ASCII 가 현재 터미널에 잘리지 않고 들어가는지 판정한다 — 표시 분기 전용.\n *\n * **왜 필요한가**: QR 은 데이터량에 비례해 모듈 수가 고정되고(각 모듈 최소 1셀), `qrcode-terminal`\n * 의 `small` 은 세로만 절반으로 줄일 뿐 가로 폭은 줄일 수 없다. ECC 도 L 고정이라 더 압축 불가.\n * relay 딥링크 QR 은 약 43열 × 22행이 물리적 최소라 좁은 터미널에선 잘려 스캔 불가가 된다.\n * 게다가 본 TUI 는 alt screen buffer(스크롤 없음)라 넘치면 상단이 사라진다 → 들어갈 때만 그린다.\n */\n\n/** QR 외 UI 가 차지하는 행의 기본 추정치(MainScreen 의 단출한 레이아웃 기준). 화면별로 override. */\nexport const RESERVED_ROWS = 10\n\n/** QR ASCII 의 표시 크기(셀). 폭=가장 긴 줄의 코드포인트 수(서로게이트 안전), 높이=줄 수. */\nexport function qrDimensions(qrText: string): { width: number; height: number } {\n  const lines = qrText.split('\\n')\n  let width = 0\n  for (const line of lines) {\n    const w = [...line].length\n    if (w > width) width = w\n  }\n  return { width, height: lines.length }\n}\n\n/**\n * QR 의 표시 크기와, 현재 터미널(columns×rows)에 잘리지 않고 들어가는지를 **한 번에** 반환한다.\n *\n * 폭은 full `columns` 와 비교(QR 좌우엔 chrome 이 없어 양보 불필요)하지만, 세로는 위아래 chrome 과\n * 쌓이므로 `reserveRows` 만큼 양보한다. chrome 높이는 화면마다 다르므로(예: ManagerApp 은 로그 tail\n * 만큼 더 큼) 호출부가 자기 레이아웃에 맞는 값을 넘긴다 — 생략 시 {@link RESERVED_ROWS}.\n */\nexport function qrLayout(\n  qrText: string,\n  columns: number,\n  rows: number,\n  reserveRows: number = RESERVED_ROWS\n): { width: number; height: number; fits: boolean } {\n  const { width, height } = qrDimensions(qrText)\n  return { width, height, fits: width <= columns && height <= rows - reserveRows }\n}\n","import qrcode from 'qrcode-terminal'\n\n/**\n * url 을 ASCII QR 문자열로 변환한다. `qrcode-terminal` 은 콜백을 **동기** 호출하므로 즉시 캡처 가능.\n * 메인 화면(라이브 코어)과 관리 화면(코어 없이 재계산) 양쪽이 공유한다.\n */\nexport function renderQr(url: string): string {\n  let out = ''\n  qrcode.generate(url, { small: true }, s => {\n    out = s\n  })\n  return out.replace(/\\n$/, '')\n}\n","import { useStdout } from 'ink'\nimport { useEffect, useState } from 'react'\n\n/** stdout 크기 미상(파이프 등)일 때의 fallback — 표준 80×24. */\nconst FALLBACK = { columns: 80, rows: 24 }\n\n/**\n * 현재 터미널의 열·행을 반환하고 `resize` 이벤트로 라이브 갱신한다.\n *\n * QR fit 판정(가변 창)에 쓰인다 — 사용자가 SSH 창을 넓히면 즉시 QR 이 들어가도록 재평가하기 위함.\n * non-TTY(파이프)에서는 `columns/rows` 가 undefined 라 {@link FALLBACK} 으로 떨어진다.\n */\nexport function useTerminalSize(): { columns: number; rows: number } {\n  const { stdout } = useStdout()\n  const [size, setSize] = useState(() => ({\n    columns: stdout?.columns ?? FALLBACK.columns,\n    rows: stdout?.rows ?? FALLBACK.rows,\n  }))\n\n  useEffect(() => {\n    if (!stdout) return\n    const onResize = (): void => {\n      setSize({ columns: stdout.columns ?? FALLBACK.columns, rows: stdout.rows ?? FALLBACK.rows })\n    }\n    stdout.on('resize', onResize)\n    return () => {\n      stdout.off('resize', onResize)\n    }\n  }, [stdout])\n\n  return size\n}\n","import os from 'node:os'\r\nimport path from 'node:path'\r\n\r\nimport { Box, Text, useInput } from 'ink'\r\nimport React, { useEffect, useState } from 'react'\r\n\r\nimport type { AgentCore } from '@arva/core'\r\n\r\nimport type { HeadlessConfigStore } from '../../config/json-store'\r\nimport { identity } from '../../identity'\r\nimport type { HeadlessUiState } from '../../state'\r\nimport { Divider } from '../components/Divider'\r\nimport { prettyUrl } from '../pretty-url'\r\n\r\n/** relay 상태별 색(헤더와 동일 관용 — connected=초록 / connecting=노랑 / 그 외=빨강). */\r\nconst STATUS_COLOR: Record<string, string> = {\r\n  connected: 'green',\r\n  connecting: 'yellow',\r\n  disconnected: 'red',\r\n  error: 'red',\r\n}\r\n\r\n/**\r\n * 비-internal IPv4 주소 목록. WS 포트와 합쳐 LAN 페어링 엔드포인트를 추정하는 데 쓴다.\r\n * `os.networkInterfaces()` 는 전 플랫폼 동일 API라 분기 없이 안전하다.\r\n */\r\nfunction localAddresses(): string[] {\r\n  const out: string[] = []\r\n  for (const iface of Object.values(os.networkInterfaces())) {\r\n    if (!iface) continue\r\n    for (const addr of iface) {\r\n      if (addr.family === 'IPv4' && !addr.internal) out.push(addr.address)\r\n    }\r\n  }\r\n  return out\r\n}\r\n\r\n/** 라벨(고정폭) + 값 한 줄. 값이 길면 Ink 가 wrap 한다(pair url 등). */\r\nfunction Row({ label, value, color }: { label: string; value: string; color?: string }) {\r\n  return (\r\n    <Box>\r\n      <Text color=\"gray\">{`  ${label.padEnd(9)} `}</Text>\r\n      <Text color={color}>{value}</Text>\r\n    </Box>\r\n  )\r\n}\r\n\r\n/**\r\n * 정보(Info) 화면 — 읽기전용 진단 집약. 페어링 URL(토큰 url)·relay·로컬 서버·에이전트·타임아웃·\r\n * 시스템 정보를 한곳에 모은다. 편집은 OptionsScreen 담당(여긴 조회만).\r\n *\r\n * **보안**: relay 시크릿은 **값을 표시하지 않고** 설정 여부(✓/✗)만 노출한다(시크릿 비로깅 규약과 일관).\r\n * **키 소유권**: `useInput` 은 active(=현재 화면이 info)일 때만 Esc(뒤로)를 처리한다.\r\n * clients 수는 ws 내부 카운트가 신뢰 소스라 1s 폴링(MainScreen 과 동일 패턴).\r\n *\r\n * @param active 현재 라우터 화면이 info 인지(키 입력 게이팅).\r\n */\r\nexport function InfoScreen({\r\n  state,\r\n  config,\r\n  core,\r\n  active,\r\n  onBack,\r\n}: {\r\n  state: HeadlessUiState\r\n  config: HeadlessConfigStore\r\n  core: AgentCore\r\n  active: boolean\r\n  onBack: () => void\r\n}) {\r\n  const [clients, setClients] = useState(0)\r\n\r\n  useEffect(() => {\r\n    const t = setInterval(() => {\r\n      setClients(core.ws.getActiveMobileClientCount())\r\n    }, 1000)\r\n    return () => clearInterval(t)\r\n  }, [core])\r\n\r\n  useInput(\r\n    (_input, key) => {\r\n      if (key.escape) onBack()\r\n    },\r\n    { isActive: active }\r\n  )\r\n\r\n  const statusColor = STATUS_COLOR[state.relayStatus] ?? 'gray'\r\n  const retry = state.relayRetry ? ` (retry ${state.relayRetry})` : ''\r\n  const [primary, ...fallbacks] = config.getRelayUrls()\r\n  const secretSet = config.getRelayAgentSecret().length > 0\r\n  const lan = localAddresses()\r\n  const dataDir = path.join(os.homedir(), '.ai_remote_vibe_agent')\r\n\r\n  return (\r\n    <Box flexDirection=\"column\">\r\n      <Divider />\r\n      <Text>Info</Text>\r\n\r\n      <Divider />\r\n      <Text color=\"gray\">CONNECTION</Text>\r\n      {state.qr ? (\r\n        <>\r\n          <Row label=\"pair url\" value={prettyUrl(state.qr.url)} color=\"cyan\" />\r\n          <Row\r\n            label=\"mode\"\r\n            value={`${state.qr.mode}${state.qr.details.relayToken ? `   token ${state.qr.details.relayToken}` : ''}`}\r\n          />\r\n        </>\r\n      ) : (\r\n        <Row label=\"pair url\" value=\"Preparing… (waiting for relay)\" color=\"yellow\" />\r\n      )}\r\n\r\n      <Divider />\r\n      <Text color=\"gray\">RELAY</Text>\r\n      <Row label=\"status\" value={`${state.relayStatus}${retry}`} color={statusColor} />\r\n      <Row label=\"target\" value={primary ?? '-'} />\r\n      {fallbacks.map(url => (\r\n        <Row key={url} label=\"fallback\" value={url} />\r\n      ))}\r\n      <Row\r\n        label=\"secret\"\r\n        value={secretSet ? '✓ set' : '✗ missing (relay auth unavailable)'}\r\n        color={secretSet ? 'green' : 'red'}\r\n      />\r\n\r\n      <Divider />\r\n      <Text color=\"gray\">LOCAL SERVER</Text>\r\n      <Row label=\"ws port\" value={String(config.getWsPort())} />\r\n      <Row label=\"lan\" value={lan.length > 0 ? lan.join(', ') : '(none)'} />\r\n      <Row label=\"clients\" value={String(clients)} color={clients > 0 ? 'green' : 'gray'} />\r\n\r\n      <Divider />\r\n      <Text color=\"gray\">AGENT</Text>\r\n      <Row label=\"version\" value={identity.version} />\r\n      <Row label=\"install\" value={`${config.getInstallId().slice(0, 8)}…`} />\r\n      <Row label=\"fcm\" value={String(config.getFcmTokens().length)} />\r\n      <Row label=\"machines\" value={String(core.ssh.getMachines().length)} />\r\n\r\n      <Divider />\r\n      <Text color=\"gray\">TIMEOUTS</Text>\r\n      <Row\r\n        label=\"hb/po/au\"\r\n        value={`${config.getHeartbeatIntervalMs()} · ${config.getPongTimeoutMs()} · ${config.getAuthTimeoutMs()} ms`}\r\n      />\r\n\r\n      <Divider />\r\n      <Text color=\"gray\">SYSTEM</Text>\r\n      <Row label=\"os\" value={`${os.platform()} ${os.arch()} · ${os.hostname()}`} />\r\n      <Row label=\"node\" value={process.version} />\r\n      <Row label=\"data\" value={dataDir} />\r\n      <Row label=\"logs\" value={path.join(dataDir, 'logs')} />\r\n\r\n      <Divider />\r\n      <Text color=\"cyan\">Esc back</Text>\r\n    </Box>\r\n  )\r\n}\r\n","/**\n * 페어링 URL 을 사람이 읽기 쉬운 형태로 디코드한다 — **화면 표시 전용**.\n *\n * relay 모드 URL 은 `…/pair?u=<encodeURIComponent(ws)>` 구조라 `wss%3A%2F%2F…` 처럼 보여\n * 원격 SSH·작은 터미널에서 폰으로 직접 입력·공유하기 어렵다. percent-encoding 을 풀어\n * `…/pair?u=wss://relay…/mobile/<id>` 로 노출한다.\n *\n * **원본을 바꾸지 않는다**: QR 인코딩·실제 페어링에는 원시 URL(`state.qr.url`)을 써야 한다.\n * 표시값만 디코드하며, 디코드된 딥링크도 모바일 URL 파서(`searchParams.get('u')`)가 동일 ws 로\n * 환원하므로(ws 경로에 `&`/`#` 없음) 사람이 보고 직접 입력해도 동작한다.\n *\n * 깨진 `%` 시퀀스 등으로 decode 가 throw 하면 원본을 그대로 반환한다(표시 깨짐 방지).\n *\n * @param url `state.qr.url` (relay 딥링크 또는 LAN `ws://…` — LAN 은 `%` 없어 no-op).\n */\nexport function prettyUrl(url: string): string {\n  try {\n    return decodeURIComponent(url)\n  } catch {\n    return url\n  }\n}\n","import { Box, Text, useInput } from 'ink'\r\nimport React, { useState } from 'react'\r\n\r\nimport type { AgentCore } from '@arva/core'\r\nimport type { ConnectionState, Machine } from '@arva/shared/types'\r\n\r\nimport { ConfirmDialog } from '../components/ConfirmDialog'\r\nimport { Divider } from '../components/Divider'\r\nimport { MachineFormScreen } from './MachineFormScreen'\r\n\r\n/** 한 화면에 보일 머신 행 수(초과 시 위/아래 \"N more\"). */\r\nconst ROWS = 8\r\n\r\n/** 연결 상태별 도트 + 색. 데스크탑 MachineItem 과 동일 시맨틱(connected=green …). */\r\nconst STATUS_DOT: Record<ConnectionState, { dot: string; color: string }> = {\r\n  connected: { dot: '●', color: 'green' },\r\n  disconnected: { dot: '○', color: 'gray' },\r\n  connecting: { dot: '◌', color: 'yellow' },\r\n  error: { dot: '✗', color: 'red' },\r\n}\r\n\r\n/** 머신 연결 정보 한 줄: local 은 \"local\", ssh 는 user@host:port. */\r\nfunction connInfo(m: Machine): string {\r\n  const isLocal = m.machineType === 'local' || m.host === '127.0.0.1' || m.host === 'localhost'\r\n  return isLocal ? 'local' : `${m.username}@${m.host}:${m.port}`\r\n}\r\n\r\n/** 머신 한 행(선택 표시 ▶ + 이름 + 연결정보 + 상태 도트). */\r\nfunction MachineRow({ m, selected }: { m: Machine; selected: boolean }) {\r\n  const s = STATUS_DOT[m.status] ?? { dot: '·', color: 'gray' }\r\n  return (\r\n    <Box>\r\n      <Text color=\"cyan\">{selected ? '▶ ' : '  '}</Text>\r\n      <Box width={18}>\r\n        {/* 이름이 18폭을 넘으면 박스 안에서 wrap → 행이 2줄이 되어 리스트 윈도잉이 깨진다. truncate 로 1줄 고정. */}\r\n        <Text wrap=\"truncate\">{m.name}</Text>\r\n      </Box>\r\n      <Box width={24}>\r\n        <Text color=\"gray\" wrap=\"truncate\">\r\n          {connInfo(m)}\r\n        </Text>\r\n      </Box>\r\n      <Text color={s.color} wrap=\"truncate\">\r\n        {s.dot} {m.status}\r\n      </Text>\r\n    </Box>\r\n  )\r\n}\r\n\r\n/**\r\n * 머신 관리 화면 — 목록/선택(↑↓) + 추가(a)/수정(e)/삭제(d). 삭제는 ConfirmDialog 로 확인하고\r\n * `removeMachine` 후 **반드시 `broadcastMachines()`** 를 호출한다(코어 mutation 은 broadcast 를\r\n * 자동으로 하지 않음 — ws-manager 도 동일하게 짝지어 호출). broadcast 가 store 를 갱신해 목록이 라이브로 반영된다.\r\n *\r\n * **키 소유권**: list 모드일 때만 list `useInput` 이 활성. form/confirm sub-mode 에선 비활성으로 두어\r\n * 자식(폼 필드 / ConfirmDialog)이 키를 단독 소비한다(이중 처리 방지).\r\n *\r\n * @param machines App 이 `core.ssh.getMachines()` 로 매 렌더 전달하는 전체 Machine 목록(수정에 전 필드 필요).\r\n */\r\nexport function MachinesScreen({\r\n  machines,\r\n  core,\r\n  active,\r\n  onBack,\r\n}: {\r\n  machines: Machine[]\r\n  core: AgentCore\r\n  active: boolean\r\n  onBack: () => void\r\n}) {\r\n  const [selected, setSelected] = useState(0)\r\n  const [mode, setMode] = useState<'list' | 'form' | 'confirm'>('list')\r\n  const [editing, setEditing] = useState<Machine | null>(null)\r\n\r\n  const count = machines.length\r\n  // 삭제/외부 갱신으로 목록이 줄면 selected 가 범위를 벗어날 수 있어 읽을 때 클램프.\r\n  const sel = Math.min(selected, Math.max(0, count - 1))\r\n\r\n  useInput(\r\n    (input, key) => {\r\n      if (key.escape) {\r\n        onBack()\r\n      } else if (key.upArrow) {\r\n        setSelected(Math.max(0, sel - 1))\r\n      } else if (key.downArrow) {\r\n        setSelected(Math.min(count - 1, sel + 1))\r\n      } else if (input === 'a') {\r\n        setEditing(null)\r\n        setMode('form')\r\n      } else if (input === 'e') {\r\n        if (machines[sel]) {\r\n          setEditing(machines[sel])\r\n          setMode('form')\r\n        }\r\n      } else if (input === 'd') {\r\n        if (machines[sel]) setMode('confirm')\r\n      }\r\n    },\r\n    { isActive: active && mode === 'list' }\r\n  )\r\n\r\n  // confirm 모드 Esc 취소(ConfirmDialog 의 SelectInput 은 ↑↓/Enter 만 소비 → Esc 는 여기서).\r\n  useInput(\r\n    (_input, key) => {\r\n      if (key.escape) setMode('list')\r\n    },\r\n    { isActive: active && mode === 'confirm' }\r\n  )\r\n\r\n  if (mode === 'form') {\r\n    return (\r\n      <MachineFormScreen\r\n        machine={editing}\r\n        core={core}\r\n        active={active}\r\n        onDone={() => setMode('list')}\r\n        onCancel={() => setMode('list')}\r\n      />\r\n    )\r\n  }\r\n\r\n  // selected 가 보이도록 중앙 정렬 슬라이스(긴 목록 윈도잉).\r\n  const start = Math.max(0, Math.min(sel - Math.floor(ROWS / 2), Math.max(0, count - ROWS)))\r\n  const visible = machines.slice(start, start + ROWS)\r\n  const hiddenAbove = start\r\n  const hiddenBelow = Math.max(0, count - (start + ROWS))\r\n\r\n  const target = machines[sel]\r\n  const hasSession = target ? core.ssh.hasSession(target.id) : false\r\n  // local 머신은 코어가 부팅 시 자동 재시드 → 삭제는 세션 한정. 사용자에게 알린다.\r\n  const deleteWarning = target\r\n    ? target.machineType === 'local'\r\n      ? 'local machine — auto-restored on restart'\r\n      : hasSession\r\n        ? 'active session — will disconnect on delete'\r\n        : undefined\r\n    : undefined\r\n\r\n  return (\r\n    <Box flexDirection=\"column\">\r\n      <Divider />\r\n      <Text>Machines ({count})</Text>\r\n      {hiddenAbove > 0 ? <Text color=\"gray\">↑ {hiddenAbove} more</Text> : null}\r\n      {visible.map((m, i) => (\r\n        <MachineRow key={m.id} m={m} selected={start + i === sel} />\r\n      ))}\r\n      {hiddenBelow > 0 ? <Text color=\"gray\">↓ {hiddenBelow} more</Text> : null}\r\n      <Divider />\r\n      {mode === 'confirm' && target ? (\r\n        <ConfirmDialog\r\n          message={`Delete \"${target.name}\"?`}\r\n          confirmLabel=\"Delete\"\r\n          warning={deleteWarning}\r\n          isActive={active && mode === 'confirm'}\r\n          onConfirm={() => {\r\n            // 코어 mutation 은 useInput(SelectInput) 핸들러 안 — throw 시 TUI 가 죽지 않게 가드.\r\n            // 머신이 이미 사라졌다면 무시(broadcast/재조회로 목록이 정리됨).\r\n            try {\r\n              core.ssh.removeMachine(target.id)\r\n              core.ssh.broadcastMachines()\r\n            } catch {\r\n              // 이미 제거됨 — no-op\r\n            }\r\n            // count 는 삭제 전 길이 → count-2 = 삭제 후 마지막 유효 인덱스(읽기 클램프와 함께 안전).\r\n            setSelected(Math.max(0, Math.min(sel, count - 2)))\r\n            setMode('list')\r\n          }}\r\n          onCancel={() => setMode('list')}\r\n        />\r\n      ) : (\r\n        <Text color=\"cyan\">↑↓ select · a add · e edit · d delete · Esc back</Text>\r\n      )}\r\n    </Box>\r\n  )\r\n}\r\n","import { Box, Text } from 'ink'\nimport SelectInput from 'ink-select-input'\nimport React from 'react'\n\n/**\n * 파괴적 작업 확인 다이얼로그(머신 삭제 등). Cancel 이 첫 항목(안전 기본값)으로 하이라이트된다.\n *\n * **focus 모델**: `isActive` 일 때만 ↑↓/Enter 를 소비한다. 호출 화면은 다이얼로그가 떠 있는 동안\n * 자신의 리스트 `useInput` 을 비활성(`isActive:false`)으로 두어 키 이중 처리(목록 이동 + 다이얼로그\n * 선택 동시 발생)를 막는다.\n *\n * @param warning 선택적 경고 줄(예: \"활성 세션이 있습니다\") — 노란색으로 본문 아래 표기.\n */\nexport function ConfirmDialog({\n  message,\n  confirmLabel,\n  warning,\n  isActive,\n  onConfirm,\n  onCancel,\n}: {\n  message: string\n  confirmLabel: string\n  warning?: string\n  isActive: boolean\n  onConfirm: () => void\n  onCancel: () => void\n}) {\n  return (\n    <Box flexDirection=\"column\" borderStyle=\"round\" borderColor=\"red\" paddingX={1}>\n      <Text>{message}</Text>\n      {warning ? <Text color=\"yellow\">⚠ {warning}</Text> : null}\n      <Box marginTop={1}>\n        <SelectInput\n          isFocused={isActive}\n          items={[\n            { key: 'cancel', label: 'Cancel', value: 'cancel' },\n            { key: 'confirm', label: confirmLabel, value: 'confirm' },\n          ]}\n          onSelect={item => (item.value === 'confirm' ? onConfirm() : onCancel())}\n        />\n      </Box>\n    </Box>\n  )\n}\n","import path from 'node:path'\r\n\r\nimport { Box, Text } from 'ink'\r\nimport React, { useEffect, useMemo, useState } from 'react'\r\n\r\nimport type { AgentCore } from '@arva/core'\r\nimport type { Machine } from '@arva/shared/types'\r\n\r\nimport { validateMachineForm } from '../../config/validate'\r\nimport { Divider } from '../components/Divider'\r\nimport { Field } from '../components/Field'\r\nimport { SelectField } from '../components/SelectField'\r\nimport { useFieldFocus } from '../useFieldFocus'\r\nimport { type MachineFormState, buildMachinePayload, machineToForm } from './machine-payload'\r\n\r\n/**\r\n * 머신 추가/수정 폼. add(machine=null)는 type 선택부터, edit 는 type 고정(데스크탑과 동일 — 생성 후 타입 불변).\r\n *\r\n * **키 소유권 / focus**: 폼 레벨 `useInput`(active) 은 Tab(다음 필드)·Esc(취소)만 처리한다. 타이핑/Enter/↑↓는\r\n * 포커스된 필드(TextInput/SelectInput)가 단독 소비 → 한 시점에 한 필드만 키를 받는다. 비포커스 SelectField 는\r\n * 드롭다운을 마운트하지 않아 잔여 `useInput` 이 없다. Tab=필드 이동, ↑↓=포커스된 select 내부 이동으로 분리.\r\n *\r\n * **저장**: 검증 통과 시 add→`addMachine`, edit→`editMachine`(비파괴 머지 payload) 후 **반드시\r\n * `broadcastMachines()`**(코어 mutation 은 broadcast 안 함). 그 broadcast 가 목록을 라이브 갱신한다.\r\n *\r\n * @param machine null=추가, 값=수정(전체 Machine — 고급 옵션 보존 베이스).\r\n */\r\nexport function MachineFormScreen({\r\n  machine: base,\r\n  core,\r\n  active,\r\n  onDone,\r\n  onCancel,\r\n}: {\r\n  machine: Machine | null\r\n  core: AgentCore\r\n  active: boolean\r\n  onDone: () => void\r\n  onCancel: () => void\r\n}) {\r\n  const [form, setForm] = useState<MachineFormState>(() => machineToForm(base))\r\n  const [errors, setErrors] = useState<string[]>([])\r\n  const keys = useMemo(() => core.ssh.listAvailableKeys(), [core])\r\n\r\n  const set = (patch: Partial<MachineFormState>) => setForm(f => ({ ...f, ...patch }))\r\n\r\n  // key 인증인데 선택 키가 없고 사용 가능한 키가 있으면 첫 키를 기본 선택(표시값=실제 선택값 일치).\r\n  useEffect(() => {\r\n    if (\r\n      form.machineType === 'ssh' &&\r\n      form.authMethod === 'key' &&\r\n      !form.privateKeyPath &&\r\n      keys.length > 0\r\n    ) {\r\n      set({ privateKeyPath: keys[0] })\r\n    }\r\n    // eslint-disable-next-line react-hooks/exhaustive-deps\r\n  }, [form.machineType, form.authMethod, keys])\r\n\r\n  // 활성 필드 순서(타입/인증에 따라 동적). type 은 add 에서만(edit 은 타입 불변).\r\n  const order: string[] = []\r\n  if (!base) order.push('type')\r\n  order.push('name')\r\n  if (form.machineType === 'ssh') {\r\n    order.push('host', 'port', 'username', 'auth', form.authMethod === 'key' ? 'key' : 'password')\r\n  } else {\r\n    order.push('localShell', 'localCwd')\r\n  }\r\n\r\n  const submit = () => {\r\n    const errs = validateMachineForm({\r\n      machineType: form.machineType,\r\n      name: form.name,\r\n      host: form.host,\r\n      port: Number.parseInt(form.port, 10),\r\n      username: form.username,\r\n      authMethod: form.authMethod,\r\n      privateKeyPath: form.privateKeyPath,\r\n      password: form.password,\r\n      isAdd: !base,\r\n    })\r\n    if (errs.length > 0) {\r\n      setErrors(errs)\r\n      return\r\n    }\r\n    // 코어 mutation 은 useInput 핸들러 안에서 호출된다. editMachine 은 머신이 폰에서 삭제되면\r\n    // 'not found Machine' 을 throw 하는데, Ink 는 핸들러 throw 를 잡지 않아 렌더 루프가 무너지고\r\n    // 헤드리스 에이전트(+모든 폰 세션)가 죽는다. → try/catch 로 인라인 에러 전환.\r\n    try {\r\n      const payload = buildMachinePayload(base, form)\r\n      if (base) core.ssh.editMachine({ ...payload, id: base.id })\r\n      else core.ssh.addMachine(payload)\r\n      core.ssh.broadcastMachines()\r\n      onDone()\r\n    } catch {\r\n      setErrors(['save'])\r\n    }\r\n  }\r\n\r\n  const { focusedKey, advance } = useFieldFocus(order, {\r\n    isActive: active,\r\n    onSubmit: submit,\r\n    onEscape: onCancel,\r\n  })\r\n\r\n  const invalid = (k: string) => errors.includes(k)\r\n\r\n  // 인증 필드: key(목록 선택) / key 없음(password 전환 안내) / password 입력. return JSX 평탄화를 위해 분리.\r\n  const authField =\r\n    form.authMethod === 'key' ? (\r\n      keys.length > 0 ? (\r\n        <SelectField\r\n          label=\"Key\"\r\n          value={form.privateKeyPath || keys[0]}\r\n          isFocused={focusedKey === 'key'}\r\n          options={keys.map(k => ({ label: path.basename(k), value: k }))}\r\n          onSelect={v => {\r\n            set({ privateKeyPath: v })\r\n            advance('key')\r\n          }}\r\n        />\r\n      ) : (\r\n        <Box>\r\n          <Box width={12}>\r\n            <Text color={focusedKey === 'key' ? 'cyan' : 'gray'}>Key</Text>\r\n          </Box>\r\n          <Text color=\"yellow\">no SSH keys — switch Auth to password</Text>\r\n        </Box>\r\n      )\r\n    ) : (\r\n      <Field\r\n        label=\"Password\"\r\n        value={form.password}\r\n        onChange={v => set({ password: v })}\r\n        focus={focusedKey === 'password'}\r\n        onSubmit={() => advance('password')}\r\n        mask=\"*\"\r\n        placeholder={base ? (base.passwordLost ? '(lost — re-enter)' : '(unchanged)') : ''}\r\n        invalid={invalid('password')}\r\n      />\r\n    )\r\n\r\n  return (\r\n    <Box flexDirection=\"column\">\r\n      <Text>{base ? `Edit ${base.name}` : 'Add machine'}</Text>\r\n      <Divider />\r\n\r\n      {order.includes('type') ? (\r\n        <SelectField\r\n          label=\"Type\"\r\n          value={form.machineType}\r\n          isFocused={focusedKey === 'type'}\r\n          options={[\r\n            { label: 'ssh', value: 'ssh' },\r\n            { label: 'local', value: 'local' },\r\n          ]}\r\n          onSelect={v => {\r\n            set({ machineType: v })\r\n            advance('type')\r\n          }}\r\n        />\r\n      ) : null}\r\n\r\n      <Field\r\n        label=\"Name\"\r\n        value={form.name}\r\n        onChange={v => set({ name: v })}\r\n        focus={focusedKey === 'name'}\r\n        onSubmit={() => advance('name')}\r\n        invalid={invalid('name')}\r\n      />\r\n\r\n      {form.machineType === 'ssh' ? (\r\n        <>\r\n          <Field\r\n            label=\"Host\"\r\n            value={form.host}\r\n            onChange={v => set({ host: v })}\r\n            focus={focusedKey === 'host'}\r\n            onSubmit={() => advance('host')}\r\n            invalid={invalid('host')}\r\n          />\r\n          <Field\r\n            label=\"Port\"\r\n            value={form.port}\r\n            onChange={v => set({ port: v.replace(/[^0-9]/g, '') })}\r\n            focus={focusedKey === 'port'}\r\n            onSubmit={() => advance('port')}\r\n            invalid={invalid('port')}\r\n          />\r\n          <Field\r\n            label=\"Username\"\r\n            value={form.username}\r\n            onChange={v => set({ username: v })}\r\n            focus={focusedKey === 'username'}\r\n            onSubmit={() => advance('username')}\r\n            invalid={invalid('username')}\r\n          />\r\n          <SelectField\r\n            label=\"Auth\"\r\n            value={form.authMethod}\r\n            isFocused={focusedKey === 'auth'}\r\n            options={[\r\n              { label: 'key', value: 'key' },\r\n              { label: 'password', value: 'password' },\r\n            ]}\r\n            onSelect={v => {\r\n              set({ authMethod: v })\r\n              advance('auth')\r\n            }}\r\n          />\r\n          {authField}\r\n        </>\r\n      ) : (\r\n        <>\r\n          <Field\r\n            label=\"Shell\"\r\n            value={form.localShell}\r\n            onChange={v => set({ localShell: v })}\r\n            focus={focusedKey === 'localShell'}\r\n            onSubmit={() => advance('localShell')}\r\n            placeholder=\"(auto)\"\r\n          />\r\n          <Field\r\n            label=\"Cwd\"\r\n            value={form.localCwd}\r\n            onChange={v => set({ localCwd: v })}\r\n            focus={focusedKey === 'localCwd'}\r\n            onSubmit={() => advance('localCwd')}\r\n            placeholder=\"(home)\"\r\n          />\r\n        </>\r\n      )}\r\n\r\n      <Divider />\r\n      {errors.length > 0 ? (\r\n        <Text color=\"red\">\r\n          {errors.includes('save')\r\n            ? 'Save failed — machine may have been changed/deleted'\r\n            : `Required/format error: ${errors.join(', ')}`}\r\n        </Text>\r\n      ) : null}\r\n      <Text color=\"cyan\">Tab move · Enter next · Ctrl+S save · Esc cancel</Text>\r\n    </Box>\r\n  )\r\n}\r\n","/**\n * 옵션 입력 검증(순수 함수). TUI 옵션 화면이 저장 전에 호출하고, 단위 테스트가 동일 함수를 검증한다.\n *\n * setter({@link ./json-store})는 영속만 책임지므로, 잘못된 값이 파일에 들어가지 않도록 입력 경계(UI)에서\n * 이 함수들로 막는다. 사용자 노출 메시지는 호출 측이 만든다(여기선 boolean/사유 코드만).\n */\n\n/** relay URL 스킴 검증 — `ws://` 또는 `wss://` 만 허용(코어가 WebSocket 으로 접속). */\nexport function isValidRelayUrl(url: string): boolean {\n  const u = url.trim()\n  if (!(u.startsWith('ws://') || u.startsWith('wss://'))) return false\n  try {\n    // URL 파서로 host 존재까지 확인(ws://  처럼 host 누락 거부).\n    return new URL(u).host.length > 0\n  } catch {\n    return false\n  }\n}\n\n/** WS 포트 검증 — 1..65535 정수. */\nexport function isValidPort(n: number): boolean {\n  return Number.isInteger(n) && n >= 1 && n <= 65535\n}\n\n/** 타임아웃(ms) 검증 — 양의 정수. */\nexport function isValidTimeoutMs(n: number): boolean {\n  return Number.isInteger(n) && n > 0\n}\n\n/**\n * 머신 폼 필수/숫자/인증 검증. SSH 머신은 host/username 필수, port 는 1..65535.\n * local 머신은 host/port/username 검증 생략(코어가 127.0.0.1 로 채움).\n *\n * 인증: key 인증이면 privateKeyPath 필수(SSH 키 0개면 선택 불가 → password 로 전환 유도).\n * password 인증은 **추가(isAdd)** 시에만 비밀번호 필수 — 편집은 빈칸=기존 유지라 검증 제외.\n *\n * @returns 유효하면 빈 배열, 아니면 위반 필드 키 목록(UI 가 i18n 없이 인라인 표기).\n */\nexport function validateMachineForm(input: {\n  machineType: 'local' | 'ssh'\n  name: string\n  host: string\n  port: number\n  username: string\n  authMethod?: 'key' | 'password'\n  privateKeyPath?: string\n  password?: string\n  isAdd?: boolean\n}): string[] {\n  const errs: string[] = []\n  if (!input.name.trim()) errs.push('name')\n  if (input.machineType === 'ssh') {\n    if (!input.host.trim()) errs.push('host')\n    if (!input.username.trim()) errs.push('username')\n    if (!isValidPort(input.port)) errs.push('port')\n    if (input.authMethod === 'key' && !(input.privateKeyPath ?? '').trim()) errs.push('key')\n    if (input.authMethod === 'password' && input.isAdd && !(input.password ?? '').trim()) {\n      errs.push('password')\n    }\n  }\n  return errs\n}\n","import { Box, Text } from 'ink'\nimport TextInput from 'ink-text-input'\nimport React from 'react'\n\n/**\n * 라벨 + 텍스트 입력 한 줄. 머신/옵션 폼의 기본 입력 단위.\n *\n * **focus 모델**: `focus` 가 true 인 필드만 키 입력을 소비한다(`ink-text-input` 의 `focus` prop).\n * 폼은 한 시점에 한 필드만 `focus=true` 로 둔다 — 이렇게 하지 않으면 Ink 가 핸들러를 stop-propagate\n * 하지 않아 여러 입력에 같은 키가 동시에 들어간다(폼에서 'q' 타이핑 시 앱 종료되는 버그의 원인).\n * 필드 간 이동(Tab)은 폼 레벨 `useInput` 이 담당하고, 타이핑/Enter 만 여기서 처리한다.\n */\nexport function Field({\n  label,\n  value,\n  onChange,\n  onSubmit,\n  focus,\n  placeholder,\n  mask,\n  invalid,\n}: {\n  label: string\n  value: string\n  onChange: (v: string) => void\n  /** Enter 시 호출 — 폼이 다음 필드로 advance 하는 데 사용. */\n  onSubmit?: () => void\n  focus: boolean\n  placeholder?: string\n  /** 비밀번호 입력처럼 마스킹할 문자(예: '*'). */\n  mask?: string\n  /** 검증 실패 표시(빨간 ✗). 메시지는 폼이 별도 표기. */\n  invalid?: boolean\n}) {\n  return (\n    <Box>\n      <Box width={12}>\n        <Text color={focus ? 'cyan' : 'gray'}>{label}</Text>\n      </Box>\n      <TextInput\n        value={value}\n        onChange={onChange}\n        onSubmit={onSubmit}\n        focus={focus}\n        placeholder={placeholder}\n        mask={mask}\n      />\n      {invalid ? <Text color=\"red\"> ✗</Text> : null}\n    </Box>\n  )\n}\n","import { Box, Text } from 'ink'\nimport SelectInput from 'ink-select-input'\nimport React from 'react'\n\n/** SelectField 한 항목 — 표시 라벨과 값. 값 타입은 호출 측이 지정. */\nexport interface SelectOption<V extends string> {\n  label: string\n  value: V\n}\n\n/**\n * 라벨 + 세로 선택 목록(머신 타입/인증/SSH 키 등 열거형 선택).\n *\n * **focus 모델**: `isFocused` 일 때만 ↑↓ 로 항목 이동 + Enter 선택을 소비한다. 필드 간 이동은\n * 폼 레벨 Tab 이 담당하므로 ↑↓ 충돌이 없다(Tab=필드 이동, ↑↓=포커스된 select 내부 이동).\n * 현재 값은 indicator(▶)가 아니라 라벨 옆 `= value` 로도 보여 비포커스 상태에서 선택값을 식별한다.\n *\n * @param onSelect 항목 확정 시 호출(폼이 상태 갱신 + 다음 필드 advance 에 사용).\n */\nexport function SelectField<V extends string>({\n  label,\n  options,\n  value,\n  isFocused,\n  onSelect,\n}: {\n  label: string\n  options: Array<SelectOption<V>>\n  value: V\n  isFocused: boolean\n  onSelect: (value: V) => void\n}) {\n  const initialIndex = Math.max(\n    0,\n    options.findIndex(o => o.value === value)\n  )\n  const current = options.find(o => o.value === value)\n  return (\n    <Box flexDirection=\"column\">\n      <Box>\n        <Box width={12}>\n          <Text color={isFocused ? 'cyan' : 'gray'}>{label}</Text>\n        </Box>\n        <Text>{current?.label ?? value}</Text>\n      </Box>\n      {isFocused ? (\n        <Box marginLeft={12}>\n          <SelectInput\n            items={options.map(o => ({ key: o.value, label: o.label, value: o.value }))}\n            isFocused={isFocused}\n            initialIndex={initialIndex}\n            onSelect={item => onSelect(item.value)}\n          />\n        </Box>\n      ) : null}\n    </Box>\n  )\n}\n","import { useInput } from 'ink'\r\nimport { useState } from 'react'\r\n\r\n/**\r\n * Tab/Enter 기반 폼 필드 포커스 상태머신. MachineForm·Options 두 폼이 공유한다(동일 로직 중복 제거).\r\n *\r\n * `order` 는 매 렌더 동적일 수 있어(머신 폼은 type/auth 에 따라 필드 집합이 바뀜) 인자로 받고,\r\n * `focusedKey` 는 읽을 때 clamp 한다 — 필드가 줄어 stale focused 인덱스가 범위를 벗어나도 안전.\r\n * Tab=다음 필드(wrap), Esc=onEscape, 마지막 필드에서 `advance`=onSubmit.\r\n *\r\n * **어느 필드에서나 저장(Ctrl+S)**: 마지막 필드까지 Enter 로 이동해야만 저장되던 흐름이 \"저장 방법을\r\n * 못 찾는\" 문제로 이어져, 폼 레벨에서 Ctrl+S 를 onSubmit 에 바인딩한다. 문자 키가 아니라 ctrl 조합이라\r\n * 포커스된 TextInput/SelectInput 이 소비하지 않아(입력값에 안 섞임) 어떤 필드에서든 안전하게 저장된다.\r\n *\r\n * **키 소유권**: `useInput` 은 `isActive` 일 때만 동작한다. Ink 는 핸들러를 stop-propagate 하지 않아\r\n * 게이팅이 없으면 비활성 화면의 Tab/Esc 가 동시 발동한다.\r\n *\r\n * @returns `focusedKey`(현재 포커스 필드 키)와 `advance(key)`(Enter/select 확정 → 다음 필드 또는 제출).\r\n */\r\nexport function useFieldFocus<K extends string>(\r\n  order: readonly K[],\r\n  opts: { isActive: boolean; onSubmit: () => void; onEscape: () => void }\r\n): { focusedKey: K; advance: (key: K) => void } {\r\n  const [focused, setFocused] = useState(0)\r\n  const focusedKey = order[Math.min(focused, order.length - 1)]\r\n\r\n  const advance = (key: K): void => {\r\n    const i = order.indexOf(key)\r\n    if (i >= order.length - 1) opts.onSubmit()\r\n    else setFocused(i + 1)\r\n  }\r\n\r\n  useInput(\r\n    (input, key) => {\r\n      if (key.escape) opts.onEscape()\r\n      else if (key.ctrl && input === 's') opts.onSubmit()\r\n      else if (key.tab) setFocused(f => (Math.min(f, order.length - 1) + 1) % order.length)\r\n    },\r\n    { isActive: opts.isActive }\r\n  )\r\n\r\n  return { focusedKey, advance }\r\n}\r\n","import os from 'node:os'\n\nimport type { Machine } from '@arva/shared/types'\n\n/** 머신 폼이 제어하는 입력 상태(전부 문자열 — TextInput 기반, port 는 제출 시 parse). */\nexport interface MachineFormState {\n  machineType: 'local' | 'ssh'\n  name: string\n  host: string\n  port: string\n  username: string\n  authMethod: 'key' | 'password'\n  privateKeyPath: string\n  /** 빈 문자열 = 미입력. 편집 시 미입력은 기존 비밀번호 유지(빈값으로 덮어쓰지 않음). */\n  password: string\n  localShell: string\n  localCwd: string\n}\n\n/** base 의 id/status 를 떼낸 나머지(고급 옵션 보존용 — multiplexer/ringBufferBytes/spawn* 등). */\nfunction carryOver(base: Machine | null): Partial<Machine> {\n  if (!base) return {}\n  const { id: _id, status: _status, ...rest } = base\n  return rest\n}\n\n/** Machine → 폼 초기 상태. add(null)는 기본값, edit 는 기존 값(password 는 항상 빈칸으로 시작). */\nexport function machineToForm(m: Machine | null): MachineFormState {\n  const isLocal = m?.machineType === 'local'\n  return {\n    machineType: m?.machineType ?? 'ssh',\n    name: m?.name ?? '',\n    host: m && !isLocal ? m.host : '',\n    port: m ? String(m.port) : '22',\n    username: m && !isLocal ? m.username : '',\n    authMethod: m?.authMethod ?? 'key',\n    privateKeyPath: m?.privateKeyPath ?? '',\n    password: '',\n    localShell: m?.localShell ?? '',\n    localCwd: m?.localCwd ?? '',\n  }\n}\n\n/**\n * 폼 상태 → addMachine/editMachine payload(Omit<Machine,'id'|'status'>).\n *\n * **비파괴 머지(리뷰 F3)**: edit 시 base 의 전 필드를 먼저 펼치고 폼이 제어하는 필드만 덮어쓴다 →\n * 폼에 없는 고급 옵션(multiplexer/ringBufferBytes/spawn*)이 보존된다. password 는 사용자가 새로\n * 입력하지 않으면 base 값을 유지(빈 문자열로 덮어써 `passwordLost` 가 되는 사고 방지). privateKeyPath\n * 도 마찬가지로 미선택 시 base 값을 유지한다.\n */\nexport function buildMachinePayload(\n  base: Machine | null,\n  f: MachineFormState\n): Omit<Machine, 'id' | 'status'> {\n  const carried = carryOver(base)\n  if (f.machineType === 'local') {\n    return {\n      ...carried,\n      machineType: 'local',\n      name: f.name.trim(),\n      host: base?.host ?? '127.0.0.1',\n      port: base?.port ?? 22,\n      username: base?.username ?? os.userInfo().username,\n      localShell: f.localShell.trim() || base?.localShell,\n      localCwd: f.localCwd.trim() || base?.localCwd,\n    }\n  }\n  return {\n    ...carried,\n    machineType: 'ssh',\n    name: f.name.trim(),\n    host: f.host.trim(),\n    port: Number.parseInt(f.port, 10),\n    username: f.username.trim(),\n    authMethod: f.authMethod,\n    privateKeyPath: f.authMethod === 'key' ? f.privateKeyPath || undefined : base?.privateKeyPath,\n    password: f.authMethod === 'password' ? f.password || base?.password : base?.password,\n  }\n}\n","import { Box, Text, useInput } from 'ink'\r\nimport React, { useEffect, useMemo, useState } from 'react'\r\n\r\nimport type { AgentCore } from '@arva/core'\r\n\r\nimport { getServiceBackend, resolveExec } from '../../service'\r\nimport { runSelfUpdate } from '../../update'\r\nimport { Divider } from '../components/Divider'\r\nimport { LogLines } from '../components/LogLines'\r\nimport { PairQrCell } from '../components/PairQrCell'\r\nimport type { HeadlessUiState, PtyState } from '../../state'\r\nimport { identity } from '../../identity'\r\nimport { prettyUrl } from '../pretty-url'\r\nimport { renderQr } from '../qr'\r\nimport { type SessionWindow, scrollWindow, sessionWindow } from '../scroll-window'\r\nimport { useTerminalSize } from '../useTerminalSize'\r\n\r\n/** verbose 로그 뷰에 한 번에 보일 줄 수(스크롤 윈도). */\r\nconst VERBOSE_ROWS = 16\r\n\r\n/** PTY 상태별 색(idle=회색, running=초록, dead/oom=빨강). */\r\nconst PTY_COLOR: Record<PtyState, string> = {\r\n  idle: 'gray',\r\n  running: 'green',\r\n  dead: 'red',\r\n  oom: 'red',\r\n}\r\n\r\n/**\r\n * 기본 화면 본문 — 세션(머신/PTY) · 클라이언트 수 · 페어링 URL(+ 토글형 QR).\r\n * 로그 스팸 없이 \"지금 무슨 상태인지\"만 보여준다. 상세는 verbose(`v`)로.\r\n *\r\n * 세션 리스트는 MainScreen 이 계산한 `sessWin`(상단 기준 윈도)으로 잘라 렌더한다 — 전부 렌더하면\r\n * 세션이 많을 때 하단 chrome(Clients·키 힌트)이 터미널 밖으로 밀려 잘리기 때문. 윈도 계산은 힌트 표시에도\r\n * 필요해 부모가 한 번만 하고 내려준다(같은 계산 중복 방지).\r\n *\r\n * 페어링 URL 은 **항상 텍스트로** 노출한다 — 카메라로 못 찍는 환경(원격 SSH·작은 터미널)에서 폰에\r\n * 직접 입력/공유할 수 있게. relay 모드면 토큰 포함 딥링크, LAN 모드면 `ws://…/mobile`.\r\n * QR ASCII 는 화면을 크게 차지하므로 **기본 숨김** — `c` 토글(`qrVisible`)로 펼친다.\r\n */\r\nfunction DefaultBody({\r\n  sessions,\r\n  sessWin,\r\n  clients,\r\n  qrText,\r\n  qrUrl,\r\n  qrMode,\r\n  qrVisible,\r\n  columns,\r\n  rows,\r\n}: {\r\n  sessions: Array<{ id: string; name: string; pty: PtyState }>\r\n  sessWin: SessionWindow\r\n  clients: number\r\n  qrText: string | null\r\n  qrUrl: string | null\r\n  qrMode: string | null\r\n  qrVisible: boolean\r\n  columns: number\r\n  rows: number\r\n}) {\r\n  // 윈도(상단 기준)는 MainScreen 이 한 번 계산해 내려준다 — 슬라이스만 적용.\r\n  const { start, end, hiddenAbove, hiddenBelow } = sessWin\r\n  const view = sessions.slice(start, end)\r\n  return (\r\n    <Box flexDirection=\"column\">\r\n      <Divider />\r\n\r\n      {qrUrl ? (\r\n        <Box flexDirection=\"column\">\r\n          <Text color=\"gray\">Scan to pair:</Text>\r\n          <PairQrCell qrText={qrText} visible={qrVisible} columns={columns} rows={rows} />\r\n          <Box>\r\n            <Text color=\"gray\">URL </Text>\r\n            <Text color=\"cyan\">{qrUrl}</Text>\r\n            {qrMode ? <Text color=\"gray\">  ({qrMode})</Text> : null}\r\n          </Box>\r\n        </Box>\r\n      ) : (\r\n        <Text color=\"yellow\">Preparing QR… (waiting for relay)</Text>\r\n      )}\r\n\r\n      <Divider />\r\n\r\n      <Text color=\"gray\">Sessions</Text>\r\n      {hiddenAbove > 0 ? <Text color=\"gray\">↑ {hiddenAbove} more</Text> : null}\r\n      {view.map(sess => (\r\n        <Box key={sess.id}>\r\n          <Text color=\"gray\">{'  '}</Text>\r\n          {/* minWidth={0} 로 이름 박스가 내용보다 좁게 shrink → truncate 발동(긴 이름이 wrap 돼 세션 윈도 높이가 깨지는 것 방지). */}\r\n          <Box minWidth={0}>\r\n            <Text wrap=\"truncate\">{sess.name}</Text>\r\n          </Box>\r\n          <Text color=\"gray\"> · </Text>\r\n          <Text color={PTY_COLOR[sess.pty]}>{sess.pty}</Text>\r\n        </Box>\r\n      ))}\r\n      {hiddenBelow > 0 ? <Text color=\"gray\">↓ {hiddenBelow} more</Text> : null}\r\n      <Box>\r\n        <Text color=\"gray\">Clients </Text>\r\n        <Text color={clients > 0 ? 'green' : 'gray'}>{clients}</Text>\r\n      </Box>\r\n    </Box>\r\n  )\r\n}\r\n\r\n/**\r\n * verbose 로그 뷰 — control-plane 이벤트를 시간순으로, `↑/↓` 스크롤.\r\n */\r\nfunction VerboseLog({ logs, scroll }: { logs: string[]; scroll: number }) {\r\n  const { start, end, hiddenAbove, hiddenBelow } = scrollWindow(logs.length, scroll, VERBOSE_ROWS)\r\n  const view = logs.slice(start, end)\r\n  return (\r\n    <Box flexDirection=\"column\">\r\n      {hiddenAbove > 0 ? (\r\n        <Text color=\"gray\" wrap=\"truncate\">\r\n          ↑ {hiddenAbove} more\r\n        </Text>\r\n      ) : (\r\n        <Divider />\r\n      )}\r\n      <LogLines lines={view} />\r\n      {hiddenBelow > 0 ? <Text color=\"gray\">↓ {hiddenBelow} more</Text> : null}\r\n    </Box>\r\n  )\r\n}\r\n\r\n/**\r\n * 메인(모니터) 화면 — 세션·클라이언트·URL(+`c` 토글 QR) + verbose 로그 토글. `m` 머신 / `o` 옵션 / `i` 정보 전환.\r\n *\r\n * **키 소유권**: `useInput` 은 `isActive`(=현재 화면이 main) 일 때만 동작한다. `q`(종료)가 main 에서만\r\n * 살아 있어, 머신/옵션 폼에서 'q' 타이핑이 앱을 종료시키는 사고를 구조적으로 막는다(다른 화면은 Esc→main 후 q).\r\n *\r\n * @param active 현재 라우터 화면이 main 인지(키 입력 게이팅).\r\n * @param onNavigate 화면 전환 콜백(App 의 screen state 변경).\r\n * @param onExit 종료(App 의 Ink exit 호출).\r\n */\r\nexport function MainScreen({\r\n  state,\r\n  core,\r\n  active,\r\n  onNavigate,\r\n  onExit,\r\n}: {\r\n  state: HeadlessUiState\r\n  core: AgentCore\r\n  active: boolean\r\n  onNavigate: (screen: 'machines' | 'options' | 'info' | 'getApps') => void\r\n  onExit: () => void\r\n}) {\r\n  const { columns, rows } = useTerminalSize()\r\n  const [verbose, setVerbose] = useState(false)\r\n  const [scroll, setScroll] = useState(0)\r\n  // 기본 화면 Sessions 리스트의 상단 기준 스크롤 offset(verbose 로그 scroll 과 분리 — 모드별 ↑↓ 소유).\r\n  const [sessionScroll, setSessionScroll] = useState(0)\r\n  const [clients, setClients] = useState(0)\r\n  // QR ASCII 는 화면을 크게 차지 → 기본 숨김(URL 텍스트만). `c`(QR code)로 토글 — 소문자 q=종료와 헷갈리던 Shift+Q 를 대체.\r\n  const [qrVisible, setQrVisible] = useState(false)\r\n  // 자동 시작(systemd): `a` 는 부팅 enable 만 하고, 실제 백그라운드 전환은 'q' 종료 시 핸드오프\r\n  // (포그라운드 코어 cleanup → 서비스 start)로 처리한다. 즉시 --now 로 띄우면 같은 포트·install_id 로\r\n  // 두 번째 인스턴스가 충돌하기 때문(footgun). 서비스 가동 중 재접속은 cli 가 ManagerApp 으로 분기.\r\n  const [handoffOnQuit, setHandoffOnQuit] = useState(false)\r\n  const [autostartNotice, setAutostartNotice] = useState<string | null>(null)\r\n  // 업데이트 실패 메시지(성공은 프로세스 재시작으로 화면을 벗어나므로 실패만 표시 — 항상 빨간 글씨).\r\n  const [updateError, setUpdateError] = useState<string | null>(null)\r\n  const [isUpdating, setIsUpdating] = useState(false)\r\n\r\n  // 폰 접속 수는 ws 내부 카운트가 신뢰 소스 — 1s 폴링(동일값이면 setState skip).\r\n  useEffect(() => {\r\n    const t = setInterval(() => {\r\n      setClients(core.ws.getActiveMobileClientCount())\r\n    }, 1000)\r\n    return () => clearInterval(t)\r\n  }, [core])\r\n\r\n  // 자동 시작 토글 — 미등록이면 등록(부팅 enable, start 는 q 핸드오프로 미룸), 등록 상태면 해제.\r\n  const toggleAutostart = (): void => {\r\n    const backend = getServiceBackend()\r\n    if (!backend.isSupported()) {\r\n      setAutostartNotice('auto-start is not supported on this platform')\r\n      return\r\n    }\r\n    const st = backend.status()\r\n    if (st.enabled) {\r\n      backend.uninstall()\r\n      setHandoffOnQuit(false)\r\n      setAutostartNotice('auto-start off')\r\n      return\r\n    }\r\n    const exec = resolveExec()\r\n    if (exec.isNpxCache) {\r\n      setAutostartNotice('npx is unsuitable as a service — install with npm i -g @junyoung-kim/reins first')\r\n      return\r\n    }\r\n    const res = backend.install(exec)\r\n    if (!res.ok) {\r\n      setAutostartNotice('auto-start registration failed (check service status)')\r\n      return\r\n    }\r\n    setHandoffOnQuit(true)\r\n    // persistCommand 가 있으면(linux linger off) 1줄 안내를 덧붙이고, 없으면(이미 영속 or mac/win)\r\n    // 간결한 등록 확인만. 어느 경우든 'q' 종료 시 백그라운드(서비스)로 핸드오프된다.\r\n    setAutostartNotice(\r\n      res.persistCommand\r\n        ? `✅ registered — to persist after logout run once: ${res.persistCommand}  (background on 'q')`\r\n        : \"✅ auto-start registered — switches to background on 'q'\"\r\n    )\r\n  }\r\n\r\n  /**\r\n   * npm 자동 업데이트 — `U` 키 핸들러.\r\n   *\r\n   * npx 캐시 경로면 전역 설치 안내 후 중단(서비스 ExecStart 와 동일 가드).\r\n   * 업데이트 중에는 재진입을 막는다(`isUpdating` 플래그).\r\n   * 성공 시 runSelfUpdate 가 재시작 직전 `core.cleanup()`(포트/relay/PTY 정리)을 호출한 뒤\r\n   * process.exit(0) 하므로 이 함수는 반환되지 않는다. 실패 시 errorMessage 를 빨간 글씨로 노출한다.\r\n   */\r\n  const triggerUpdate = (): void => {\r\n    if (isUpdating) return\r\n    const exec = resolveExec()\r\n    if (exec.isNpxCache) {\r\n      setUpdateError('Auto-update requires global install — run: npm i -g @junyoung-kim/reins@latest')\r\n      return\r\n    }\r\n    setIsUpdating(true)\r\n    setUpdateError(null)\r\n    void runSelfUpdate(() => core.cleanup()).then(result => {\r\n      setIsUpdating(false)\r\n      if (!result.ok) {\r\n        setUpdateError(result.errorMessage ?? 'Auto-update failed')\r\n      }\r\n    })\r\n  }\r\n\r\n  useInput(\r\n    (input, key) => {\r\n      if (input === 'q') {\r\n        // 자동 시작을 켠 세션이면 핸드오프: 포그라운드 코어를 먼저 정리(포트/relay 해제)한 뒤\r\n        // 서비스를 start 한다 — 빈 포트에 서비스가 깨끗이 바인딩되도록(중복 인스턴스 방지).\r\n        if (handoffOnQuit) {\r\n          // 잔여 레이스(수용, /review 결정): cleanup 의 wss.close() 는 포트 해제를 *시작*만 하므로,\r\n          // 바로 아래 startService() 가 아직 안 풀린 로컬 WS 포트를 바인딩 시도할 수 있다. 단 이는\r\n          // **LAN-direct 로컬 서버**에만 영향 — 목표 시나리오(클라우드)에서 모바일 재연결은 relay\r\n          // 경로라 레이스와 무관하다(서비스의 relay 접속은 포트 바인딩과 별개로 성공). LAN 직결까지\r\n          // 보장하려면 코어의 bind 재시도가 정석이며 별도 작업으로 둔다.\r\n          core.cleanup()\r\n          // start 실패 시 포그라운드는 이미 정리돼 아무것도 안 떠 있는 상태가 된다 → stderr 로\r\n          // 알려 사용자가 수동 복구하게 한다(Ink stdout 버퍼와 분리돼 종료 후에도 보임).\r\n          if (!getServiceBackend().start()) {\r\n            process.stderr.write(\r\n              'reins: service start failed — run `reins service install` manually\\n'\r\n            )\r\n          }\r\n        }\r\n        onExit()\r\n      } else if (input === 'r') core.ws.reconnect()\r\n      else if (input === 'n') core.ws.requestQr()\r\n      else if (input === 'm') onNavigate('machines')\r\n      else if (input === 'o') onNavigate('options')\r\n      else if (input === 'i') onNavigate('info')\r\n      else if (input === 'd') onNavigate('getApps')\r\n      else if (input === 'a') toggleAutostart()\r\n      else if (input === 'U') triggerUpdate()\r\n      else if (input === 'c') setQrVisible(v => !v)\r\n      else if (input === 'v') {\r\n        setVerbose(v => !v)\r\n        setScroll(0)\r\n      } else if (verbose && key.upArrow) {\r\n        const { maxScroll } = scrollWindow(state.logs.length, scroll, VERBOSE_ROWS)\r\n        setScroll(s => Math.min(s + 1, maxScroll))\r\n      } else if (verbose && key.downArrow) {\r\n        setScroll(s => Math.max(s - 1, 0))\r\n      } else if (key.upArrow) {\r\n        // 기본 모드: Sessions 리스트 위로(offset 감소).\r\n        setSessionScroll(s => Math.max(s - 1, 0))\r\n      } else if (key.downArrow) {\r\n        // 기본 모드: Sessions 리스트 아래로(offset 증가, maxOffset 클램프).\r\n        const { maxOffset } = sessionWindow(state.machines.length, sessionScroll, rows)\r\n        setSessionScroll(s => Math.min(s + 1, maxOffset))\r\n      }\r\n    },\r\n    { isActive: active }\r\n  )\r\n\r\n  const sessions = state.machines.map(m => ({\r\n    id: m.id,\r\n    name: m.name,\r\n    pty: state.sessions[m.id] ?? 'idle',\r\n  }))\r\n\r\n  // 세션 윈도는 여기서 한 번만 계산해 DefaultBody 렌더와 힌트 양쪽에 쓴다.\r\n  // 가려진 세션이 있고 기본 모드면 하단 힌트에 ↑↓ scroll 을 노출(없으면 숨김).\r\n  const sessWin = sessionWindow(sessions.length, sessionScroll, rows)\r\n  const sessionsScrollable = !verbose && (sessWin.hiddenAbove > 0 || sessWin.hiddenBelow > 0)\r\n\r\n  const qrText = useMemo(() => (state.qr ? renderQr(state.qr.url) : null), [state.qr?.url])\r\n\r\n  return (\r\n    <Box flexDirection=\"column\">\r\n      {verbose ? (\r\n        <VerboseLog logs={state.logs} scroll={scroll} />\r\n      ) : (\r\n        <DefaultBody\r\n          sessions={sessions}\r\n          sessWin={sessWin}\r\n          clients={clients}\r\n          qrText={qrText}\r\n          qrUrl={state.qr ? prettyUrl(state.qr.url) : null}\r\n          qrMode={state.qr?.mode ?? null}\r\n          qrVisible={qrVisible}\r\n          columns={columns}\r\n          rows={rows}\r\n        />\r\n      )}\r\n      <Divider />\r\n      {state.latestVersion && !isUpdating ? (\r\n        <Text color=\"yellow\">\r\n          v{state.latestVersion} available — press U to update (current: v{identity.version})\r\n        </Text>\r\n      ) : null}\r\n      {isUpdating ? <Text color=\"yellow\">Updating…</Text> : null}\r\n      {updateError ? <Text color=\"red\">{updateError}</Text> : null}\r\n      {autostartNotice ? <Text color=\"yellow\">{autostartNotice}</Text> : null}\r\n      <Text color=\"cyan\">\r\n        q quit · r reconnect · n new QR · m machines · o options · i info · d get apps · a\r\n        auto-start · U update · c {qrVisible ? 'hide' : 'show'} qr · v{' '}\r\n        {verbose ? 'hide log' : 'verbose'}\r\n        {verbose || sessionsScrollable ? ' · ↑↓ scroll' : ''}\r\n      </Text>\r\n    </Box>\r\n  )\r\n}\r\n","import { Text } from 'ink'\r\nimport React from 'react'\r\n\r\n/**\r\n * 윈도잉된 로그 줄 목록 렌더 — verbose 로그(`MainScreen`)와 서비스 로그 tail(`ManagerApp`) 공용.\r\n *\r\n * 각 줄을 `wrap=\"truncate\"` 로 **항상 한 줄**로 유지하는 것이 핵심: 스크롤 윈도가 줄 수(VERBOSE_ROWS/\r\n * LOG_ROWS) 기준으로 높이를 계산하므로, 한 줄이 길어 wrap 되면 윈도 높이가 어긋나 프레임이 깨진다.\r\n * 슬라이싱/스크롤 계산은 호출부가 담당하고 여기선 표시(빈 목록은 '…')만 한다.\r\n */\r\nexport function LogLines({ lines }: { lines: string[] }) {\r\n  if (lines.length === 0) return <Text color=\"gray\">…</Text>\r\n  return (\r\n    <>\r\n      {lines.map((line, i) => (\r\n        <Text key={`${i}-${line}`} color=\"gray\" wrap=\"truncate\">\r\n          {line}\r\n        </Text>\r\n      ))}\r\n    </>\r\n  )\r\n}\r\n","/** {@link scrollWindow} 결과 — 슬라이스 경계 + 가려진 줄 수 + 스크롤 상한. */\nexport interface ScrollWindow {\n  /** 표시 시작 인덱스(slice start). */\n  start: number\n  /** 표시 끝 인덱스(slice end, exclusive). */\n  end: number\n  /** 윈도 위로 가려진 줄 수(\"↑ N more\"). */\n  hiddenAbove: number\n  /** 윈도 아래로 가려진 줄 수(\"↓ N more\"). */\n  hiddenBelow: number\n  /** 허용되는 최대 scroll(바닥에서 더 못 올라가는 한계). */\n  maxScroll: number\n}\n\n/**\n * verbose 로그 스크롤 윈도를 계산하는 순수 함수. `scroll` 은 **바닥(최신)에서 위로 떨어진 줄 수**다.\n *\n * VerboseLog 렌더와 키 입력 클램프가 같은 수식을 두 번 쓰지 않도록 한 곳으로 모은다(드리프트 방지).\n * `scroll` 은 내부에서 `[0, maxScroll]` 로 클램프하므로 호출자가 stale 값을 넘겨도 슬라이스가 범위를\n * 벗어나지 않는다.\n *\n * @param total 전체 로그 줄 수.\n * @param scroll 바닥에서 위로 떨어진 줄 수(클램프 전 raw 값 허용).\n * @param rows 한 화면에 보일 줄 수.\n */\nexport function scrollWindow(total: number, scroll: number, rows: number): ScrollWindow {\n  const maxScroll = Math.max(0, total - rows)\n  const clamped = Math.min(Math.max(0, scroll), maxScroll)\n  const end = total - clamped\n  const start = Math.max(0, end - rows)\n  return { start, end, hiddenAbove: start, hiddenBelow: total - end, maxScroll }\n}\n\n/**\n * Sessions 윈도 budget 산출 상수 — 터미널 높이에서 빼는 고정 chrome 줄 수와 행 수 클램프 범위.\n *\n * `termRows` 는 **전체 터미널 높이**이고 Sessions 리스트는 App 보더+헤더 안쪽에 중첩되므로,\n * `SESSION_RESERVE` 는 세션 행을 제외한 **모든** 고정 줄을 센다(QR 숨김·힌트 1줄 기준):\n * App 보더 2 + 헤더 3(reins·Relay·target) + DefaultBody(구분선 2·Scan/QR/URL 3·\"Sessions\" 1·Clients 1)\n * + ↑/↓ 인디케이터 2 + 하단 구분선 1 + 키 힌트 1 = 16. 전체 프레임(chrome + 세션 행)이 터미널 높이를\n * 넘지 않게 보수적으로 잡아, 작은 SSH 창에서도 헤더·키 힌트가 잘리지 않게 한다. (긴 키 힌트 줄바꿈·\n * autostart 알림이 뜨는 순간은 바닥 보더 한 줄을 잔여 마진으로 흡수 — 원래의 무제한 오버플로우보다 안전.)\n */\nconst SESSION_RESERVE = 16\nconst SESSION_MIN_ROWS = 3\nconst SESSION_MAX_ROWS = 12\n\n/** {@link sessionWindow} 결과 — 상단 기준 슬라이스 경계 + 가려진 줄 수 + offset 상한 + 적용 행 수. */\nexport interface SessionWindow {\n  /** 표시 시작 인덱스(slice start). */\n  start: number\n  /** 표시 끝 인덱스(slice end, exclusive). */\n  end: number\n  /** 윈도 위로 가려진 세션 수(\"↑ N more\"). */\n  hiddenAbove: number\n  /** 윈도 아래로 가려진 세션 수(\"↓ N more\"). */\n  hiddenBelow: number\n  /** offset 의 최대값(이보다 더 못 내려감). */\n  maxOffset: number\n  /** 터미널 높이 기반으로 실제 적용된 윈도 행 수. */\n  rows: number\n}\n\n/**\n * 메인(모니터) 화면 Sessions 리스트의 **상단 기준** 스크롤 윈도를 계산하는 순수 함수.\n *\n * {@link scrollWindow}(verbose 로그)는 바닥(최신) 기준이지만, Sessions 는 선택 커서가 없는 패시브\n * 표시라 **위에서부터** 보여주고 `offset` 만큼 아래로 내려가는 게 자연스럽다 — 그래서 별도 함수로 둔다.\n *\n * 윈도 행 수는 터미널 높이에서 고정 chrome({@link SESSION_RESERVE})을 뺀 값을 `[MIN, MAX]` 로 클램프한다.\n * 작은 창에서도 하단 키 힌트가 잘리지 않게 하기 위함(요청의 근본 원인). `offset` 은 `[0, maxOffset]` 으로\n * 클램프하므로 호출자가 stale 값을 넘겨도 슬라이스가 범위를 벗어나지 않는다.\n *\n * @param total    전체 세션 수.\n * @param offset   위에서부터 몇 줄 내려갔는지(클램프 전 raw 값 허용).\n * @param termRows 현재 터미널 행 수(useTerminalSize).\n */\nexport function sessionWindow(total: number, offset: number, termRows: number): SessionWindow {\n  const rows = Math.min(SESSION_MAX_ROWS, Math.max(SESSION_MIN_ROWS, termRows - SESSION_RESERVE))\n  const maxOffset = Math.max(0, total - rows)\n  const start = Math.min(Math.max(0, offset), maxOffset)\n  const end = Math.min(total, start + rows)\n  return { start, end, hiddenAbove: start, hiddenBelow: total - end, maxOffset, rows }\n}\n","import { Box, Text } from 'ink'\r\nimport React, { useState } from 'react'\r\n\r\nimport type { AgentCore } from '@arva/core'\r\n\r\nimport type { HeadlessConfigStore } from '../../config/json-store'\r\nimport { isValidPort, isValidRelayUrl, isValidTimeoutMs } from '../../config/validate'\r\nimport { Divider } from '../components/Divider'\r\nimport { Field } from '../components/Field'\r\nimport { useFieldFocus } from '../useFieldFocus'\r\n\r\n/** 옵션 폼 필드 순서(마지막 필드 Enter = 저장). */\r\nconst ORDER = ['relayUrls', 'wsPort', 'heartbeat', 'pong', 'auth'] as const\r\n\r\n/** config 값을 폼 문자열 상태로 로드. relay URL 은 쉼표 구분(첫 항목=primary). */\r\nfunction loadForm(config: HeadlessConfigStore) {\r\n  return {\r\n    relayUrls: config.getRelayUrls().join(', '),\r\n    wsPort: String(config.getWsPort()),\r\n    heartbeat: String(config.getHeartbeatIntervalMs()),\r\n    pong: String(config.getPongTimeoutMs()),\r\n    auth: String(config.getAuthTimeoutMs()),\r\n  }\r\n}\r\n\r\n/**\r\n * 옵션 편집 화면 — relay URL(쉼표 구분, 첫=primary)·WS 포트·타임아웃(heartbeat/pong/auth) 편집 +\r\n * install_id / FCM 토큰 수 / relay 타깃 읽기전용 표시.\r\n *\r\n * **적용 시점**: relay URL 은 저장 후 변경됐으면 `core.ws.reconnect()` 로 즉시 반영(코어가 연결마다\r\n * getRelayUrls 재read). WS 포트·타임아웃은 코어가 부팅 시 1회 read 하므로 **재시작 후 반영**(저장은 즉시) —\r\n * 화면에 안내. relay URL 만 쉼표 편집으로 단순화(per-item add/remove 대신 — TUI 단순성, 순서=우선순위 유지).\r\n *\r\n * **키 소유권**: 폼 레벨 `useInput`(active) 은 Tab(다음)·Esc(뒤로)만. 타이핑/Enter 는 포커스된 필드가 소비.\r\n */\r\nexport function OptionsScreen({\r\n  config,\r\n  core,\r\n  active,\r\n  onBack,\r\n}: {\r\n  config: HeadlessConfigStore\r\n  core: AgentCore\r\n  active: boolean\r\n  onBack: () => void\r\n}) {\r\n  const [form, setForm] = useState(() => loadForm(config))\r\n  const [errors, setErrors] = useState<string[]>([])\r\n  const [saved, setSaved] = useState(false)\r\n\r\n  const set = (patch: Partial<typeof form>) => {\r\n    setForm(f => ({ ...f, ...patch }))\r\n    setSaved(false)\r\n  }\r\n\r\n  const save = () => {\r\n    const urls = form.relayUrls\r\n      .split(',')\r\n      .map(s => s.trim())\r\n      .filter(Boolean)\r\n    const port = Number.parseInt(form.wsPort, 10)\r\n    const heartbeat = Number.parseInt(form.heartbeat, 10)\r\n    const pong = Number.parseInt(form.pong, 10)\r\n    const auth = Number.parseInt(form.auth, 10)\r\n\r\n    const errs: string[] = []\r\n    if (urls.length === 0 || !urls.every(isValidRelayUrl)) errs.push('relayUrls')\r\n    if (!isValidPort(port)) errs.push('wsPort')\r\n    if (!isValidTimeoutMs(heartbeat)) errs.push('heartbeat')\r\n    if (!isValidTimeoutMs(pong)) errs.push('pong')\r\n    if (!isValidTimeoutMs(auth)) errs.push('auth')\r\n    if (errs.length > 0) {\r\n      setErrors(errs)\r\n      return\r\n    }\r\n\r\n    const relayChanged = JSON.stringify(urls) !== JSON.stringify(config.getRelayUrls())\r\n    // 코어 호출(reconnect)이 useInput 핸들러 안 — throw 시 TUI 가 죽지 않게 가드.\r\n    try {\r\n      // 한 번의 writeStore 로 일괄 저장(필드별 setter 5회 = 같은 파일 5회 쓰기 방지). relay_urls 는\r\n      // 변경됐을 때만 포함 — 안 그러면 env/기본값으로 동적 해석되던 값을 store 에 고착시킨다.\r\n      config.setOptions({\r\n        ...(relayChanged ? { relay_urls: urls } : {}),\r\n        ws_port: port,\r\n        heartbeat_interval_ms: heartbeat,\r\n        pong_timeout_ms: pong,\r\n        auth_timeout_ms: auth,\r\n      })\r\n      // relay URL 만 라이브 반영(reconnect). 포트/타임아웃은 재시작 필요 → reconnect 호출하지 않음.\r\n      if (relayChanged) core.ws.reconnect()\r\n    } catch {\r\n      setErrors(['save'])\r\n      return\r\n    }\r\n    setErrors([])\r\n    setSaved(true)\r\n  }\r\n\r\n  const { focusedKey, advance } = useFieldFocus(ORDER, {\r\n    isActive: active,\r\n    onSubmit: save,\r\n    onEscape: onBack,\r\n  })\r\n\r\n  const invalid = (k: string) => errors.includes(k)\r\n\r\n  return (\r\n    <Box flexDirection=\"column\">\r\n      <Divider />\r\n      <Text>Options</Text>\r\n\r\n      <Field\r\n        label=\"Relay\"\r\n        value={form.relayUrls}\r\n        onChange={v => set({ relayUrls: v })}\r\n        focus={focusedKey === 'relayUrls'}\r\n        onSubmit={() => advance('relayUrls')}\r\n        placeholder=\"wss://… , wss://…(fallback)\"\r\n        invalid={invalid('relayUrls')}\r\n      />\r\n      <Field\r\n        label=\"WS Port\"\r\n        value={form.wsPort}\r\n        onChange={v => set({ wsPort: v.replace(/[^0-9]/g, '') })}\r\n        focus={focusedKey === 'wsPort'}\r\n        onSubmit={() => advance('wsPort')}\r\n        invalid={invalid('wsPort')}\r\n      />\r\n      <Field\r\n        label=\"Heartbeat\"\r\n        value={form.heartbeat}\r\n        onChange={v => set({ heartbeat: v.replace(/[^0-9]/g, '') })}\r\n        focus={focusedKey === 'heartbeat'}\r\n        onSubmit={() => advance('heartbeat')}\r\n        invalid={invalid('heartbeat')}\r\n      />\r\n      <Field\r\n        label=\"Pong\"\r\n        value={form.pong}\r\n        onChange={v => set({ pong: v.replace(/[^0-9]/g, '') })}\r\n        focus={focusedKey === 'pong'}\r\n        onSubmit={() => advance('pong')}\r\n        invalid={invalid('pong')}\r\n      />\r\n      <Field\r\n        label=\"Auth\"\r\n        value={form.auth}\r\n        onChange={v => set({ auth: v.replace(/[^0-9]/g, '') })}\r\n        focus={focusedKey === 'auth'}\r\n        onSubmit={() => advance('auth')}\r\n        invalid={invalid('auth')}\r\n      />\r\n      <Text color=\"gray\" wrap=\"truncate\">\r\n        ⚠ WS Port·timeouts apply after restart (relay reconnects immediately).\r\n      </Text>\r\n\r\n      <Divider />\r\n      <Text color=\"gray\">install_id {config.getInstallId().slice(0, 8)}…</Text>\r\n      <Text color=\"gray\">FCM tokens {config.getFcmTokens().length}</Text>\r\n\r\n      <Divider />\r\n      {errors.length > 0 ? (\r\n        <Text color=\"red\">\r\n          {errors.includes('save') ? 'Save failed — please retry' : `Format error: ${errors.join(', ')}`}\r\n        </Text>\r\n      ) : null}\r\n      {saved ? <Text color=\"green\">Saved (port/timeouts apply after restart)</Text> : null}\r\n      <Text color=\"cyan\">Tab move · Enter next · Ctrl+S save · Esc back</Text>\r\n    </Box>\r\n  )\r\n}\r\n","import { Box, Text, useApp, useInput } from 'ink'\r\nimport React, { useEffect, useMemo, useState } from 'react'\r\n\r\nimport type { HeadlessConfigStore } from '../config/json-store'\r\nimport { identity } from '../identity'\r\nimport { computePairUrl } from '../pairing'\r\nimport { getServiceBackend, type ServiceStatus } from '../service'\r\nimport { Divider } from './components/Divider'\r\nimport { LogLines } from './components/LogLines'\r\nimport { PairQrCell } from './components/PairQrCell'\r\nimport { tailLog } from './log-tail'\r\nimport { prettyUrl } from './pretty-url'\r\nimport { renderQr } from './qr'\r\nimport { useTerminalSize } from './useTerminalSize'\r\n\r\n/** 관리 화면 로그 tail 줄 수. */\r\nconst LOG_ROWS = 8\r\n\r\n/**\r\n * QR 외 chrome 높이 추정 — 헤더/서비스/persist/divider×3/Scan/URL/Logs 라벨/footer(≈10) + 로그 tail.\r\n * MainScreen 보다 로그 tail({@link LOG_ROWS}) 만큼 크므로 기본 RESERVED_ROWS(10) 대신 이 값을 쓴다.\r\n */\r\nconst QR_RESERVE_ROWS = 10 + LOG_ROWS\r\n\r\n/**\r\n * 서비스 가동 중 SSH 재접속 시의 **관리/상태 화면**(코어 미기동).\r\n *\r\n * `cli.tsx` 가 단일 인스턴스 가드(`isServiceActive`)로 이 화면을 띄운다 — 두 번째 코어를 만들지 않아\r\n * 같은 install_id 이중 relay 접속(footgun)을 막는다. 라이브 세션/clients 는 서비스 프로세스 메모리에\r\n * 있어 보여주지 못하지만, **페어링 QR 은 영속 토큰+config 로 재계산**(서비스가 서빙 중인 것과 동일)하고,\r\n * 상태(systemctl)·`ws.log` tail 로 사실상의 동작을 확인할 수 있다. 액션은 재시작/중지/자동시작 해제.\r\n *\r\n * 상태/로그는 신뢰 소스(systemctl·파일)를 2s 폴링한다.\r\n */\r\nexport function ManagerApp({ config }: { config: HeadlessConfigStore }) {\r\n  const { exit } = useApp()\r\n  const { columns, rows } = useTerminalSize()\r\n  const [status, setStatus] = useState<ServiceStatus>(() => getServiceBackend().status())\r\n  const [logs, setLogs] = useState<string[]>(() => tailLog('ws', LOG_ROWS))\r\n  const [qrVisible, setQrVisible] = useState(false)\r\n  const [notice, setNotice] = useState<string | null>(null)\r\n\r\n  // 폴링 비용: status() 가 tick 당 백엔드 조회(systemd=is-active/is-enabled+loginctl)를 spawn 한다\r\n  // (systemctl 가용성은 hasSystemctlUser 캐시로 1회만). 관리 화면은 상태 변화가 드물어 3s 로 둔다.\r\n  useEffect(() => {\r\n    const t = setInterval(() => {\r\n      setStatus(getServiceBackend().status())\r\n      setLogs(tailLog('ws', LOG_ROWS))\r\n    }, 3000)\r\n    return () => clearInterval(t)\r\n  }, [])\r\n\r\n  // relay primary 가 바뀌지 않는 한 안정 — config 는 stable 참조.\r\n  const pairUrl = useMemo(() => computePairUrl(config.getRelayUrls()), [config])\r\n  const qrText = useMemo(() => (pairUrl ? renderQr(pairUrl) : null), [pairUrl])\r\n  // 다중 relay면 computePairUrl 은 primary 가정 — failover 중이면 어긋날 수 있음을 노출(pairing.ts 한계).\r\n  const multiRelay = useMemo(() => config.getRelayUrls().length > 1, [config])\r\n\r\n  useInput((input) => {\r\n    const backend = getServiceBackend()\r\n    if (input === 'q') exit()\r\n    else if (input === 'c') setQrVisible(v => !v)\r\n    else if (input === 'r') {\r\n      setNotice(backend.restart() ? 'restart requested' : 'restart failed')\r\n      setStatus(backend.status())\r\n    } else if (input === 'x') {\r\n      setNotice(backend.stop() ? 'stopped — press q to close this window' : 'stop failed')\r\n      setStatus(backend.status())\r\n    } else if (input === 'u') {\r\n      backend.uninstall()\r\n      setNotice('auto-start disabled — press q to close')\r\n      setStatus(backend.status())\r\n    }\r\n  })\r\n\r\n  return (\r\n    <Box flexDirection=\"column\" borderStyle=\"round\" paddingX={1}>\r\n      <Box justifyContent=\"space-between\">\r\n        <Text bold>reins — running as service</Text>\r\n        <Text color=\"gray\">v{identity.version}</Text>\r\n      </Box>\r\n\r\n      <Box>\r\n        <Text color=\"gray\">Service </Text>\r\n        <Text color={status.active ? 'green' : 'red'}>\r\n          {status.active ? '● active' : '○ inactive'}\r\n        </Text>\r\n        <Text color=\"gray\">{status.enabled ? ' (enabled)' : ' (disabled)'}</Text>\r\n      </Box>\r\n      <Box>\r\n        <Text color=\"gray\">Persist </Text>\r\n        {status.persists ? (\r\n          <Text color=\"green\">on → keeps running in the background</Text>\r\n        ) : status.persistCommand ? (\r\n          <Text color=\"yellow\">off → run: {status.persistCommand}</Text>\r\n        ) : (\r\n          <Text color=\"yellow\">off → session-scoped (re-register to persist)</Text>\r\n        )}\r\n      </Box>\r\n\r\n      <Divider />\r\n\r\n      {pairUrl ? (\r\n        <Box flexDirection=\"column\">\r\n          <Text color=\"gray\">Scan to pair:</Text>\r\n          <PairQrCell\r\n            qrText={qrText}\r\n            visible={qrVisible}\r\n            columns={columns}\r\n            rows={rows}\r\n            reserveRows={QR_RESERVE_ROWS}\r\n          />\r\n          <Box>\r\n            <Text color=\"gray\">URL </Text>\r\n            <Text color=\"cyan\">{prettyUrl(pairUrl)}</Text>\r\n          </Box>\r\n          {multiRelay ? (\r\n            <Text color=\"yellow\">(assumes primary relay — may differ if failed over)</Text>\r\n          ) : null}\r\n        </Box>\r\n      ) : (\r\n        <Text color=\"yellow\">Pair URL unavailable (check session-token / relay URL)</Text>\r\n      )}\r\n\r\n      <Divider />\r\n      <Text color=\"gray\">Logs (ws.log tail)</Text>\r\n      <LogLines lines={logs} />\r\n\r\n      <Divider />\r\n      {notice ? <Text color=\"green\">{notice}</Text> : null}\r\n      <Text color=\"cyan\">\r\n        r restart · x stop · u disable · c {qrVisible ? 'hide' : 'show'} qr · q quit\r\n      </Text>\r\n    </Box>\r\n  )\r\n}\r\n","import fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\n\n/** 헤드리스 scoped 로그 디렉터리(코어 logger 기본값과 동일 규약). */\nconst LOG_DIR = path.join(os.homedir(), '.ai_remote_vibe_agent', 'logs')\n\n/**\n * 컴포넌트 로그 파일(`{component}.log`)의 마지막 N줄을 읽는다(없으면 빈 배열).\n *\n * 관리 화면이 라이브 코어 없이 \"지금 무슨 일이 있는지\"를 보여주는 용도. 파일은 5MB rotation 이라\n * 전체를 읽지 않고 **끝 16KB** 만 읽어(중간 줄이 잘릴 수 있으니 첫 줄은 버려도 무방) 폴링 비용을 막는다.\n */\nexport function tailLog(component: string, lines: number): string[] {\n  const file = path.join(LOG_DIR, `${component}.log`)\n  try {\n    const size = fs.statSync(file).size\n    const readBytes = Math.min(size, 16 * 1024)\n    const fd = fs.openSync(file, 'r')\n    try {\n      const buf = Buffer.alloc(readBytes)\n      fs.readSync(fd, buf, 0, readBytes, size - readBytes)\n      return buf\n        .toString('utf-8')\n        .split(/\\r?\\n/)\n        .filter(Boolean)\n        .slice(-lines)\n    } finally {\n      fs.closeSync(fd)\n    }\n  } catch {\n    return []\n  }\n}\n"],"mappings":"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","names":["os","path","fs","os","path","crypto","path","$r","$g","$b","$a","channels","toCss","r","g","b","a","toPaddedHex","toRgba","toColor","color","blend","bg","fg","fgR","fgG","fgB","bgR","bgG","bgB","css","rgba","isOpaque","ensureContrastRatio","ratio","result","opaque","rgbaColor","opacity","multiplyOpacity","factor","toColorRGB","$ctx","$litmusColor","canvas","ctx","rgbaMatch","rgb","relativeLuminance","relativeLuminance2","rs","gs","bs","rr","rg","rb","bgRgba","fgRgba","bgL","fgL","contrastRatio","resultA","reduceLuminance","resultARatio","resultB","increaseLuminance","resultBRatio","cr","toChannels","value","c","s","l1","l2","DEFAULT_ANSI_COLORS","colors","v","i","constrain","low","high","escapeHTMLChar","BaseSerializeHandler","_buffer","range","excludeFinalCursorPosition","cell1","cell2","oldCell","startRow","endRow","startColumn","endColumn","row","line","startLineColumn","endLineColumn","col","cell","isLastRow","rows","equalFg","equalBg","equalFlags","StringSerializeHandler","buffer","_terminal","start","end","rowSeparator","currentLine","nextLine","thisRowLastChar","thisRowLastSecondChar","nextRowFirstChar","isNextRowFirstCharDoubleWidth","isValid","sgrSeq","fgChanged","bgChanged","flagsChanged","isEmptyCell","rowEnd","content","realCursorRow","realCursorCol","cursorMoved","moveRight","offset","curAttrData","SerializeAddon","terminal","scrollback","maxRows","correctRows","options","handler","HTMLSerializeHandler","onlySelection","selection","modes","alternativeScreenContent","_options","target","targetLength","padString","foreground","background","globalStyleDefinitions","isFg","x","fgHexColor","bgHexColor","styleDefinitions","log","log","mode","m","log","m","os","path","log","path","b","path","os","p","os","p","fs","path","crypto","v","m","sessionMgr","cached","cwd","b","os","path","fs","args","k","args","log","args","p","fs","path","p","config","log","DATA_DIR","path","os","fs","config","identity","delay","log","log","os","path","fs","path","path","os","fs","os","path","fs","os","path","fs","os","path","SESSION_TOKEN_FILE","path","os","fs","fs","os","path","fs","spawnSync","args","os","path","path","os","p","args","fs","exec","fs","path","os","path","path","os","p","exec","fs","path","fs","os","path","p","exec","path","os","args","fs","exec","config","exec","m","path","os","fs","m","spawn","process","v","b","args","Box","Text","useEffect","useState","spawn","Text","useEffect","useState","m","Text","w","jsx","Text","jsx","jsxs","args","spawn","useState","useEffect","cached","v","Text","os","os","path","Box","Text","useInput","useEffect","useState","jsx","jsxs","os","Box","Text","config","useState","useEffect","useInput","path","Box","Text","useInput","useState","Box","Text","jsx","jsxs","path","Box","Text","useEffect","useMemo","useState","Box","Text","jsx","jsxs","Box","Text","SelectInput","jsx","jsxs","useInput","useState","os","m","Fragment","jsx","jsxs","useState","useMemo","useEffect","k","path","v","Box","Text","jsx","jsxs","m","Box","Text","useState","mode","useInput","Box","Text","useInput","useEffect","useMemo","useState","Text","Fragment","jsx","jsx","jsxs","Box","Text","useState","useEffect","exec","useInput","v","m","useMemo","Box","Text","useState","jsx","jsxs","config","useState","k","Box","Text","v","jsx","jsxs","useState","useEffect","config","Box","Text","Box","Text","useApp","useInput","useEffect","useMemo","useState","fs","os","path","jsx","jsxs","config","useApp","useState","useEffect","useMemo","useInput","v","Box","Text","jsx","path","os","log"]}