Skip to main content

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://.

PartMeaning
{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:

ParameterMeaning
cols, rowsThe terminal's starting size in characters. The node picks a size when they are absent.
nodeThe id of the node that hosts the worktree. Send it when two nodes have a worktree with the same project and number.
attach_only1 to attach to the session that is already running and never start a new one. Send it when you reconnect.
providerWhich 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:

ByteMeaning
0Frame type
1–2Payload length, big-endian, at most 65,535
3…Payload
TypePayload
0 inputBytes to type into the terminal, as UTF-8. Split long input over several frames.
1 resize4 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.

CodeMeaningReconnect?
1000The terminal session ended.No — open the console again to start a new session.
4401Connection refused.No.
4404No such worktree, one you cannot open, or a query parameter value it does not accept.No.
4409Your organization's limit on active worktrees is full. The close reason is a sentence to show the user.No — free a worktree first.
4502The 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.
4503The 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.