claude-print/docs/notes/terminal-probes.md
jedarden 930aeafd0f docs(bf-1v8): fix README exit codes and sync flags table, add docs/notes stubs
- Fix exit-code table: change incorrect codes 3/4 to correct values 124 (timeout) and 130 (interrupted), matching src/error.rs implementation
- Add missing timeout flags to flags table: --first-output-timeout (90s), --stream-json-timeout (90s), --stop-hook-timeout (120s), matching src/cli.rs defaults
- Add docs/notes/hook-design.md covering relay hook mechanics, FIFO protocol, and keeper fd pattern (from src/hook.rs, src/poller.rs)
- Add docs/notes/terminal-probes.md covering Ink probe table and response bytes (from src/terminal.rs)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-02 13:57:00 -04:00

109 lines
4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Terminal Probes
Claude Code's TUI (built on Ink, a React/Yoga-based framework) sends DEC terminal queries at startup and hangs indefinitely if unanswered. The terminal emulator in `claude-print` scans PTY output for these probes and responds automatically.
## Probe Table
| Probe Bytes | Response Bytes | Name | Notes |
|-------------|---------------|------|-------|
| `ESC [ c` or `ESC [ 0 c` | `ESC [ ? 6 c` | DA1 (Device Attributes) | Primary terminal type query |
| `ESC [ > c` or `ESC [ > 0 c` | `ESC [ > 0 ; 0 ; 0 c` | DA2 (Device Attributes 2) | Secondary terminal type query |
| `ESC [ 6 n` | `ESC [ 1 ; 1 R` | DSR (Device Status Report) | Cursor position report |
| `ESC [ > q` or `ESC [ > 0 q` | `ESC P >\| claude-print ESC \` | XTVERSION (Terminal Identification) | DCS string with ST terminator |
| `ESC [ 1 8 t` | `ESC [ 8 ; <rows> ; <cols> t` | Window Size | Responds with configured dimensions |
### Response Details
- **DA1**: `ESC [ ? 6 c` — Indicates "VT102" compatibility level
- **DA2**: `ESC [ > 0 ; 0 ; 0 c` — Format: `> <version> ; <options> ; <rom-version>c`
- **DSR**: `ESC [ 1 ; 1 R` — Cursor at row 1, column 1
- **XTVERSION**: `ESC P >\| claude-print ESC \` — DCS string with identifier and ST (String Terminator = ESC + backslash)
- Note: The final two bytes are ESC (`0x1B`) + backslash (`0x5C`), not a backtick
- **Window Size**: `ESC [ 8 ; <rows> ; <cols> t` — Configured dimensions (default 220×50 from stty fallback)
## Implementation
The probe responder (`src/terminal.rs`) uses a byte-by-byte state machine to handle probes that may be split across chunk boundaries:
### State Machine
```
Empty → ESC received
Partial → accumulating CSI sequence
Complete → CSI sequence complete
Invalid → not a recognized probe
```
### CSI Format
A CSI (Control Sequence Introducer) sequence has the structure:
```
ESC [ <params> <final-byte>
```
- `<params>`: intermediate/parameter bytes in range `0x20-0x3F`
- `<final-byte>`: terminator in range `0x40-0x7E`
### Matching Logic
Probe identification:
```rust
// params = everything after ESC [ up to the final byte
if params == b"c" || params == b"0c" DA1
else if params == b">c" || params == b">0c" DA2
else if params == b"6n" DSR
else if params == b">q" || params == b">0q" XTVersion
else if params == b"18t" WinSize
else Unknown probe (silently ignored)
```
## Deduplication
Each probe type is answered at most once per session using a bitmask:
- Bit 0: DA1
- Bit 1: DA2
- Bit 2: DSR
- Bit 3: XTVersion
- Bit 4: WinSize
If the same probe is received again, no response is emitted.
## Unknown Sequences
Unknown escape sequences are **silently ignored** — they are never treated as an error. This ensures version-resilience: if Ink adds new probe types in future versions, `claude-print` will not hang; it simply won't respond to the unrecognized probes.
The startup sequencer has a fallback timeout (0.8 s idle after ≥ 200 bytes received) to cover cases where the terminal doesn't respond to all probes or emits unexpected output.
## Version Resilience
The probe responder is designed to survive Claude Code version changes:
1. **Unknown probes ignored**: New probe types won't crash the binary
2. **Split-chunk handling**: Probe bytes straddling chunk boundaries are correctly assembled
3. **Length cap**: Sequences exceeding `MAX_PROBE_LEN` (32 bytes) are discarded as invalid
4. **Lenient matching**: Both bare (`c`) and parameterized (`0c`) forms are recognized where applicable
## Window Size Fallback
Window size is probed in order:
1. `TIOCGWINSZ` on `STDOUT_FILENO`
2. `TIOCGWINSZ` on `STDIN_FILENO`
3. Open `/dev/tty` and `TIOCGWINSZ`
4. Fallback: `220 × 50`
In headless/NEEDLE mode, steps 13 fail and the fallback is always used.
## Integration with Event Loop
The terminal emulator runs on every chunk of PTY output in the event loop:
```
master_fd POLLIN → read chunk → feed to TerminalEmu → response bytes queued
next writable poll → write response bytes to master_fd
```
This ensures low-latency probe responses — Ink receives answers before its own timeouts.