Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
triaged
Persistent daemon process that manages terminal session state, PTY multiplexing, and canonical VT performance structures for Triage, the attention-routing terminal supervisor.
The daemon runs persistently in the background, keeping terminal scrollbacks, layout grids, and active PTY handles alive even when no clients are attached.
It runs on macOS, Linux, and Windows. The local control plane uses a Unix
domain socket on macOS/Linux and a named pipe on Windows, and triaged can
register itself to start at login on each platform (see
Running as a background service). Terminal
sessions run on ConPTY on Windows and on a standard PTY elsewhere.
Installation
Building from source needs a nightly toolchain on aarch64. The
cerainference engine behind the session summarizer uses unstable NEON intrinsics there, and currently requires 1.99.0-nightly or newer (nightly-2026-07-08and later). On an older toolchain the build fails insidecerawith a wall ofE0658errors rather than a clear message.rustup toolchain install nightlyand retry withcargo +nightly install triaged. The unstable intrinsics are gated to aarch64, so x86-64 does not hit this. Prebuilt binaries below need none of this.
Or grab a prebuilt binary instead of compiling: releases after v0.1.6 attach a
Triage-cli-<os>-v<version> archive (.tar.gz for macOS/Linux, .zip for
Windows) to the
Releases page, containing the
triaged, triage, and triage-mcp binaries — unpack it and put them on your
PATH. The bundled triaged already embeds the web client. Each archive is
signed and checksummed — see Verifying a download.
Architecture. Each archive is built on its GitHub-hosted runner: the macOS binaries are Apple Silicon (arm64) (
macos-latest), Linux and Windows are x86-64. On a different architecture (e.g. an Intel Mac), install viacargo installinstead.
Running the Daemon
Start the persistent supervisor process:
Running as a background service
Instead of launching triaged by hand, register it to start automatically at
login and run in the background:
This installs a per-user service that runs inside your login session — so it can own your interactive terminals and the per-user control socket/pipe — using each platform's native mechanism:
| Platform | Mechanism | Location |
|---|---|---|
| macOS | LaunchAgent (launchctl) |
~/Library/LaunchAgents/com.hyeons-lab.triaged.plist |
| Linux | systemd user unit (systemctl --user) |
~/.config/systemd/user/triaged.service |
| Windows | Scheduled Task at logon (schtasks) |
task triaged |
install embeds the path of the triaged binary you ran, so install from the
binary you want the service to launch (e.g. the one cargo install triaged
placed on your PATH).
Linux: surviving logout. A systemd
--userservice stops when your last session ends. To keeptriagedrunning after you log out, enable lingering:loginctl enable-linger $USER(theinstallstep prints this reminder).
Web Server & Connecting
triaged embeds an HTTP/WebSocket server that, by default, listens on
0.0.0.0:7777 — all interfaces, so the client can connect from another device
on your LAN/tailnet. Access is gated by device-code + PIN pairing
(require_pairing, default true), and the daemon logs a warning at startup when
bound to an unspecified address. A single TCP port serves three things:
- Web client — a built-in browser UI is served at
/. Open http://127.0.0.1:7777 (orhttp://<daemon-host>:7777from another device) to attach to your sessions from a browser, no separate install required. - WebSocket API — clients (the web UI, the native desktop/mobile clients, or
your own integration) attach over
ws://<daemon-host>:7777/ws. - Pairing approval page — served at
/pair(see Pairing below).
Connecting from another device
The default bind (0.0.0.0:7777) is already reachable from other devices on your
network. To restrict the daemon to loopback instead, set a narrower bind address
in ~/.config/triage/config.toml:
[]
= "127.0.0.1:7777" # loopback only; or a specific tailnet IP
= true
Then point a client at http://<daemon-host>:7777 (web UI) or
ws://<daemon-host>:7777/ws (API). The daemon serves plain HTTP and WebSocket
only — it does not terminate TLS, so front it with a reverse proxy (e.g. Caddy
or nginx) if you need https/wss. Because the daemon owns live PTYs and
scrollback, you can detach and re-attach from any client without disturbing the
running shells.
Pairing approval is host-only by default. Even with a routable bind, the
/pairapproval page is only served to loopback / same-host connections unless you opt in to tailnet identity approval (see Pairing).
Prebuilt desktop clients
Besides the browser UI, native desktop clients are published as artifacts on each
GitHub release. Publishing a new triaged version automatically creates the
v<version> tag, compiles the Flutter client on each platform, and attaches the
builds to the Releases page:
| Platform | Asset | Contains |
|---|---|---|
| macOS | Triage-macos-v<version>.zip |
Triage.app |
| Windows | Triage-windows-v<version>.zip |
triage_client.exe + DLLs + data/ |
| Linux | Triage-linux-v<version>.tar.gz |
triage_client + lib/ + data/ |
Download the one for your OS, unpack it, and point it at your daemon's address.
These builds carry no OS code-signing certificate. The macOS client is ad-hoc signed; the Windows and Linux clients are unsigned. macOS and Windows will warn before running them — follow the per-platform steps below, or build from source if you'd rather not bypass those protections.
This is unrelated to the minisign signatures carried by assets from releases after
v0.1.6(see Verifying a download). Developer ID signing and notarization for the macOS client are wired up but conditional — see Release signing.
macOS — unzip, then open the app. If Gatekeeper blocks it (the download quarantine flag), clear the flag and retry:
# Blocked? Drop the "downloaded from the internet" flag and retry:
Without the terminal you can instead right-click Triage.app → Open → Open
in the dialog (only needed once); if it's still blocked, allow it under System
Settings → Privacy & Security → Open Anyway.
Windows — unzip and run triage_client.exe. SmartScreen may show "Windows
protected your PC" → choose More info → Run anyway.
Linux — extract and run the binary. A Secret Service provider (e.g.
gnome-keyring) must be available for the pairing token to persist:
Verifying a download
Releases after v0.1.6 attach a
minisign signature (.minisig) and a
.sha256 checksum to every asset — the desktop clients above and the CLI
archives alike; releases through v0.1.6 predate this and have neither.
Download the asset and both sidecars into the same directory and run the
commands from there: the .sha256 records only the asset's basename, so
sha256sum -c fails to find it from anywhere else. With <asset> your
archive's filename, and minisign from your package manager
(brew install minisign, apt install minisign):
# macOS has no sha256sum; use: shasum -a 256 -c <asset>.sha256
A good signature prints Signature and comment signature verified and a trusted
comment of the form triage release vX.Y.Z. The public key above is pinned from
.github/minisign.pub;
see Release signing
for the scheme, key custody, and rotation policy.
Pairing
When require_pairing is enabled (the default), every client must complete a
one-time PIN exchange before it can attach. This is a device-authorization-style
flow, and the approval step is restricted to the daemon host by default. You
can also opt in to approval from allowlisted Tailscale identities.
- Challenge. A new client connects to
/wsand sends ahellowith itsclient_id(and a stored token, if it has one). With no valid token the daemon treats it as unauthenticated, and the client requests a pairing challenge. The daemon returns a short-liveddevice_code. - Approve. The client surfaces the device code. By default, open the
approval URL on the machine running the daemon —
http://127.0.0.1:7777/pair?device_code=<device_code>. By default the/pairpage is served to loopback / same-host connections (is_local_pairing_peer). Ifpair_approval_tailnet_usersis configured, it is also served to remote peers whose authenticated Tailscale login is on that allowlist. (Settingpair_approval_trust_local_peers = false— for a loopback reverse-proxy deployment — drops the loopback/same-host shortcut entirely, so even127.0.0.1requests must be on the tailnet allowlist or/pairreturns 404.) The page validates the device code and displays a one-time, device-bound PIN with an expiry. - Enter the PIN. That PIN is typed back into the waiting client. The client
exchanges it (
pair(pin, client_id)) and the daemon — after verifying the PIN is bound to that exact client/device — issues a bearer token. - Attach. The client stores the token and reconnects with
hello { client_id, token }; the daemon authenticates it and the session attaches. The token is reused on subsequent launches, so pairing is a one-time step per client.
Pairing can be disabled for trusted, isolated setups by setting
require_pairing = false under [remote], in which case clients attach without
the PIN exchange.
To approve pairing from your own tailnet devices, add the Tailscale login names
that may open /pair:
[]
# Bind to this host's Tailscale IP so only tailnet traffic can reach /pair
# (see the security caveats below); avoid 0.0.0.0 with tailnet approval.
= "100.x.y.z:7777"
= true
= ["you@example.com"]
# Optional. Set false when a loopback reverse proxy fronts the daemon, so
# forwarded requests are NOT auto-trusted as local and must pass the allowlist.
# pair_approval_trust_local_peers = false
When a non-local peer requests /pair, triaged runs
tailscale whois --json <peer-ip>:<peer-port> on the daemon host, reads
UserProfile.LoginName, and compares it with the allowlist. If the tailscale
CLI is missing, the lookup times out, or the login is not allowlisted, /pair
remains unavailable to that peer. A successful lookup is cached per peer IP for
a few seconds; a failed lookup is cached only briefly, so a transient
tailscale hiccup won't lock out a legitimate user for long. triaged logs a
startup warning if the allowlist is set but tailscale isn't runnable, or if it
is bound to an unspecified address.
Security caveats for tailnet approval. Identity is derived from the peer's TCP connection, so deploy accordingly:
- Bind to the tailnet interface, not
0.0.0.0. With an all-interfaces bind the daemon trusts the connection's source IP as the identity input; bind to the host's Tailscale IP (e.g.bind = "100.x.y.z:7777") so only traffic that actually arrives over tailscale can reach/pair.triagedwarns at startup when an allowlist is configured on an unspecified bind.- A loopback reverse proxy bypasses the local-peer check.
triagedterminates no TLS, so an HTTPS reverse proxy forwards over loopback — every proxied request then looks like a same-host connection. By default such peers are auto-approved; setpair_approval_trust_local_peers = falseso that even loopback peers must pass the tailnet allowlist (the proxy must then forward genuine tailnet source IPs, or enforce the allowlist itself).- Tagged nodes share one identity. Tailscale reports every tag-owned (non-user) node with the synthetic login
tagged-devices, so it is rejected from the allowlist — list real user logins, not shared/service identities.- Allowlisted identities can self-approve. An allowlisted device can both request and approve its own pairing, so list only identities you trust to authorize new devices — the allowlist replaces, it does not add to, the "approval requires host access" guarantee.
Update checks
triaged periodically asks the release host for the latest published version and
surfaces an "update available" notice — the triage TUI shows a one-row banner
naming the newer version. Nothing is ever downloaded or installed
automatically; the check is a notification, and upgrading stays your call
(cargo install triaged, or a new release archive, followed by
a handover).
Defaults, in ~/.config/triage/config.toml:
[]
= true # set false to disable the check entirely
= 6 # how often to poll; must be > 0
= "stable" # the only channel supported today
The config parser rejects unknown keys, so a typo here is a startup error rather than a silently ignored setting.
Zero-Downtime Upgrades (Process Handover)
On Unix-like operating systems (including Linux, WSL, and macOS), triaged supports zero-downtime updates. This allows you to upgrade the daemon binary or restart the service without dropping active terminal sessions or interrupting running foreground shells.
How it Works
The upgrade is performed using a robust, low-level Three-Phase Sync Protocol:
- Transfer Phase: The new daemon process is launched and connects to the running old daemon over a Unix Domain Socket, initiating a file descriptor transfer using
SCM_RIGHTS(sendmsg/recvmsg). The old daemon passes all active master PTY file descriptors and the bound TCP listening socket directly to the new process. - Adoption & Sync Phase: The new daemon adopts the active descriptors, reconstructs the in-memory virtual terminal grids and scrollback history by replaying the session log files, and starts network supervision.
- Teardown Phase: Once adopted, the new daemon writes a synchronization byte back to the old daemon. The old daemon gracefully drops its session references (without closing the underlying shells), closes its Unix socket, and exits, completing a zero-downtime handover.
Initiating a Handover
To upgrade or restart the daemon with zero downtime, run the new binary with the --handover (or -U) flag:
Windows Support (no zero-downtime handover)
The daemon runs natively on Windows: its local control plane uses a named pipe
(\\.\pipe\triage-<user>) in place of the Unix domain socket, and terminal
sessions run on ConPTY via portable-pty. Clients (TUI, MCP, GUI) connect the
same way they do on macOS and Linux.
The one capability that does not cross over is zero-downtime handover — it
relies on low-level file-descriptor passing (SCM_RIGHTS) that is native to
POSIX platforms. On Windows, upgrading or restarting the daemon falls back
gracefully to Triage's robust Session Restore flow, which saves session
metadata and restores shell/workspace layout structures on restart.