## Why

When a process running in a booterm terminal pane exits, the browser currently receives a bare `{type: 'exit', code: N}` frame and the socket closes (`apps/booterm/src/ws/attach.ts:170-183`). There is no structured metadata: no last output lines, no session title, no parent agent attribution. The inference loop in apps/server and apps/coder cannot react when a long-running task completes because the notification carries no context beyond the exit code.

The reference implementation (`/opt/forks/opencode-extras/opencode-pty`) solves this with `<pty_exited>` structured notifications carrying exit code, last output lines, session metadata, and timeout status. Booterm already tracks all of this data (registry `SessionMeta` with `sessionId`, `paneId`, `title`, `description`, `parentAgent`; ring buffer with output lines via `appendOutput`). The data is present but never surfaced on exit.

## What Changes

- Enhance the booterm WS exit notification from a bare `{type: 'exit', code}` to a structured `pty_exited` frame carrying: exit code, last N output lines from the ring buffer, session metadata (title, description, parentAgent), and timeout status.
- Add `pty_exited` as a new frame type in the cross-app WsFrame contract (`packages/contracts`).
- Update the web frontend to parse and handle the new frame type.

## Scope

- **In scope**: structured exit notification over booterm WS; new WsFrame type in contracts; web frontend handling.
- **Out of scope**: log-search extras (already implemented in booterm registry ring buffer + search route), per-session timeouts (already implemented in registry + sweepExpired), pattern-based PTY log search (already in `searchRingBuffer`). These exist; this change only adds the exit notification. Broker publish for inference-loop consumption is deferred (see Deferred section).

## Non-goals

- Changing the booterm WS binary/text frame protocol for ongoing data.
- Adding persistence for exit events (no DB table; frames are ephemeral like all broker frames).
- Modifying the coder's PTY dispatch flow (which uses `child_process.spawn`, not booterm PTYs).
