Wire protocol
This page describes the protocol between the phone and Tealhand Host as implemented in tealhand-proto,
tealhand-host (src/ctl.rs, src/pairing.rs, src/files.rs), tealhand-files (src/serve.rs) and
tealhand-core (src/link.rs, src/files.rs, src/transfer.rs). Capability version: 12.
Since version 7 the client side is a phone or one of the owner’s computers (decision 52): a computer’s own
client is tealhand-core in computer mode inside the Tealhand app’s process, signs with the computer’s endpoint
key, dials through the Host’s own iroh endpoint, and says tealhand-computer/<version> in its Hello. Below,
“phone” in a direction means either client unless the text says a phone only.
Transport
Section titled “Transport”- iroh 1.x QUIC between two endpoint keys (Ed25519). iroh proves each peer’s key in the TLS handshake.
- The ALPN is chosen once per connection: one connection per pair of devices and ALPN.
tealhand/pair/1: accepted from anyone, but only while the computer’s connection door is open (a QR is showing) and fewer than 4pair/1connections are live. Otherwise the connection is closed withNotAllowed.tealhand/ctl/1,tealhand/files/1and every other ALPN: only for current, unexpired roster members, in both directions. Otherwise closed withNotOnRoster.tealhand/files/1carries the shared-folder protocol (Thefiles/1session).tealhand/ssh/1carries a terminal, for phones and computers on the device list while SSH access is on: each stream starts with a signedshell.openthat the computer checks before it reads any SSH byte (tealhand-ssh).- Reserved, no messages yet:
tealhand/blobs/1,tealhand/llm/1,tealhand/cam/1.
Framing
Section titled “Framing”Each frame is a 4-byte big-endian length followed by that many bytes: the postcard encoding of an Envelope.
MAX_FRAMEis 65,536 bytes. A reader refuses a larger length before reading the body; a writer refuses to write one. The stream is then closed.- Decoding is strict: bytes left over after the envelope are an error.
read_frameandwrite_frameare not cancel-safe: read from a dedicated loop and close the stream on any error.
Envelope { v: u32 capability version of the sender (ctl/1: the negotiated version after Hello) ty: MsgType id: u64 request id; answers carry the id of what they answer; unasked events use 0 body: bytes postcard of the struct that `ty` names}Enums are append-only
Section titled “Enums are append-only”postcard encodes an enum variant as its position in the declaration, as a varint. So the variant order of every wire enum is the wire format: new variants go at the end, nothing is reordered or removed. Unknown variants are a hard decode error.
Message types
Section titled “Message types”| Value | MsgType |
Body | Direction |
|---|---|---|---|
| 0 | Hello |
Hello { version: u32, min_version: u32, app: String } |
Both, first on ctl/1 |
| 1 | Request |
Request { command: bytes } (an encoded SignedCommand) |
Phone or computer to computer |
| 2 | Response |
Response { result: Result<bytes, ErrorCode> } (Ok: an encoded ActionOutput) |
Computer to phone |
| 3 | Subscribe |
Subscribe { command: bytes } (a signed device.status) |
Phone or computer to computer |
| 4 | StatusPush |
StatusPush { status: bytes, tasks: bytes } (postcard DeviceStatus and Vec<TaskInfo>) |
Computer to phone |
| 5 | Event |
Event { at: u64, kind: EventKind } |
Computer to phone |
| 6 | Close |
(none read) | Either |
| 7 | PairHello |
see the pair/1 session |
Phone to computer |
| 8 | PairReply |
see the pair/1 session |
Computer to phone |
| 9 | PairConfirm |
PairConfirm { roster: bytes } (an encoded SignedRoster) |
Phone to computer |
| 10 | PairResult |
PairResult { ok: bool, error: Option<ErrorCode> } |
Computer to phone |
| 11 | RosterPush |
RosterPush { roster: bytes } (an encoded SignedRoster) |
Phone to computer (from a computer: NotAllowed) |
| 12 | SessionRequest |
SessionRequest { action: bytes } (a postcard Action, unsigned), on files/1 only, after the signed opener |
Phone to computer |
| 13 | ApprovalAsk |
ApprovalAsk { command: bytes } (an encoded SignedCommand: shell.open, or shell.run_key of a saved command marked for the fingerprint), version 9 |
Computer to computer |
| 14 | ApprovalAnswer |
ApprovalAnswer { id: u64, grant: Option<bytes> } (an encoded SignedApproval; None declines), version 9 |
Phone to computer |
| 15 | PutResume |
PutResume { command: bytes, key: [u8; 16] } (a signed files.put that does not replace), on files/1 only, version 10 |
Phone or computer to computer |
| 16 | PutEnd |
PutEnd { sha256: [u8; 32] } (the whole file), after a resumable upload’s bytes, version 10 |
Phone or computer to computer |
| 17 | PutDiscard |
PutDiscard { key: [u8; 16] } (drop a paused upload), version 10 |
Phone or computer to computer |
Versions
Section titled “Versions”CAPABILITY_VERSION = 12 and MIN_PEER_VERSION = 6. negotiate(peer) refuses the peer with VersionTooOld
when peer.version < MIN_PEER_VERSION or peer.min_version > CAPABILITY_VERSION; otherwise the session version
is the lower of the two versions. The receiver checks the minimum itself, whatever the sender claims.
The history, one integer per protocol change: 1 is the first version (2026-10-16); 2 adds RosterPush, the power and
rule events, the NoGpu, TooManyRules and TooLate codes and CancelledBy; 3 adds the files/1 ALPN,
SessionRequest, the error codes 17 to 25, the download and run events, the field wake in PairReply, and the
new actions and outputs below; 4 adds RunInfo.output, the end of a finished saved command’s output in
shell.list; 5 adds download pages (DownloadState::Page, DownloadInfo.links and the DownloadPage event); 6
makes download.start name its folder (into), adds the Default change to files.share and adds commands.add
and commands.remove; 7 lets a computer on the device list command the owner’s other computers and adds the
PeerChanged event and CancelledBy::Computer. The minimum stays 6: a Host sends neither addition to a peer that
negotiated 6 (it tells CancelledBy::Computer as ThisPc), so a version-6 phone keeps working. 8 adds os and
wake to StatusPush (an older peer gets the version-7 body). 9 adds the owner’s phone approval of a computer’s
terminal or fingerprint command (decision 54): ApprovalAsk, ApprovalAnswer, the ApprovalRequest and
ApprovalClosed events, ApprovalWhat, and the codes 26 to 28; a Host sends the events only to phones that
negotiated 9 and the codes only in answer to an ApprovalAsk. 10 adds files.move and files.hash, the messages
15 to 17 (resumable uploads), the outputs PutFrom and Hash and the code 29 Mismatch (decisions 57 and 58); a
client sends them only to a Host that negotiated 10 (an older Host gets a plain files.put and no check at the
end, and the phone says to update it before a move). 11 adds lans to StatusPush: up to 4 opaque
8-byte LAN tags (decision 56, wakers), sent only to a peer that negotiated 11 (versions 8 to 10 get the version-8
body). 12 adds a
coding agent on the computer itself asking the phone (decision 55): ApprovalWhat::AgentTool and the AgentNotice
event, sent only to phones that negotiated 12. 18 adds the code Disconnected (a computer disconnected one
phone, also the QUIC close code; decision 59). 19 adds member phones and the recovery code (decisions 60 and 61):
device lists in format v2, PairReply.managed, PairJoin, PairResult.roster, ApprovalWhat::AddPhone, the
Roster event, RecoverAsk, RecoverBase and RecoverConfirm; a Host sends them only to a peer that negotiated
19, and the owner phone signs a v2 list only when every computer on it negotiated 19. A build refuses a peer below 6, so update both
ends.
The ctl/1 session
Section titled “The ctl/1 session”One QUIC connection per client (phone or computer) and computer, and one long-lived bidirectional stream, opened by the client.
- Hello. The phone sends
Hellowithin 10 seconds, with the envelope’svequal tohello.version(otherwiseMalformed). The computer answers with its ownHello(app=tealhand-host; the phone sendstealhand-phone/<version>, a computer’s own clienttealhand-computer/<version>). When the versions do not overlap, the computer closes the connection withVersionTooOldand sends noHelloof its own; the phone reads that close code. From then on every frame must carryv= the negotiated version, or the connection is closed withMalformed. Theappstring is capped and only logged at debug level. - Request and Response.
Request { command }is answered by aResponsewith the sameid. - Subscribe and StatusPush.
Subscribe { command }, where the command is a signeddevice.status, is checked like any request. On success the computer sends aStatusPushat once and then every 5 seconds, all with theSubscribe’sid, and re-checks the phone’s roster membership before each push (closing withNotOnRosterwhen it is gone). On failure it answers with aResponsecarrying the error, and there is no subscription. One subscription per connection; a second one getsBusy. When the computer cannot read its own status, it still sends a push with nothing known, so the phone does not mistake silence for a dead link. - RosterPush.
RosterPush { roster }is answered by aResponse:Okwith an empty body, or the error. Only a phone delivers the device list: from a computer it isNotAllowedand a journal line. The roster must be signed by the stored owner. The same roster again (same hash) is answeredOkand changes nothing, so a push whose answer was lost can be repeated. Any higher version of the same owner is accepted, without aprev_hashmatch, so a computer that missed versions catches up; a lower or equal version with a different hash isRollback, and another owner is refused. A higher version may remove, rename or expire devices and add computers, but it may not add a phone, change the kind or the keys of a device already listed, or give a new computer keys (NotAllowed, the list stays). If it is a valid higher version that no longer lists this computer, the computer wipes its saved connection, answersOk, and closes all connections. - Event. The computer sends
Eventframes withid0, unasked. 5a. Approval (version 9). A computer sendsApprovalAsk { command }before ashell.open, or ashell.run_keyof a saved command marked for the fingerprint, on another computer. The Host verifies the command like aRequest, checks every other gate (remote control, SSH access, the elevated Host), answersNoPhoneat once when no phone with a presence key is connected at version 9, else sendsApprovalRequestto its phones and answers theApprovalAskwhen one of them did, or after 60 s withNoAnswer; other frames go on meanwhile. A phone answersApprovalAnswer { id, grant }:Nonedeclines (Declinedfor the computer); a grant is checked against that phone’s presence key (see Signed payloads) and leaves a one-time ticket (30 s): the computer’s next matching command counts as having presence. - Close. Either side may send
Close. A secondHello, a message type only the computer sends, or a body that does not decode closes the connection withMalformed. A frame that is too large or broken just ends the session, with no close code.
Limits on the computer: 30 requests (Request, Subscribe, RosterPush, ApprovalAsk or ApprovalAnswer) per 10
seconds per connection, at most 2 approvals waiting per computer and 8 per Host, more get
Busy; at most 4 connections per phone; 120 seconds of silence from a phone without a subscription closes the
connection (Expired); a write that takes over 10 seconds closes it (Expired). The phone treats 25 seconds of
silence from a subscribed computer as a lost link and reconnects.
Events
Section titled “Events”| Value | EventKind |
Meaning |
|---|---|---|
| 0 | RuleFired { rule_id: u64, deadline: u64, then: PowerAction } |
A rule’s shutdown or sleep notice started; deadline in Unix seconds; then is Shutdown 0 or Sleep 1 |
| 1 | PowerCancelled { by: CancelledBy } |
The notice was cancelled; by is Phone 0, ThisPc 1 (the person at the computer, or the Host itself), RosterChange 2 (the phone that asked left the list) or Computer 3 (another computer on the list, version 7). No text crosses the wire: the phone words it |
| 2 | RuleRefused { rule_id: u64, reason: u8 } |
A rule was refused: 0 another user signed in, 1 task not found, 2 remote control off |
| 3 | RuleCancelled { rule_id: u64 } |
A rule was cancelled |
| 4 | PowerPending { deadline: u64 } |
A power.shutdown (not a rule) notice started |
| 5 | RuleNotify { rule_id: u64 } |
A notify rule fired |
| 6 | PowerFailed { rule_id: Option<u64> } |
A shutdown or sleep did not happen (the helper failed, or a power.shutdown was refused at its deadline); None without a rule |
| 7 | DownloadProgress { id: u64, received: u64, total: Option<u64> } |
A download made progress, at most once a second |
| 8 | DownloadDone { id: u64, size: u64 } |
A download finished |
| 9 | DownloadFailed { id: u64, code: ErrorCode } |
A download stopped for good |
| 10 | RunDone { session: u64, exit_code: Option<i32>, tail: String } |
A saved command finished, with the end of its output (at most 2 KiB, cleaned). The phone notifies you when a run ends |
| 11 | DownloadPage { id: u64 } |
A download’s link answered with a web page, not a file (version 5): download.list has the file links found on it |
| 12 | PeerChanged { by: [u8; 32], action: String, detail: Option<String> } |
Version 7: another computer on the list changed something here (anything but a read). by is its endpoint key (the phone names it from its list), action the action’s id such as files.delete, detail a short name the Host chose (a file or folder name, a shared folder, a download’s host, a saved command’s key; at most 64 characters), never a path, a link, text or file content. Sent only to phones that negotiated 7, never to computers. The phone shows “Studio PC deleted a.txt on Cloud PC” |
| 13 | ApprovalRequest { id: u64, by: [u8; 32], what: ApprovalWhat, nonce: [u8; 16], expires_at: u64 } |
Version 9: the computer by asks to do what here (ShellOpen 0, or RunKey { key } 1), or, version 12, a coding agent on this computer itself (by is this computer) asks to use a tool (AgentTool { agent, tool, summary, folder } 2; answered like the others, and an approval leaves no ticket) and waits until expires_at. Phones only. The phone shows “Studio PC wants to open a terminal on Cloud PC” with Decline and Approve |
| 14 | ApprovalClosed { id: u64 } |
Version 9: that request ended (answered, declined, timed out); the phone takes it away |
| 15 | AgentNotice { agent: String, notice: AgentNoticeKind, folder: String } |
Version 12: a coding agent on this computer needs the person (NeedsInput 0) or finished (Finished 1) in the project folder. Phones only. The phone shows “Claude finished in ~/app on Lab PC” |
When the work came back during a rule’s notice, the computer sends PowerCancelled { by: ThisPc }.
The pair/1 session
Section titled “The pair/1 session”One bidirectional stream, opened by the phone. There is no Hello: every envelope’s v must equal
CAPABILITY_VERSION, otherwise VersionTooOld.
- Phone:
PairHello { endpoint: [u8; 32], name: String, device_key: bytes, presence_key: bytes, proof: [u8; 32] }. The phone opens the stream within 10 seconds and sends the hello within 30 seconds. Each key field is the 33-byte compressed key followed by a 64-byte signature of the bind message (97 bytes). - Computer:
PairReply { endpoint: [u8; 32], name: String, os: String, proof: [u8; 32], wake: Vec<[u8; 6]> }, with the hello’sid.wakeis the computer’s MAC addresses for Wake-on-LAN, at most 4, and the transcript covers it. The Host sends an empty list until wake-on-LAN is built. - Phone:
PairConfirm { roster }, the new roster signed by thedevicekey, within 2 minutes. - Computer, after the person clicks Connect or Cancel:
PairResult { ok, error }with the confirm’sid. A timeout givesExpired; a refusalNotAllowed. When the hello or the confirm is refused, the computer sendsPairResultwith the error code at once and closes.
The QR text is tealhand:pair?v=1&p=<64 hex: computer key>&a=<hex: address bytes>&s=<32 hex: secret>&e=<expiry, decimal Unix seconds>, lowercase hex, exactly this order, at most 2208 bytes, with an address of 1 to 1024
bytes. The address bytes are the postcard of { version: u8 = 1, id: [u8; 32], relay: Option<String> (https, at most 256 bytes), direct: list of at most 12 IPv4 or IPv6 socket addresses }. Transcripts, proofs and
the check code: How commands are signed.
The files/1 session
Section titled “The files/1 session”One QUIC connection per phone and computer on tealhand/files/1, with at most 2 per phone. The phone opens every
stream. The full stream layout is the header comment of crates/tealhand-files/src/serve.rs; in short:
- Control stream, the first bidirectional stream.
Helloboth ways within 10 seconds, negotiated as onctl/1; from then on every frame carries the negotiatedv. Then the opener: aRequestwith a signedfiles.list, answered by aResponse(Ok(Roots | Listing)or an error). After a good opener the session is open, andSessionRequest { action }with an unsignedfiles.listorfiles.statis answered by aResponse. Another opener on the same connection replaces the session; a refused one ends it. - Transfer streams, one per transfer, opened after the computer’s
Hello, at most 8 at once (more getResponse(Err(Busy))):- Range read:
SessionRequestwith an unsignedfiles.get { path, offset, len }. The answer isResponse(Ok(FileData { size, modified, len }))withlencut at the end of the file, then exactlylenraw bytes in pieces of at mostFILE_CHUNK(64 KiB), then the computer finishes the stream. “To the end” islen = u64::MAX - offset. The phone may stop early. - Upload: a
Requestwith a signedfiles.put { path, size, on_conflict }; after every check the answer isResponse(Ok(empty)), then exactlysizeraw bytes, then the phone finishes its side, thenResponse(Ok(Entry)). A phone that resets its send side abandons the upload. - Resumable upload (version 10):
PutResume { command, key }with a signedfiles.putthat does not replace; after every check the answer isResponse(Ok(PutFrom { offset })), the bytes the computer already holds for that sender and key. Then exactlysize - offsetraw bytes, thenPutEnd { sha256 }with the SHA-256 of the whole file, then the sender finishes its side, thenResponse(Ok(Entry)), orMismatchwhen the hash differs (the temporary file is gone). A stream that breaks, stalls or is reset leaves the upload paused at its last synced byte; the nextPutResumewith the same key, path, size and conflict rule goes on from there.PutDiscard { key }drops a paused upload (Response(Ok(empty))). - Hash (version 10):
SessionRequestwith an unsignedfiles.hash { path }answersResponse(Ok(Hash { size, modified, sha256 })): how a copy to the phone or another computer is checked at its end. - Blob: a
Requestwith a signedscreen.shotanswersResponse(Ok(Shot { .. }))andlenraw bytes. The Host does not serve it yet.
- Range read:
- Closed with
Malformed: a frame that does not decode, a wrongv, a secondHello, aSessionRequestbefore the opener, an unsigned action other than the allowed reads, a signed action that does not belong onfiles/1, or an upload whose byte count is notsize. - Checked on every request: the phone is still a member and remote control is on (else the connection is closed with that code), the folder is still shared, and the path is resolved again.
Limits: Hello within 10 seconds; a session idle for 10 minutes is closed (Expired); 100 session requests and 30
signed requests per 10 seconds per connection (more get Busy); a transfer with no progress for 60 seconds is
reset; a frame write over 10 seconds closes the connection. The jail rules:
Files.
Signed payloads
Section titled “Signed payloads”SignedCommand { body: bytes, sig_device: [u8; 64], sig_presence: Option<[u8; 64]> }CommandBody { issuer: [u8; 32], target: [u8; 32], action: bytes, nonce: [u8; 16], issued_at: u64, expires_at: u64 }signed message: "tealhand-cmd-v1" || body
SignedRoster { body: bytes, sig: [u8; 64] }Roster { version: u64, prev_hash: [u8; 32], owner: [u8; 33], issued_at: u64, devices: Vec<DeviceEntry> }DeviceEntry { endpoint: [u8; 32], name: String, kind: DeviceKind (Phone 0, Computer 1), device_key: Option<[u8; 33]>, presence_key: Option<[u8; 33]>, added_at: u64, expires_at: Option<u64> }signed message: "tealhand-roster-v1" || body; hash = SHA-256(signed message)
SignedApproval { body: bytes, sig_presence: [u8; 64] }ApprovalBody { approver: [u8; 32], requester: [u8; 32], target: [u8; 32], what: ApprovalWhat, nonce: [u8; 16], issued_at: u64, expires_at: u64 }signed message: "tealhand-approve-v1" || body (the phone's presence key only; version 9)A grant is accepted only from the phone it names (approver is the connection’s peer, a phone on the roster),
signed by that phone’s presence_key, for exactly the request (requester, target, what, the Host’s nonce),
at most 90 s long, in time, and once.
sig_device is the issuer’s signature of the signed message: a phone’s ECDSA P-256 signature by its
device_key, or a computer’s Ed25519 signature by its endpoint key (checked strictly; version 7). The key tried is
fixed by the roster entry of the connection’s peer. A computer’s command never carries sig_presence.
Endpoint keys and nonces are fixed-size arrays (no length prefix). Hardware keys (33 bytes, compressed) and
signatures (64 bytes, raw r then s) are length-prefixed byte strings. Decoding is strict, and a decoded body must re-encode to the same
bytes, so every message has exactly one encoding.
Actions and outputs on the wire
Section titled “Actions and outputs on the wire”CommandBody.action carries a postcard Action. Its variant order is not the order of the action table:
| Value | Action |
|---|---|
| 0 | DeviceStatus |
| 1 | DeviceTasks |
| 2 | RuleList |
| 3 | RuleAdd(RuleSpec) |
| 4 | RuleCancel { rule_id } |
| 5 | PowerSleep |
| 6 | PowerShutdown |
| 7 | PowerCancel |
| 8 | DeviceRename { device, name } |
| 9 | DeviceRemove { device } |
| 10 | RemoteSet { enabled } |
| 11 | DeviceForget |
| 12 | FilesList { path: Option<FilePath>, cursor: u32 } (None: the shared folders) |
| 13 | FilesStat { path } |
| 14 | FilesGet { path, offset, len } |
| 15 | FilesPut { path, size, on_conflict } |
| 16 | FilesMkdir { path } |
| 17 | FilesRename { path, to, replace } |
| 18 | FilesDelete { path } |
| 19 | FilesShare(ShareChange): Add { path, name, read_only, downloads }, Remove { root } or Default { root: Option<RootId> } (version 6; None clears the default for downloads) |
| 20 | DownloadStart { url, name, into } (into: the shared folder to save in, any one that is not read only; None: the default for downloads) |
| 21 | DownloadCancel { id } |
| 22 | DownloadList |
| 23 | ShellOpen { cols, rows } |
| 24 | ShellRunKey { key, session } |
| 25 | ShellClose { session } |
| 26 | ShellList |
| 27 | ClipGet |
| 28 | ClipSet { text } |
| 29 | PowerWake { device, macs } |
| 30 | ScreenShot |
| 31 | CommandsAdd { key, label, line, cwd, needs_presence } (version 6; only while SSH access is on) |
| 32 | CommandsRemove { key } (version 6; only while SSH access is on) |
| 33 | FilesMove { from, to_folder, on_conflict } (version 10) |
| 34 | FilesHash { path } (version 10; unsigned, in a files/1 session) |
A path is FilePath { root: RootId(u32), rel: Vec<String> }: a shared folder’s number and the names below it. A
Response’s Ok bytes carry a postcard ActionOutput:
| Value | ActionOutput |
|---|---|
| 0 | Status(DeviceStatus) |
| 1 | Tasks(Vec<TaskInfo>) |
| 2 | Rules(Vec<RuleInfo>) |
| 3 | RuleAdded(RuleId) |
| 4 | Ok |
| 5 | Roots(Vec<RootInfo>) |
| 6 | Listing(Listing) |
| 7 | Entry(FileEntry) |
| 8 | FileData { size, modified, len } (len raw bytes follow) |
| 9 | Downloads(Vec<DownloadInfo>) |
| 10 | DownloadStarted(DownloadId) |
| 11 | ShellOpened(SessionId) |
| 12 | Shell(ShellInfo) |
| 13 | RunStarted(SessionId) |
| 14 | Clip(String) |
| 15 | Shot { width, height, len } |
| 16 | PutFrom { offset } (version 10) |
| 17 | Hash { size, modified, sha256 } (version 10) |
Other wire enums: Trigger (ProcessEnds { name } 0, GpuIdle { minutes: u16 } 1), Then (Shutdown 0,
Sleep 1, Notify 2), RuleState (Waiting 0, Firing { deadline } 1, Done { at } 2,
Refused { reason } 3, Cancelled 4), RefuseReason (OtherUserSignedIn 0, TaskNotFound 1,
RemoteControlOff 2). DeviceStatus is { cpu_pct, gpu_pct: Option<f32>, mem_free, mem_total: Option<u64>, battery_pct: Option<u8>, uptime_s: Option<u64>, remote_control: bool }; TaskInfo is
{ pid: u32, name: String, cpu_pct, gpu_pct: Option<f32> }.
In JSON (the planner and the schema) an action is {"action": "<id>", "params": {...}}, with no params for
actions without parameters, and a device as 64 lowercase hex digits.
Error codes and close codes
Section titled “Error codes and close codes”ErrorCode values double as QUIC application close codes (0 is no error). In a postcard Response or
PairResult, the code is encoded as its position, which is the number minus 1.
| Close code | ErrorCode |
|---|---|
| 1 | Malformed |
| 2 | VersionTooOld |
| 3 | NotOnRoster |
| 4 | BadSignature |
| 5 | Expired |
| 6 | Replay |
| 7 | PresenceRequired |
| 8 | RemoteControlOff |
| 9 | OtherUserSignedIn |
| 10 | TaskNotFound |
| 11 | NotAllowed |
| 12 | Busy |
| 13 | Internal |
| 14 | NoGpu |
| 15 | TooManyRules |
| 16 | TooLate |
| 17 | NotFound |
| 18 | Exists |
| 19 | PathRefused |
| 20 | TooLarge |
| 21 | NoSpace |
| 22 | AddressRefused |
| 23 | FetchFailed |
| 24 | NotSetUp |
| 25 | Unsupported |
| 26 | Declined (version 9) |
| 27 | NoPhone (version 9) |
| 28 | NoAnswer (version 9) |
What each means and what the phone says: Error codes.