Private stdin/stdout IPC protocol for linkcode-pty. For build, development, and benchmark notes, see README.md.
The Rust implementation is in src/proto.rs; the TypeScript counterpart is in apps/daemon/src/pty/codec.ts. Stderr is reserved for diagnostics and must not carry protocol data.
Each frame has a 5-byte header followed by a body:
[u32 little-endian total][u8 type][body]
total is the byte length of the type byte plus the body:
total = 1 + body.length
Rules:
totalmust be non-zero.totalmust be at most16 MiB.- If frame decoding fails, the stream is considered corrupt. The receiver should stop using that sidecar instance instead of trying to resynchronize mid-stream.
- Multi-byte integers are little-endian.
Daemon to sidecar:
| Type | Name | Body |
|---|---|---|
0x01 |
OPEN |
JSON OpenParams |
0x02 |
INPUT |
binary DataFrame |
0x03 |
RESIZE |
JSON ResizeParams |
0x04 |
CLOSE |
JSON CloseParams |
0x05 |
CREDIT |
JSON CreditParams |
Sidecar to daemon:
| Type | Name | Body |
|---|---|---|
0x81 |
OPENED |
JSON Opened |
0x82 |
OUTPUT |
binary DataFrame |
0x83 |
EXIT |
JSON Exit |
0x84 |
ERROR |
JSON Error |
Unknown frame types are ignored by the current sidecar. A malformed OPEN frame fails only that terminal — the sidecar replies ERROR for its terminalId and keeps running every other terminal. If the frame is malformed badly enough that terminalId itself cannot be recovered, the sidecar cannot reply ERROR at all: it logs to stderr, and the daemon's pending open for that frame is reclaimed by its own open timeout (10s) rather than hanging forever. This should not happen in practice, since the daemon always encodes terminalId itself.
INPUT and OUTPUT carry raw bytes so terminal traffic never pays JSON/base64 overhead:
[u16 little-endian terminal_id_length][terminal_id UTF-8 bytes][raw bytes]
Rules:
terminal_id_lengthmust be at least1and at most65535.terminal_idis generated by the daemon and must be unique among pending and live terminals.raw bytesare not interpreted by the sidecar.- The daemon decodes
OUTPUTbytes into UTF-8 text with a streamingTextDecoderper terminal.
JSON object keys are camelCase. Unknown JSON fields may be ignored by receivers. The examples below are illustrative; exact shell paths are platform-specific.
{
"terminalId": "term-mj7w5r-1",
"cols": 80,
"rows": 24,
"cmd": "/bin/zsh",
"args": [],
"cwd": "/Users/alice/project",
"env": {}
}Fields:
| Field | Type | Required | Notes |
|---|---|---|---|
terminalId |
string | yes | Non-empty UTF-8, max 65535 bytes, unique while pending/live. |
cols |
number | yes | Terminal columns, encoded as Rust u16. |
rows |
number | yes | Terminal rows, encoded as Rust u16. |
cmd |
string | yes | Executable already resolved by the daemon. |
args |
string[] | no | Defaults to []. |
cwd |
string or null | no | Working directory. If omitted/null, the sidecar does not set one. |
env |
object | no | Additional environment variables. Defaults to {}. |
credit |
number or null | no | Initial read-credit budget in bytes (Rust u64). Omitted/null means unthrottled. |
Behavior:
- On success, the sidecar sends
OPENEDbefore anyOUTPUTfor that terminal. - If the terminal id already exists, the sidecar sends
ERROR. - If spawning fails, the sidecar sends
ERROR. - If the
OPENJSON itself is malformed, the sidecar logs to stderr and repliesERRORfor thatterminalIdalone; every other terminal keeps running. IfterminalIdcannot be recovered from the malformed body, noERRORis sent — see Frame types.
{
"terminalId": "term-mj7w5r-1",
"cols": 120,
"rows": 32
}RESIZE is best effort. Missing terminals and platform resize failures do not currently produce an ERROR.
{
"terminalId": "term-mj7w5r-1"
}CLOSE requests termination. Cleanup is reported by the later EXIT frame from the reader thread when the PTY reaches EOF and the child has been reaped. CLOSE also lifts the terminal's read-credit gate so a parked reader can drain to that EOF; the dying process is unthrottled from this point on.
{
"terminalId": "term-mj7w5r-1",
"bytes": 65536
}Grants bytes of additional PTY read budget to one terminal (flow control). A terminal opened with a credit field reads at most its remaining budget from the PTY and parks once the budget hits zero — the kernel PTY buffer then fills and the shell's writes block, propagating backpressure into a flooding process. Terminals opened without credit ignore grants and stay unthrottled (compatibility with pre-credit daemons). CREDIT is best effort: an unknown or already-exited terminalId is ignored. Sidecar shutdown, like CLOSE, releases every gate so parked readers can reach EOF.
{
"terminalId": "term-mj7w5r-1",
"pid": 12345
}pid is 0 when the platform or PTY backend does not expose a process id.
{
"terminalId": "term-mj7w5r-1",
"exitCode": 0
}exitCode is null when waiting on the child failed or when the daemon synthesizes an exit because the sidecar died.
{
"terminalId": "term-mj7w5r-1",
"message": "terminal id already exists"
}ERROR is currently used for OPEN failures. Best-effort operations such as INPUT, RESIZE, and CLOSE do not currently report per-command errors.
- One sidecar process can multiplex many terminals.
- The daemon lazily starts the sidecar on first open.
- If the sidecar exits or its stream becomes corrupt, the daemon rejects pending opens and emits
exit(null)for live terminals. - After a sidecar restart, the daemon resets its frame decoder so partial bytes from the old process cannot corrupt the new stream.
- When daemon stdin closes, the sidecar kills all known terminals and exits.