Terminal WebSocket
A worktree's console in Jaah is a live terminal on the node that hosts the worktree. The browser reaches it over one WebSocket, which relays bytes both ways without reading them: what you type goes to the terminal, and what the terminal prints comes back. Opening a worktree's console in Jaah opens this connection; Worktrees describes the worktrees themselves.
URL
wss://<your Jaah host>/api/console/{project}/{number}
Use ws:// instead when the console itself is served over plain http://.
| Part | Meaning |
|---|---|
{project} | The project's name, exactly as Jaah shows it (case-sensitive). URL-encode it. |
{number} | The worktree's number within that project. |
Query parameters, all optional:
| Parameter | Meaning |
|---|---|
cols, rows | The terminal's starting size in characters. The node picks a size when they are absent. |
node | The id of the node that hosts the worktree. Send it when two nodes have a worktree with the same project and number. |
attach_only | 1 to attach to the session that is already running and never start a new one. Send it when you reconnect. |
provider | Which provider's agent account runs a new session: claude, codex, gemini, deepseek or antigravity. Leave it out to let Jaah pick. Any other value closes with 4404. |
The console sends a few more parameters for its own views; you do not need them.
Frames
Every frame is binary. The two directions are shaped differently.
From the terminal to you, frames are the terminal's raw output: bytes to feed to a terminal emulator as they arrive, in order. A frame can end in the middle of a character or an escape sequence, so do not decode each frame on its own.
From you to the terminal, each frame is a 3-byte header followed by its payload:
| Byte | Meaning |
|---|---|
| 0 | Frame type |
| 1–2 | Payload length, big-endian, at most 65,535 |
| 3… | Payload |
| Type | Payload |
|---|---|
0 input | Bytes to type into the terminal, as UTF-8. Split long input over several frames. |
1 resize | 4 bytes: columns (2 bytes, big-endian), then rows (2 bytes, big-endian). |
Types 2 and 3 carry images pasted into the console and are used by the console itself.
Keep the connection busy. A connection with no traffic for 60 seconds can be dropped on the way, and
a session with no traffic for 30 minutes is closed. While the terminal is open, send an empty input frame
(the three bytes 00 00 00) every 25 seconds, as the console does.
When you may only view a worktree, the console is one-way: the connection opens, you receive the terminal's output, and every frame you send — resize included — is dropped.
Starting a session
When the worktree has no running session, opening the console starts one, unless you send
attach_only=1 or may only view the worktree. Two hourly limits apply:
- 12 connections per person per worktree that may start a session, whether or not they start one. Past it, a connection still opens but only attaches.
- 12 new sessions per worktree, for everyone together.
Past either limit, a connection that finds no running session closes with 4502.
Close codes
Every connection ends with one of these codes. The Reconnect? column says whether trying again can help.
| Code | Meaning | Reconnect? |
|---|---|---|
1000 | The terminal session ended. | No — open the console again to start a new session. |
4401 | Connection refused. | No. |
4404 | No such worktree, one you cannot open, or a query parameter value it does not accept. | No. |
4409 | Your organization's limit on active worktrees is full. The close reason is a sentence to show the user. | No — free a worktree first. |
4502 | The node has no session to attach to, or cannot provide this console. | No, on a first connection. After a drop it means the session is gone: reconnect once without attach_only to start a new one. |
4503 | The connection to the node failed or broke; any session on the node is untouched. | Yes — reconnect with attach_only=1. |
Any other code — 1006 when the network drops, or 1012 while Jaah restarts — is a lost connection:
reconnect with attach_only=1. The close reason, when there is one, is a short readable explanation.
Example — open a terminal and handle its close code
Paste this into the browser console on a page of the Jaah console. Replace the project name and number with a worktree you can open; the output prints as text.
const project = 'my-project'
const number = 1
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'
const base = `${proto}//${location.host}/api/console/${encodeURIComponent(project)}/${number}?cols=120&rows=40`
const RETRYABLE = new Set([4503, 1006, 1012])
const KEEPALIVE = new Uint8Array([0, 0, 0]) // an empty input frame
function connect(attachOnly) {
const socket = new WebSocket(attachOnly ? `${base}&attach_only=1` : base)
socket.binaryType = 'arraybuffer'
const decoder = new TextDecoder() // streaming decode: frames can split a character
const keepalive = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) socket.send(KEEPALIVE)
}, 25000)
socket.addEventListener('message', (frame) => {
console.log(decoder.decode(new Uint8Array(frame.data), { stream: true }))
})
socket.addEventListener('close', (closed) => {
clearInterval(keepalive)
console.log(`closed: ${closed.code} ${closed.reason}`)
if (RETRYABLE.has(closed.code)) {
setTimeout(() => connect(true), 1000) // the session is still running: attach to it again
}
})
return socket
}
// Type into the terminal: a type-0 frame.
function send(socket, text) {
const payload = new TextEncoder().encode(text)
const frame = new Uint8Array(3 + payload.length)
frame[0] = 0
frame[1] = payload.length >> 8
frame[2] = payload.length & 0xff
frame.set(payload, 3)
socket.send(frame)
}
const terminal = connect(false)
// Once it is open: send(terminal, 'ls\r')
A worktree that does not exist closes at once and prints closed: 4404.