Telex
Telex is a CLI-first message fabric for AI agent sessions. Ephemeral sessions
attach to durable addresses, exchange typed operational messages with answerback
liveness, and leave an auditable disposition record. It runs as a single binary,
telex (telex.exe on Windows), over local SQLite (zero configuration) or
networked Postgres.
Who this is for
This guide is for people who install, configure, and operate telex, and who point
their agent sessions at it. Agents themselves read their operating instructions
from the binary at runtime with telex skill and telex copilot skill; those
instructions are version-matched to the installed binary and are not duplicated
here.
What it does
- A durable address names a responsibility to be served (a node, a workstream, a session).
- A per-user exchange daemon owns presence, delivery buffering, and the message store.
- A session attaches to an address, sends and receives typed messages, and records disposition by message id.
- Delivery is at-least-once and durable: a message persists until the recipient acks it, and a full history is available for audit.
Where to go next
- Install the binary.
- Run the Quickstart on the zero-config local store.
- Read the Concepts to understand addresses, delivery, attention, and disposition.
- Follow a Guide for agent setup, Copilot CLI push delivery, multi-session coordination, or a networked backend.
- Consult the CLI reference, generated from the installed binary.
Install
Telex is a single binary. Install a prebuilt binary, or build from source with Rust.
macOS / Linux
curl -fsSL https://raw.githubusercontent.com/lossyrob/telex/main/install.sh | sh
Windows (PowerShell)
irm https://raw.githubusercontent.com/lossyrob/telex/main/install.ps1 | iex
With Rust (any platform)
cargo install --git https://github.com/lossyrob/telex --features entra
The entra feature adds Azure Entra authentication for Postgres backends; the
published release binaries include it. Omit the feature if you do not need it.
Prebuilt binaries are also attached to each GitHub release.
Supported platforms
Prebuilt release binaries are published for Windows (x86_64 and ARM64), Linux
(x86_64), and macOS (Apple Silicon and Intel). On other platforms — including ARM
Linux (Raspberry Pi, Graviton, ARM WSL) — install from source with cargo install
(the install script points ARM-Linux users there automatically).
Verify
For an installation that must not change the Windows user PATH, set
$env:TELEX_NO_MODIFY_PATH = '1' before running install.ps1. The default still
adds the install’s bin directory to user PATH when absent.
Both installers accept the same trusted mirror overrides as telex upgrade:
TELEX_UPGRADE_API_BASE (default https://api.github.com) and
TELEX_UPGRADE_DOWNLOAD_BASE (default https://github.com). These are intended
for controlled release validation or an explicitly trusted mirror, not a new
authentication boundary. Do not send a real GITHUB_TOKEN to a fixture or
untrusted endpoint. An isolated install root alone does not isolate the daemon,
configuration, or database.
telex --version
Updating
Once telex is installed, update in place to the latest compatible public release:
telex upgrade
This discovers the latest GitHub release, downloads this platform’s asset, verifies its
SHA-256 checksum, and installs it through the versioned layout (keeping the previous version
for telex rollback). Pin an explicit release with telex upgrade --version vX.Y.Z, or
install a local build with telex upgrade --from <binary>. See
Operating telex for details, including the fail-closed behavior and
GITHUB_TOKEN for higher API rate limits.
Retaining a previous executable does not guarantee a safe downgrade. In particular, schema-3 migration is not reversed by rollback: v0.1.2 supports only schema 2 and refuses schema-3 stores. Back up before upgrading and reattach wanted addresses after ordered daemon replacement.
Initialize (optional)
Telex creates its local store and schema on first use, so no init step is required for the default SQLite store. To pre-create and validate a backend (useful for Postgres, to surface connection or permission errors early):
telex init --backend <name>
Shell notes
Examples in this guide use POSIX shell syntax. On Windows PowerShell, set
environment variables with $env: instead of export, for example
$env:TELEX_SESSION_ID = "quickstart". The binary is telex.exe, invoked as
telex.
Watcher runtime
Watcher is a separate application, not part of the single-binary Telex release
install. The Watcher installation and launch guide
provides its supported Windows/Linux bootstrap, default discovery record,
backend/authentication prerequisites, shutdown and rollback. Session-group stall
monitoring starts from that verified record. Both Telex and Watcher need the
entra feature when the existing backend profile uses Entra.
Copilot CLI plugin
If you drive agents with GitHub Copilot CLI, install the telex plugin from the marketplace so messages arrive as turns (push delivery):
copilot plugin marketplace add lossyrob/telex
copilot plugin install telex@telex
Release install scripts print a tag-pinned marketplace command
(copilot plugin marketplace add lossyrob/telex#vX.Y.Z) so the plugin and the
installed binary stay on the same release. See the
Copilot CLI push delivery guide.
Quickstart
No server setup and no configuration are required. The first daemon-backed
command auto-spawns a per-user local exchange over a SQLite store at
~/.telex/telex.db.
Send and read a message
Set a stable session id once, then send a message to yourself and read it back:
export TELEX_SESSION_ID=quickstart # PowerShell: $env:TELEX_SESSION_ID = "quickstart"
telex --address me send --to me --body "hello"
telex --address me inbox --all
send needs a session id (from --session or $TELEX_SESSION_ID) and a sender
address. Here --to me is the recipient and the global --address me supplies
the sender. inbox --all lists recent messages; the default inbox lists only
actionable ones (messages requiring disposition).
Print the agent usage guide
The binary carries its own runtime instructions for agents. Print them with:
telex skill
This is the generic (pull) workflow. In Copilot CLI, run telex copilot skill
for the push-delivery workflow instead.
A two-session taste
One session attaches to an address and waits; another finds it and sends. Run these in two terminals.
Session A registers an address and waits for one message:
export TELEX_SESSION_ID=session-a # PowerShell: $env:TELEX_SESSION_ID = "session-a"
telex attach --address session:a --description "session A waiting for coordination"
telex wait --address session:a
Session B registers, finds A by its description, and sends:
export TELEX_SESSION_ID=session-b # PowerShell: $env:TELEX_SESSION_ID = "session-b"
telex attach --address session:b --description "session B requesting status"
telex resolve --match "waiting for coordination"
telex send --to session:a --subject "Status request" --body "Please send status." --attention interrupt
Session A’s wait returns the message as JSON. A then acks it, dispositions it,
and can reply in the same thread:
telex ack --address session:a --id <message-id>
telex handle --id <message-id> --note "status prepared"
telex reply --to-message <message-id> --body "Holder is live; continuing work."
See Coordinate multiple sessions for the full pattern, including the re-arm loop and attention phases.
Concepts overview
Telex has a small set of concepts. This page names them; the following pages cover each in detail.
- Address: a durable name for a responsibility. Sessions attach to addresses; messages are sent to addresses.
- Exchange and daemon: a per-user local process that owns presence, delivery buffering, and the message store, reached over local IPC.
- Stations and presence: the in-memory registration a session holds while it attends an address, with a liveness lease.
- Messages and threads: typed messages with a sender, body, and thread context; replies thread under a parent.
- Attention: how urgent a message is, from
interrupttofyi, which controls when the recipient sees it. - Disposition: the auditable outcome recorded for a message: acknowledged, handled, deferred, rejected, closed, or escalated.
- Delivery guarantees: messages are durable and delivered at-least-once; consumption is an explicit ack.
- Backends: the configured store: local SQLite by default, or networked Postgres.
The shape of a session
A session has a stable identity (--session or $TELEX_SESSION_ID). It
attaches to one or more addresses, sends and receives messages, records
disposition by message id, and detaches or stops its station when done. The
exchange retains the durable message buffer across daemon restarts.
Next: Addresses
Addresses
An address is a durable name for a responsibility being served. Sessions attach to addresses, and messages are sent to addresses rather than to a process or a person. Because the address is durable and the session is ephemeral, a message survives the session that was attending it and is delivered when a session next attends the address.
Identity vs. address
A session’s identity (--session or $TELEX_SESSION_ID) is the stable id
the exchange uses to track membership. An address is what the session serves.
One session can attend more than one address.
Sender address
Every send and reply stamps a from address so replies can route back. It
resolves from an explicit --from, then $TELEX_ADDRESS or the global
--address, then the single address the session attends. If the session attends
more than one address and no --from is given, the send is refused as ambiguous
rather than guessed.
Directory metadata
When a session attaches it can register directory metadata so other sessions can find it:
--description: a one-line statement of what the session is doing.--scope: the project or workstream the address belongs to.--tags: coarse comma-separated tags (for examplerepo:telex,role:worker).
Find addresses by description substring, scope, or tag:
telex address list --scope <scope>
telex resolve --match "<substring>"
telex resolve --tag <tag> --scope <scope>
Retire an address so it drops from normal listings with
telex address retire --address <addr>.
Next: Exchange and daemon
Exchange and daemon
The exchange is a per-user local process (a daemon) that owns presence, delivery buffering, and the message store, reached by the CLI over local IPC. Sessions do not run a resident holder process of their own; they issue one-shot commands to the exchange.
Auto-spawn
There is no manual server to start. The first daemon-backed command auto-spawns
the exchange for the selected backend. With no configuration this
is a local SQLite store at ~/.telex/telex.db.
One-shot commands
Most verbs register or act and then exit:
attachregisters the session and address, then exits.waitblocks as one client for a single delivery, then exits.send,reply,ack, and the disposition verbs act once and exit.
Because commands are one-shot, telex is driven from a script or an agent’s own turn cycle rather than a long-lived foreground process. See Set up an agent (pull).
Restart recovery
The exchange holds the durable message buffer in its store. If the daemon
restarts, the next verb reconnects and re-registers on a NeedsAttach signal and
continues against the retained buffer. A wait that finds no daemon exits with a
distinct code so the caller can run attach (the spawning and recovery verb) and
re-arm. See Exit codes.
Next: Stations and presence
Stations and presence
A station is the in-memory registration a session holds while it attends an
address. attach creates the station and claims a liveness lease (an epoch)
for the address; the exchange tracks the station’s health and any live waiter.
Presence is non-destructive
Detaching or a lease reap is non-destructive: the station and the durable message
buffer remain. A blocked wait returns a distinct presence-ended result rather
than losing the buffered messages. A later attach re-registers and continues.
Liveness backstop
attach --watch-pid anchor:<pid> registers a non-destructive liveness backstop.
When the watched process dies, blocked waits return a presence-ended result, but
the station and durable buffer are retained. This lets a station be cleaned up
when its owning process goes away without dropping messages.
Inspecting stations
Get a machine-readable projection of a session’s attended addresses, waiter counts, and station health:
telex station status --session <id>
Stop a station as the symmetric inverse of the attach and wait loop; it marks the station non-attending, releases membership durably, and waits for tracked live waiters to exit:
telex station stop --address <addr>
After station stop, a later message to the address stays queued until a future
attach or wait; it is not consumed by an orphaned waiter.
Next: Messages and threads
Messages and threads
A telex message is a typed operational message with a sender (from), a
recipient (to), an optional subject, and a body. Messages can carry CC
recipients (visible observers), a kind label, an
attention level, and arbitrary metadata.
Sending
telex send --to <addr> --subject "<subject>" --body "<body>"
telex send --to <addr> --subject "<subject>" --body-file <path> # UTF-8 file; - for stdin
telex send --to <addr> --subject "<subject>" --body-stdin # read body from stdin (UTF-8)
--body, --body-file, and --body-stdin are mutually exclusive and exactly one is required.
Prefer --body-file or --body-stdin for multiline or structured content (Markdown, code blocks,
JSON) to avoid shell quoting limits. Files and stdin are read as UTF-8 and sent exactly as written.
--body-file - and --body-stdin are equivalent: both read the body from stdin.
Windows / PowerShell: piped stdin must be UTF-8. Before piping non-ASCII content, run:
$OutputEncoding = [System.Text.Encoding]::UTF8
"Status: café ✓" | telex send --to <addr> --subject "Status" --body-stdin
For generated bodies, writing to a UTF-8 file first is the most reliable path:
$body | Out-File -Encoding utf8 body.txt
telex send --to <addr> --subject "Status" --body-file body.txt
send prints a receipt: delivered or queued-unoccupied, plus the new message
id. A queued-unoccupied receipt is durable: the message is persisted and
delivered when a station next attends the address. Sending to a retired address
is an error (address <addr> is retired), not a receipt.
Threads and replies
Reply under a parent message; the reply threads under it and routes to the parent’s sender:
telex reply --to-message <message-id> --body "<body>"
CC (observers)
Add --cc <addr> to copy observer addresses on a message. Each recipient gets
its own delivery of the same message id, with a delivery_role of to or cc:
telex send --to node:worker --cc node:lead --subject "Status" --body "..."
The primary recipient and each CC observer see the same id, thread_id, and
primary_to, but their own delivery_role and delivered_to:
| Recipient | delivery_role | delivered_to | requires disposition |
|---|---|---|---|
node:worker (to) | to | node:worker | set when the sender passes --requires-disposition |
node:lead (cc) | cc | node:lead | no |
--cc may be repeated and accepts comma-separated values. Ack and disposition
are per recipient: acking for node:lead does not consume the copy for
node:worker. An observer reads its copy with telex inbox --all or
telex read, and acks its own (message_id, address).
Reading
telex inbox --address <addr> # actionable messages (add --all for recent)
telex inbox --address <addr> --all --limit N
telex read --id <message-id> --thread # a message with compact thread context
telex read --id <message-id> --full # full history
Next: Attention levels
Attention levels
Every message carries exactly one attention level. It expresses how urgent the message is and controls when the recipient sees it.
interrupt | next-checkpoint | background | fyi
interrupt: highest urgency; handled ahead of all other messages.next-checkpoint: handle after the current safe stopping point.background: visible in the inbox; does not interrupt current work.fyi: visible and auditable, non-actionable by default.
Set the level on send or reply with --attention <level>.
How attention maps to delivery
In push delivery (Copilot CLI), the exchange maps attention to send mode
automatically: interrupt is delivered as an immediate steering interjection
into the running turn, ahead of enqueued messages; every other level is enqueued
and arrives at the next turn boundary. Neither preempts a turn already running.
In pull mode, a waiter armed with --min-attention interrupt wakes only for
urgent messages; lower levels stay durably buffered for the recipient’s next
checkpoint. See Coordinate multiple sessions for
the two-phase attention loop.
Latency note: in push mode interrupt is seen mid-stream between the model’s
iterations; other levels wait for the turn boundary. In pull mode, agent wake
dominates perceived latency, so interrupt means “handle at the next turn
boundary.”
Next: Delivery guarantees
Delivery guarantees
Durable
The exchange never loses an accepted message. A queued-unoccupied send is
persisted and delivered when a station next attends the address. The durable
buffer survives daemon restarts.
At-least-once
Delivery is at-least-once. A message may occasionally be delivered more than once (for example after a reconnect, or in push mode after a session re-attach). Consumers dedupe by message id. A message remains eligible for delivery until the recipient explicitly acks it:
telex ack --address <addr> --id <message-id>
Printing or displaying a message is transport only; it is not consumption. This is deliberate: the safe failure direction is a duplicate, never a silent loss.
Ack is per recipient
Ack consumes (message_id, recipient-address). Acking for address A does not
consume the same message id for a CC recipient B, so observers see their own copy.
Terminal workflow disposition is separate
Ack is transport consumption. Closing out the work is a separate
disposition (handle, reject, or close). A message can be
acked (consumed from the delivery buffer) while still needing a terminal workflow
disposition.
Next: Disposition
Disposition
Disposition is the auditable outcome recorded for a message. It is separate from transport: receiving a message does not disposition it, and dispositioning a message is an explicit, recorded act.
Transport consumption: ack
ack marks the delivered (message_id, recipient-address) consumed in the
exchange delivery buffer. Ack is per recipient, so acking a message for address A
never consumes the same message id for a CC recipient B.
telex ack --address <addr> --id <message-id>
Workflow disposition states
acknowledged | handled | deferred | rejected | closed | escalated
- Terminal (removed from the actionable inbox):
handled,rejected,closed. - Non-terminal (still needing final disposition):
acknowledged,deferred,escalated.
Record a workflow disposition with the matching verb, optionally with a note:
telex handle --id <message-id> --note "completed"
telex defer --id <message-id> --note "waiting on input"
telex reject --id <message-id> --note "out of scope"
telex close --id <message-id>
telex escalate --id <message-id> --note "needs operator"
Dispositions default to the current --address recipient. Pass --recipient
only to record for another recipient intentionally.
Ordering
Ack first (transport consumption), then apply the workflow disposition that
reflects the actual outcome. A message that requires disposition
(--requires-disposition on send) stays actionable until it reaches a terminal
state.
Next: Backends
Backends
A backend is a named, configured store. Selection is by name:
--backend <name>, then $TELEX_BACKEND, then the configured default, then an
implicit default SQLite store at ~/.telex/telex.db. With no setup, telex works
on local SQLite.
SQLite (default)
Local, zero-config, single-user. Nothing to configure; the implicit default store
is created on first use. Override the path for one invocation with --db <path>
or $TELEX_DB.
Postgres (networked)
Configure a Postgres backend once, then select it by name or make it the default. The connection string is a libpq URI or a key=value DSN. Provide the password by reference, never embedded in the config:
--entra: Azure Entra; telex fetches the token itself (usesaz login, or--entra-cred managedon a devbox or VM with a managed identity). Requires a build with theentrafeature, which the release binaries include.--password-env <VAR>: read the password from an environment variable.--password-command <cmd>: run a one-shot command that prints the password. Telex owns supported invocation helpers through completion/cancellation and cleanup; persistent or escaped background work is outside this contract. See the credential lifecycle.
# Postgres with a password from an env var:
telex backend add staging \
--postgres "postgresql://app@staging-db:5432/telex?sslmode=require" \
--password-env STAGING_PG_PASSWORD --schema telex
# Azure Postgres with Entra:
telex backend add prod \
--postgres "host=myserver.postgres.database.azure.com port=5432 user=me@example.com dbname=postgres sslmode=require" \
--entra --schema telex --default
Managing backends
telex backend list
telex backend show <name>
telex backend default <name>
telex backend remove <name>
telex backend kinds # backend kinds compiled into this build
The first backend added becomes the default; --default or
telex backend default <name> changes it. See the
Networked Postgres backend guide.
Next: Security and data
Security and data
Where data lives
- Local SQLite store:
~/.telex/telex.db(override with--dbor$TELEX_DB). - Config (named backends):
~/.telex/config.toml. - Runtime directory (daemon IPC and lease state): a per-user runtime directory
(on Windows under
%LOCALAPPDATA%\telex\run; on Unix a per-user socket/runtime directory). - Copilot bridge files: in the Copilot session’s extension directory; removed
on
telex copilot detachor session end.
Trust model
The local exchange serves a single operating-system user over local IPC. A same-user process operates under the current trust model. Do not place a telex store or socket where other users can read it if the messages are sensitive.
Secrets
Postgres passwords are referenced, never written to the config file: use
--entra, --password-env, or --password-command. telex backend show
redacts secrets.
Credential-command diagnostics do not repeat command text, credential output, environment, DSNs, or helper stderr. Commands run with the current user’s environment and working directory; invocation ownership is cleanup, not a sandbox or an authorization boundary. Preexisting credential agents and broker-created work are not owned termination targets.
Message content
Message bodies, subjects, metadata, and disposition history are stored in the selected backend and are readable by anyone with access to that store (the local file, or the Postgres schema). Treat the store as sensitive if the messages are.
Postgres sharing
A Postgres backend is shared across the machines and users configured to use it.
Scope access with database and schema grants, and use a dedicated --schema.
Reporting a vulnerability
Report security issues privately, not through public issues or pull requests. See the repository security policy for the private reporting channel and what to expect.
Next: Glossary
Glossary
- Address: a durable name for a responsibility being served. Messages are sent to addresses. See Addresses.
- Session identity: the stable id (
--sessionor$TELEX_SESSION_ID) the exchange uses to track a session’s membership. - Exchange (daemon): the per-user local process that owns presence, delivery buffering, and the message store. See Exchange and daemon.
- Station: the in-memory registration a session holds while it attends an address. See Stations and presence.
- Lease / epoch: the liveness claim a station holds on an address; the epoch increments when ownership changes.
- Waiter: a one-shot
telex waitclient blocked for a single delivery. - Attention: the urgency of a message:
interrupt,next-checkpoint,background, orfyi. See Attention levels. - Disposition: the recorded outcome of a message: acknowledged, handled, deferred, rejected, closed, or escalated. See Disposition.
- Ack: the explicit durable mark that a delivered message was consumed from the delivery buffer, per recipient.
- Backend: the configured store (local SQLite or networked Postgres). See Backends.
- Thread: a message and its replies, linked by
thread_idandparent_id.
Next: Set up an agent (pull)
Set up an agent (pull)
This guide covers pointing an agent session at telex in a generic harness (scripts, CI, or any harness without an in-session extension). Copilot CLI uses push delivery instead; see Copilot CLI push delivery.
Agents read the full, version-matched pull workflow from the binary with
telex skill. This guide is the operator’s view of how the pieces fit together.
1. Give the session a stable identity
Generic telex commands need a stable session id on each invocation, from
--session or $TELEX_SESSION_ID. Telex fails closed rather than guessing.
export TELEX_SESSION_ID=<stable-session-id> # PowerShell: $env:TELEX_SESSION_ID = "<stable-session-id>"
2. Attach once
telex attach --address <addr> --description "<what this session is doing>"
attach registers the session and address and exits. Add --scope and --tags
so other sessions can resolve this one.
3. Wait for one message, then re-arm
wait is one-shot: it blocks for a single delivery and exits. Drive the loop from
the agent’s own turn cycle, one wait per delivery. Write the result to files with
--out-dir so a detached waiter’s result is readable after it exits:
telex wait --address <addr> --session <id> --out-dir <dir>
On exit the waiter writes into <dir>: message.json and delivery.json (on
exit 0), status.json (always), and exit.code (written last, as the completion
marker). Read exit.code first; see Exit codes.
Do not wrap wait in an infinite shell loop. Many harnesses surface output only
when a command completes, so an internal loop hides delivered messages. Arm one
wait, handle its completion, then arm the next.
4. Ack and disposition
After reading the delivered JSON, ack it (transport consumption), then record the workflow disposition:
telex ack --address <addr> --session <id> --id <message-id>
telex handle --address <addr> --session <id> --id <message-id> --note "completed"
Dedupe by message id: delivery is at-least-once.
5. Stop the station when done
telex station stop --address <addr>
This releases membership durably and waits for tracked waiters to exit. Later messages stay queued until a future attach or wait.
Copilot push delivery
GitHub Copilot CLI and Copilot App can receive messages as turns through a live Telex bridge, without running or re-arming a waiter. On hosts with startup extension discovery, first-time App setup can load the bridge after provisioning and a session restart, even without a live reload tool. Verify liveness before treating push as attended; the existing Telex-owned pull fallback remains available. The full, version-matched Copilot workflow is printed by the installed binary:
telex copilot skill
That command is the source of truth for the Copilot path. This guide is the operator’s overview.
For a root coordinating local sessions, group stall detection adds an opt-in Watcher watch, planned-wait membership and a confirm-then-ring response when progress stops.
Choose delivery by host capability
- Already-loaded, live bridge: keep push, including in Copilot App.
- Supported live bridge loader available: provision and load the bridge below.
On Copilot CLI only, enable Copilot Extensions under
/experimentalif needed to exposeextensions_reload. - App startup discovery without a live loader: provision the bridge, restart
the App session, then run
copilot resumeand verify liveness as described below. - Restart is unacceptable or liveness does not verify: use
pull fallback. Neither an absent
extensions_reloadtool nor CLI-only/experimentalis an App requirement.
Bridge files and push registration do not prove the bridge loaded. A restart is not proof by itself. Check bridge liveness and actual delivery separately; a plain conversation clear is not an established first-time App loader.
Install the plugin
copilot plugin marketplace add lossyrob/telex
copilot plugin install telex@telex
The plugin contributes session lifecycle hooks and provisions the push bridge. It
maps $COPILOT_AGENT_SESSION_ID to the generic telex session id. In bridge mode,
the extension heartbeat, not $COPILOT_LOADER_PID, is the push liveness signal.
GitHub Copilot App emits sessionEnd(reason=complete) when each turn becomes idle;
while the bridge heartbeat remains live, Telex keeps the station attended instead
of requiring copilot resume after every response. Telex also binds attendance to
the bridge host PID, which survives extension reloads but exits with a true App
exit or one-shot CLI completion.
Bind and provision the bridge
Choose an already-loaded, live bridge, a supported live loader, or startup discovery with an acceptable App session restart. Provision with:
telex --address <addr> copilot attach --copilot-bridge --description "<work>"
Keep an already-loaded, live bridge. If the host exposes a supported live loader,
use it after provisioning (extensions_reload where available; Telex cannot
invoke it).
For first-time App setup without an advertised extensions_reload, use
startup discovery when a restart is acceptable: after provisioning, restart the
App session, then run:
telex --address <addr> copilot resume --description "<work>"
telex --address <addr> --json status
copilot resume re-provisions the push registration, re-arms daemon attendance
and rescans unacked backlog; it does not load the extension itself. Telex
preserves session-scoped bridge files across normal close/resume, so hosts with
startup extension discovery can also load those files on resume.
After either loading path, verify --json status reports
station_health: attended_push and push_registered: true for the address.
Where available, call telex_bridge_info for supporting bridge evidence; do not
require that tool when it is absent. Restart, provisioning, registration and tool
availability alone are not end-to-end delivery proof. Delivered messages arrive
as turns labelled [telex] from <addr> (<attention>).
Issue #230 records startup loading and a pushed message on one Windows App build. It does not establish support in every App build or loading after a plain conversation clear. Liveness verification, not an App version, is the gate.
For recovery on a loader-capable host, re-provision with
telex --address <addr> copilot resume --description "<work>" and use its supported
live loader, or use the attach/restart/resume path above. If a restart is
unacceptable or liveness does not verify, use pull fallback or detach
with telex --address <addr> copilot detach. Do not repeat unavailable-tool calls.
Receive and disposition
A pushed turn includes the ack and handle commands with the address, id, and
session filled in. The generic verbs do not read Copilot env vars, so they need
--session "$COPILOT_AGENT_SESSION_ID":
telex ack --address <addr> --id <message-id> --session "$COPILOT_AGENT_SESSION_ID"
telex handle --address <addr> --id <message-id> --session "$COPILOT_AGENT_SESSION_ID" --note "completed"
Sending is not push: telex send and telex reply also need --session.
CC observers
A session that should receive CC (observer) copies as turns opts in at bind time
with --wake-on-cc:
telex --address <addr> copilot attach --copilot-bridge --wake-on-cc --description "<work>"
Use the loading path and liveness check above if needed. Without --wake-on-cc,
CC copies are still buffered and visible in telex inbox --all, but are not delivered as turns.
(In Copilot pull fallback use prepare --wake-on-cc; do not start a waiter
alongside push.)
Tear down
telex --address <addr> copilot detach
This detaches the address and, when it was the last binding, removes the bridge files so nothing reloads on a later resume. Ordinary session end preserves them for startup extension discovery on resume.
Inspect stale bridge files left by other sessions with telex copilot gc --dry-run.
Fallback
When a live bridge is unavailable and a restart is unacceptable or liveness does not verify, use this path for first-time setup or recovery. Surface that push is unavailable and prepare one Telex-owned pull fallback run:
telex --address <addr> copilot fallback prepare --description "<work>"
The JSON result contains a unique run_dir plus launcher.program,
launcher.args, and a ready-to-run launcher.command. Run that launcher as one
fully detached Copilot task (detach: true). Unix uses the current telex binary directly; Windows
uses a generated PowerShell file for the detached-task compatibility path. Telex
does not detach the task itself and does not run an internal delivery loop. If
detached launch is unavailable or refused, report that limitation; do not write
a polling loop or wrapper.
Preparation is idempotent until the run writes exit.code, and it leaves push
unchanged if the launcher never starts. The running launcher atomically clears
push before entering exactly one pull-mode wait. On completion,
read exit.code first, then the exact run’s delivery.json/message.json; ack
and dedupe primary deliveries by message id before preparing the next run.
The launcher refuses to leave a live bridge unless prepare --force records an
intentional downgrade; --force is not a recovery default.
The task completion notification is only a wakeup, not the result. exit.code is
written last: 0 means read the delivery, dedupe, ack and disposition by id;
1 means inspect status.json.detail for setup/error; 2 is idle timeout;
3/4 mean daemon gone/hung; 5 means presence ended. Repair errors before
preparing another run. For 5, inspect status: do not re-arm pull if mode is
push, and leave a stopped station stopped unless attendance is still required.
If exit.code is absent, inspect for a live waiter before retrying the same
unfinished run. Re-arm explicitly, one run at a time.
6 (delivery-quarantined) means read and preserve status.json.quarantine,
not message.json/delivery.json; do not ack the undelivered message. If
may_continue is true and attendance is still required, explicitly call
fallback prepare for the next single-shot run so later deliveries can progress.
Otherwise report the quarantine condition before continuing.
7 (backend-unavailable) means inspect and report status.json.detail for
exhausted backend recovery, not delivery, idle timeout or daemon death.
Re-arm only after recovery, using fallback prepare. Older peers may report
this failure as exit 1; the exit-code reference
describes recovery-capability compatibility.
Preparation is not evidence of a running wait or a real App completion wake. Backend or wait-setup errors remain failures; guidance and source-contract tests prove neither fallback wakeup nor bridge loading.
telex --address <addr> status reports delivery_mode separately from
station_health: push is bridge delivery, pull is the Copilot fallback, and
conflict is a version-skew/race tripwire. The daemon rejects simultaneous push
and pull coverage. A busy App turn can also report push_delivery: deferred;
this means the bridge is reachable and will retry at turn-idle, not that the
station is unattended.
To return to push, first choose an already-loaded, live bridge, a supported live loader, or startup discovery with an acceptable App session restart. Stop the waiter before binding:
telex --address <addr> station stop --session "$COPILOT_AGENT_SESSION_ID"
telex --address <addr> copilot attach --copilot-bridge --description "<work>"
Use the supported live loader if needed, or restart the App session and run
copilot resume for startup discovery. Then verify bridge liveness with
--json status as above. If liveness does not verify, return to the prepared
pull fallback; do not claim push is attended. To end fallback, run station stop
and do not prepare another run. The version-matched telex copilot skill contains
the full artifact, timeout, recovery, and re-arm procedure.
Compatibility
telex copilot skill prints the installed version and source build identifier, the
bridge protocol version, and the minimum compatible plugin version, and warns if the
plugin is older than the binary supports. telex --version includes the same build
identifier, while telex --json version exposes it as version.build_id. Published
release binaries are gated to report the release commit; source builds without Git
metadata may report unknown. Git fallback is accepted only for a standalone Telex
checkout, not an unrelated ancestor repository, and the value is diagnostic rather
than an attestation.
If the drain hook reports skew, inspect version.current_exe plus Get-Command telex
or command -v telex, reinstall the plugin and binary from the same release, and make
the versioned launcher precede stale shims on PATH before restarting Copilot. A binary
rollback must be paired with the plugin from the same release.
Release install scripts pin the plugin and binary to the same tag. The plugin shape is validated against a specific Copilot CLI version; see the acceptance matrix.
Install and launch Watcher
Watcher is a separate per-user process with Windows x86_64 initial support. Linux has automated SQLite/PostgreSQL runtime coverage, not a clean-host production-soak claim; macOS fails closed before detector launch. This procedure creates a discoverable installation record, then starts the ordinary runtime. It requires PowerShell 7.4 or newer on either platform. It does not install a service or restart supervisor. macOS bootstrap remains unsupported.
An agent reads an operator-supplied absolute record path, or otherwise exactly
<config root>/watcher/installation.json. The config root is TELEX_HOME when
set, otherwise ~/.telex, following Telex’s own home rule. There is one default
record per config root; additional installations use explicit paths. Do not
search PATH or other directories for a substitute. If the record is missing,
unreadable, invalid or unsupported, stop and report that bootstrap is needed.
Use the installation tool
from a trusted checkout matching the Watcher build for initial create. It writes
a new record and an owner-only installation.ps1 copy beside it, both exclusively,
and records the copy’s SHA-256. Agents use that companion tool, not a checkout
path supplied by the operator. Its trust basis is the same private Telex config
root that already holds profile configuration and credential commands.
verify checks authority, tool hash, paths, selected Telex build, Watcher hash,
backend and authentication features without requiring credential-variable values.
preflight additionally checks the launch environment. No action starts
the runtime, connects to the backend, changes a profile or upgrades a binary.
The tool prints the record path on stderr and the JSON record on stdout.
Prepare binaries and the backend
Choose and record the exact trusted binaries before running bootstrap:
- Install or upgrade Telex through the existing release installation and
upgrade surface. Record the install root and
the exact selected versioned executable from
telex --json version --root <root>(version.install.current_binary), not the mutablebinlauncher.copilot stall-detect --helpmust succeed for session-group monitoring. - Obtain Watcher from the Windows x86_64 release archive, or build it
from a trusted pinned checkout, and retain its executable
outside
targetso subsequent builds cannot replace a running binary. Watcher is included in that Windows archive, but not installed by the single-binary Telex installer. ARM/macOS archives omit Watcher. Record the source/archive commit and build command with your operational evidence. - On Linux, also retain the executable
watcher_process_guardfrom that same build in the same directory as the retained Watcher. This existing containment helper is required to execute detectors. Bootstrap refuses a missing/non-executable helper and pins its hash. Do not copy onlytelex-watcher. - Choose an existing named backend profile holding the members’ addresses. Use absolute paths in profile configuration. Bootstrap does not invent a default profile or initialize/migrate a backend.
- Choose a new, distinct sender address for Watcher’s send-only responsibility, never a member’s receive address. Watcher attaches it as send-only when a watch is registered; an empty runtime does not establish sender/backend readiness.
A standalone copy of Watcher starts and attaches but cannot run detectors on Linux.
For the existing Entra authentication path, both packages need entra:
cargo build --locked --release -p telex -p telex-watcher --features telex/entra,telex-watcher/entra
For SQLite or ordinary password/credential-command Postgres profiles, the
default package features suffice. telex-watcher --version reports its compiled
backends and Entra support. Bootstrap refuses an older Watcher without this
feature report or either binary missing the profile’s required feature.
Installing a local Telex build still uses telex upgrade --from <binary>;
building alone does not populate the installed-current layout.
Keep the existing authentication method: password environment reference,
one-shot credential command, or Entra (cli, auto, or managed). See
Postgres authentication. An Entra CLI profile needs the existing
Azure CLI login and credential environment in the runtime’s user context.
Bootstrap grants no access and creates no credentials.
Create the installation record
The operator approves these values. Paths below are placeholders, not defaults to infer. Prepare an existing owner-controlled config root and a new private runtime directory for the registry and logs. Keep the registry distinct from any existing registry. The installation tool creates only the final record directory if absent, with owner-only permissions; it never repairs permissions on an existing directory. On Windows, private authority allows the current user, SYSTEM and local Administrators; on Linux it requires the current uid and no group/other access. Symlinks/reparse points and foreign-mutable authority ancestors are rejected; existing permissions are never repaired. Linux sticky temporary ancestors are allowed, as in the existing install-authority policy.
In PowerShell 7 on Windows or Linux, use native absolute paths:
$ErrorActionPreference = 'Stop'
$tool = '<absolute trusted checkout>/telex-watcher/installation.ps1'
$env:TELEX_HOME = '<absolute existing config root>'
# If this profile uses an explicit configuration file, retain that existing value:
# $env:TELEX_CONFIG = '<absolute existing config.toml>'
$telex = '<absolute selected versioned telex executable>'
$watcher = '<absolute retained telex-watcher executable>'
$installRoot = '<absolute trusted Telex install root>'
$registry = '<absolute NEW runtime directory>/registry.sqlite'
$backendProfile = '<existing named backend profile>'
$sender = '<new distinct Watcher send-only address>'
$environmentNames = @('TELEX_HOME')
if (Test-Path Env:TELEX_CONFIG) { $environmentNames += 'TELEX_CONFIG' }
if ($IsWindows) { $environmentNames += 'LOCALAPPDATA' }
# Add the profile's password variable and any existing credential configuration
# names needed by its command/Entra provider. Never put credential VALUES here.
# For example, add AZURE_CONFIG_DIR if the existing Azure CLI setup uses it.
& $tool -Action create -TelexExecutable $telex -WatcherExecutable $watcher `
-WatcherRegistry $registry -InstallRoot $installRoot `
-BackendProfile $backendProfile -Sender $sender `
-EnvironmentNames $environmentNames
By default this writes $env:TELEX_HOME/watcher/installation.json. To use another
location, pass -RecordPath '<absolute record path>' and give that exact path to
the root agent. Its parent must exist; the final record directory may be new.
A pre-existing record, companion tool or registry is a refusal, not permission to
overwrite. Each record needs its own private directory. If a late record-write
failure leaves the newly copied tool, retain it as evidence and resolve the
reported failure explicitly; bootstrap will not overwrite that copy on retry.
There is no --force path. If preparation fails, repair its reported condition;
do not start the runtime or quietly substitute paths.
Record schema v1
The record contains only these fields:
| Field | Meaning |
|---|---|
schemaVersion | 1; unknown versions are refused. |
configRoot | Effective absolute Telex home. |
telexExecutable | Exact trusted selected versioned Telex path. |
watcherExecutable | Trusted retained Watcher path. |
watcherRegistry | Absolute registry for this installation. |
installRoot | Trusted installed-current Telex root used by the Application Client. |
backendProfile | Explicit existing profile name. |
sender | Distinct send-only address, not a secret. |
pollIntervalSeconds | Default watch cadence, 1-86400; default 30. |
environmentNames | Required launch/detector environment names, never values. |
telexBuildId | Selected Telex’s reported build identity. |
watcherSha256 | Hash of the retained Watcher binary. |
verificationToolSha256 | SHA-256 of the owner-only installation.ps1 beside this record. |
processGuardSha256 | Linux: SHA-256 of the adjacent watcher_process_guard executable; Windows: null (uses native Job Objects). |
The record is configuration, not a live-process claim or credential store. It does not contain tokens, connection strings, password commands or environment values. Restore any required environment from the existing approved profile and credential setup; do not save secrets in a launch log. Verify refuses an altered Watcher, companion tool, Linux process guard or changed Telex selector instead of silently adopting it.
Discover and verify
Use the current Telex config root and existing profile configuration. If used,
TELEX_HOME, TELEX_CONFIG and LOCALAPPDATA must resolve consistently with
that installation. Verification checks environment-name syntax and profile
references but does not require credential values in the discovery shell.
Read the installation through verify, never just Get-Content followed by
execution of unverified record paths. The default needs no operator-supplied
location; only replace $recordPath for an explicit operator path:
$ErrorActionPreference = 'Stop'
$configRoot = if (Test-Path Env:TELEX_HOME) { $env:TELEX_HOME } else { Join-Path $HOME '.telex' }
$recordPath = Join-Path $configRoot 'watcher/installation.json'
$tool = Join-Path (Split-Path -Parent $recordPath) 'installation.ps1'
if (-not (Test-Path -LiteralPath $recordPath -PathType Leaf) -or
-not (Test-Path -LiteralPath $tool -PathType Leaf)) {
throw 'Watcher installation is missing; follow this bootstrap guide.'
}
$installation = (& $tool -Action verify -RecordPath $recordPath) |
ConvertFrom-Json -AsHashtable
$telex = $installation.telexExecutable
$watcher = $installation.watcherExecutable
$registry = $installation.watcherRegistry
$env:TELEX_HOME = $installation.configRoot
$env:TELEX_INSTALL_ROOT = $installation.installRoot
$backendProfile = $installation.backendProfile
$sender = $installation.sender
$pollSeconds = $installation.pollIntervalSeconds
Preflight and start
In the runtime’s launch shell, restore all environmentNames from the existing
approved profile and credential setup. Do not copy secret values into an agent
discovery shell, the record or a log. Do not carry a temporary test TELEX_DB
into a real install; Watcher and the stall detector use the explicit profile,
not ambient database overrides. Use the same discovered tool and record:
& $tool -Action preflight -RecordPath $recordPath | Out-Null
Preflight repeats verification and rejects missing launch variables. It does not connect to the backend or start a process; it must succeed in the environment actually inherited by the detached launch.
Check that status reports runtime-not-running before launching. A responsive
existing owner is not an invitation to start a replacement. A lost management
reply is not proof the owner is absent; inspect before retrying.
Launch the ordinary process outside the agent’s lifetime with the host’s
detached-process facility. For a Copilot task runner, launch this command once
with detach: true, preserving the environment above and recording the returned
task handle and separate stdout/stderr paths in the launch evidence:
& $watcher --registry $registry --install-root $installation.installRoot --json run
Telex does not detach this process for you. If the host refuses detached launch,
stop and report that limitation rather than leaving an attached shell that dies
with the agent. An operator can use Start-Process on Windows as described in
runtime administration,
or a persistent user terminal on Linux:
/absolute/telex-watcher --registry /absolute/registry.sqlite --install-root /absolute/telex-install --json run
For an unattended Linux shell launch, use the existing OS nohup facility, with
the same environment and explicit paths, and capture its PID and logs:
nohup /absolute/telex-watcher --registry /absolute/registry.sqlite --install-root /absolute/telex-install --json run \
</dev/null >/absolute/runtime.stdout.log 2>/absolute/runtime.stderr.log &
watcher_pid=$!
Neither launch is a restart supervisor. Keep the record, source/build identity, launch timestamp, task handle/PID, log paths and returned runtime ID together in your operational notes. A PID alone is not durable identity.
Establish readiness
Use the record’s same binary and registry:
& $watcher --registry $registry --json status
if ($LASTEXITCODE -ne 0) { throw 'Runtime is not ready; inspect its retained startup logs.' }
& $watcher --registry $registry --json list --active
if ($LASTEXITCODE -ne 0) { throw 'Runtime administration failed.' }
An initial endpoint race may report runtime-not-running; retry a bounded
read-only status check, inspecting the launched task for exit. Require the
expected runtime ID, current heartbeat and productionReady: true. Do not
register on a failed startup or claim that process creation proves readiness.
runtime-failed requires inspection of the retained stderr/logs; older builds
can omit the underlying cause. Preserve that failure instead of guessing.
Then register an ordinary v2 watch, for example the
session-group stall watch. Copy environmentNames into its
environmentAllowlist; these variables must exist in the runtime’s launch
environment, not just the registration shell. On Windows, LOCALAPPDATA is
needed by detectors that use Telex’s local daemon rendezvous; custom
TELEX_CONFIG and credential configuration must also be allowlisted when used.
Inspect the registered watch’s status, sender readiness, last successful attempt
and any pending operation. Empty-runtime readiness proves none of these.
Root delivery is independent: use either a live Copilot bridge for push, or the
documented copilot fallback prepare single-shot pull path when the host cannot
load a bridge. Preparing fallback is not delivery proof; a completed wait needs
inspection, processing and explicit re-arm. Keep the modes and their evidence
distinct. See Copilot delivery.
Shutdown, rollback and existing installations
Stop exactly this registry owner with its recorded binary:
& $watcher --registry $registry --json shutdown
if ($LASTEXITCODE -ne 0) { throw 'Shutdown unconfirmed; inspect the owner before retrying.' }
Wait for the recorded task/process to exit, then confirm status returns
runtime-not-running. Never kill a process by name or reuse an old PID without
checking identity. Shutdown detaches known senders; it does not retire watches,
erase pending receipts, stop the user’s Telex daemon or tear down root attendance.
The root owns watch retirement; each receiving participant owns its attendance.
For a failed new installation, stop its exact owner, retain registry, receipts and logs, and keep or remove only the new record and its companion tool once no root uses them. Do not delete history or reset detector state to obtain a green result.
For an existing or older installation, first inventory its selected/previous Telex version, Watcher path/hash, registry, profile, senders, pending operations, runtime identity and launch environment. If no record exists, those are operator inputs; do not adopt the installation by guessing. Preserve its records and registry. Initial bootstrap deliberately refuses an existing registry.
For an independently provisioned candidate, create an explicit new record path, a retained candidate Watcher (and its same-build Linux guard) and a new registry. Stop the old owner only with explicit operator approval. Decide how permanent watch IDs and opaque state move before registering on the candidate; never copy a live registry or discard unresolved sends. This procedure does not infer a registry/state migration. A new empty registry does not preserve an old watch’s recurrence history.
At upgrade or rollback, retain, copy, verify and swap Watcher and its Linux guard as one same-build pair in a separate directory. Preserve the previous pair for reversal; never leave a new Watcher beside an old guard or replace a running pair.
For a forward upgrade that preserves the existing registry, stop its exact owner and preserve an owner-only compatible registry snapshot, the old executable(s), installation record and logs before first candidate startup. Start the new binary against that same registry only after the approved cutover; verify the retained watch IDs, state/sequence marks, receipts and pending operation identities. This build migrates Watcher registry metadata from 2 to 3, independently of Telex backend schema and detector schema v2. Earlier binaries refuse schema 3, including marked backups and restored fenced registries. A binary swap alone is not rollback. Restoring any older registry may lose post-snapshot detector state and reuse event sequences: inspect post-backup sends and receiver dedupe history before individually resuming watches. Use the backup/restore and recovery runbook; never remove version checks, copy a live SQLite main file without its journal state, or silently start an empty replacement registry.
Telex upgrade/rollback stays on the existing binary surface, with approval and backend backup where required. An older CLI may be the required invoker for a legacy installation’s upgrade; inspect its output and verify the final selected binary afterwards. Never bypass manifest admission or ordered daemon replacement. Rollback does not downgrade the backend schema: notably v0.1.2 cannot read a schema-3 store. Retaining an old executable does not make its rollback safe. The operating guide documents those compatibility limits.
After an approved selector change, retain the old record as evidence and create a reviewed candidate record for the new exact executable; verification of the old record must fail while its selector differs. Only after explicit cutover, with the old owner stopped and its evidence preserved, may the operator move the old default record and companion tool aside and create a new default. Bootstrap never overwrites either file.
Isolated validation
Any test or field proof uses unique absolute TELEX_HOME, TELEX_DB,
TELEX_INSTALL_ROOT and a temporary LOCALAPPDATA, and absolute branch binaries.
On Linux, also use the private short test-home setup in CONTRIBUTING.md for
management endpoint tests. Use a dedicated profile with an absolute fixture
database path. Do not touch the operator’s installed binaries, daemon or real
directory permissions. Teardown stops only the owners created by the test and
removes only its named disposable paths. Local evidence supports, but does not
replace, Windows/Linux CI or real root-turn delivery evidence.
Watch a Copilot session group for stalls
A local group can stop progressing when all its active members are idle and
nothing will wake them. Register one Watcher v2 watch to notify the group’s root,
then let the root confirm through the App and ring the affected members.
The detector is shipped as telex copilot stall-detect; agents supply group
membership, a root address and a grace period, rather than writing a detector.
This works only for same-machine, same-user Copilot CLI sessions. The classifier reads bounded local log tails conservatively; its result is not authoritative App state. Cloud and remote sessions are outside this group. The installed binary prints the version-matched instructions:
telex copilot skill
telex --json copilot session-state --session <local-session-id>
The classifier returns busy, idle, waiting-on-operator, not-loaded or
unknown, with a reason and optional activity marker. Unknown is never idle.
The session id is the local CLI id (COPILOT_AGENT_SESSION_ID in that member),
not an App session/project id or a path. Keep a separate mapping to the App ids
the root uses for confirmation and wakeup.
Create the group
The root creates an absolute, private directory and one
<local-session-id>.json file for each member it launches:
{
"schemaVersion": 1,
"sessionId": "local-session-id",
"address": "project:example/worker:one",
"state": "active",
"reason": "Working on the assigned item"
}
The root owns membership creation/removal, session ids and distinct Telex
addresses. Members update only their own state and optional reason. Preserve
the other fields, write a temporary non-.json file beside the record, and
atomically replace the record. Do not rewrite a shared group file or truncate a
member file in place.
Records are strict JSON with no duplicate or extra fields. The filename must
match sessionId, which is 1-128 ASCII letters, digits, hyphens or underscores.
The detector accepts at most 32 members, 4096 bytes per record, and 64 total
directory entries including temporary files. Address/reason values are at most
256 UTF-8 bytes each. Addresses must be distinct and contain no whitespace or
control characters. Membership entries must be regular files, not symlinks.
| State | Meaning |
|---|---|
active | Expected to make progress; only active members count toward a stall. |
parked | Waiting on an identified wakeup: a watch, App automation, background shell or background agent. Put the wakeup in reason. |
operator-held | Waiting for an operator decision or gate. Identify it in reason. |
done | Assigned work is complete. The root removes the file when the member leaves. |
Before ending a turn to wait for a background shell or agent, the member sets
itself parked. On wakeup to resume work it sets itself active. If rung while
legitimately waiting, it replies with what it is waiting for and stays parked.
The detector reports backgroundWork: "unobserved" for every member: neither
the classifier nor App idleness proves background work is absent.
For example, a member can park itself with PowerShell 7:
$ErrorActionPreference = 'Stop'
$memberFile = Join-Path $groupDirectory "$env:COPILOT_AGENT_SESSION_ID.json"
$member = Get-Content -LiteralPath $memberFile -Raw | ConvertFrom-Json -AsHashtable
$member.state = 'parked'
$member.reason = 'Waiting for the background build completion notification'
$temporary = Join-Path $groupDirectory "$([guid]::NewGuid()).tmp"
$member | ConvertTo-Json -Compress |
Set-Content -LiteralPath $temporary -Encoding utf8NoBOM -NoNewline
[System.IO.File]::Move($temporary, $memberFile, $true)
Use the root-provided $groupDirectory. To resume, repeat the replacement with
state = 'active' and a current reason. A failed write/rename needs repair before
the member assumes its state changed; remove only its own abandoned temporary
file. The root may also be an active member. Empty groups and groups with no
active members are evaluated as non-stalled.
Use the root’s existing Watcher installation
Start with the verified record from Watcher installation and launch.
The Watcher runtime must be running. The root’s Telex address receives turns
through either a live Copilot bridge push path or the documented
pull fallback (telex copilot fallback prepare) when the host cannot load
the bridge. Follow Copilot delivery for that mode; push has
no waiter, while each pull wait completes once and needs explicit re-arm.
Fallback preparation alone is not proof of root delivery.
Read an explicit operator-supplied absolute record path, otherwise exactly
<config root>/watcher/installation.json, where the config root is TELEX_HOME
when set, otherwise ~/.telex. Verify with installation.ps1 beside the record;
bootstrap installs that owner-only, hash-recorded copy, so no checkout path is
needed. Discovery does not require credential values in the root’s shell.
If it is missing, unreadable, invalid or unsupported, stop and point to the
bootstrap; do not search elsewhere or guess installation values.
| Value | Source |
|---|---|
telex executable | Absolute trusted installed binary path in the installation record. |
watcherExecutable | Absolute trusted telex-watcher path used to run this Watcher. |
watcherRegistry | Absolute registry path used by this runtime and by the registration command below. |
backendProfile | Existing configured profile holding the members’ Telex addresses; use it for both the registration and detector parameter. |
| Sender | Record’s sender, distinct from member receive addresses; Watcher attaches it send-only on registration. |
pollIntervalSeconds | Record’s cadence, copied through one $pollSeconds value into both registration and detector. The default is 30 seconds. |
If bootstrap needs an operator input, ask through ask_user rather than guessing
paths, credentials or a default profile. Profile paths should be
absolute. The detector uses its explicit backend profile and ignores TELEX_DB
or global database overrides. All members belong to this one backend and one
Watcher registry; another registry’s watches are not observed.
Register the watch
Choose the group directory, root address and grace for this group. Use a new
permanent lowercase ASCII watch id ([a-z0-9][a-z0-9._-]{0,127}); removed ids
cannot be reused. This PowerShell 7.4+ template writes registration data, not a
wrapper or detector script. Read installation values through the verified record.
The record’s cadence is shared by the watch and detector; the
30-second timeout allows the detector’s 15-second observation budget plus startup.
$ErrorActionPreference = 'Stop'
$configRoot = if (Test-Path Env:TELEX_HOME) { $env:TELEX_HOME } else { Join-Path $HOME '.telex' }
$recordPath = Join-Path $configRoot 'watcher/installation.json'
# Replace recordPath only when the operator supplies an explicit absolute path.
$installationTool = Join-Path (Split-Path -Parent $recordPath) 'installation.ps1'
if (-not (Test-Path -LiteralPath $recordPath -PathType Leaf) -or
-not (Test-Path -LiteralPath $installationTool -PathType Leaf)) {
throw 'Watcher installation is missing; follow the Watcher installation bootstrap guide.'
}
$installation = (& $installationTool -Action verify -RecordPath $recordPath) | ConvertFrom-Json -AsHashtable
$groupDirectory = '<absolute group directory>'
$rootAddress = '<root Telex address>'
$graceSeconds = 300
$telex = $installation.telexExecutable
$watcher = $installation.watcherExecutable
$registry = $installation.watcherRegistry
$backendProfile = $installation.backendProfile
$sender = $installation.sender
$watchId = '<new permanent watch id>'
$pollSeconds = $installation.pollIntervalSeconds
$registrationFile = Join-Path $groupDirectory 'stall-watch-registration.txt'
$registration = @{
schemaVersion = 2
id = $watchId
command = @($telex, 'copilot', 'stall-detect')
intervalSeconds = $pollSeconds
timeoutSeconds = 30
backendProfile = $backendProfile
sender = $sender
target = $rootAddress
environmentAllowlist = @($installation.environmentNames)
parameters = @{
groupDirectory = $groupDirectory
backendProfile = $backendProfile
watcherExecutable = $watcher
watcherRegistry = $registry
graceSeconds = $graceSeconds
pollIntervalSeconds = $pollSeconds
}
}
$registration | ConvertTo-Json -Depth 4 |
Set-Content -LiteralPath $registrationFile -Encoding utf8NoBOM
& $watcher --registry $registry --json register $registrationFile
if ($LASTEXITCODE -ne 0) { throw 'Watch registration failed; inspect the error before retrying.' }
& $watcher --registry $registry --json show $watchId
graceSeconds is required, from 1 to 86400. pollIntervalSeconds is also bounded
to 1-86400 and must match the watch interval. The .txt suffix keeps the
registration out of the membership scan. Omitted workingDirectory captures the
registration command’s current absolute directory.
Watcher controls the detector’s environment. Allowlist TELEX_HOME and, if used,
TELEX_CONFIG and the profile’s credential environment names. These values must
exist in the runtime’s launch environment, not merely the registration shell.
Keep secret values out of parameters. The detector reads one Watcher v2 request
on stdin and writes one v2 result; it has no group/root/grace command-line flags.
On other shells, write the same v2 JSON fields and register the file through the
same management command; no detector script is needed.
Check monitoring and show status
Use the same trusted Watcher binary and registry:
& $watcher --registry $registry --json status $watchId
& $watcher --registry $registry --json health $watchId
& $watcher --registry $registry --json attempts $watchId --limit 5
& $watcher --registry $registry --json events $watchId --limit 5
& $watcher --registry $registry --json list --active
A registered watch does not prove successful observations. Inspect last attempt,
last success, health, diagnostic category and any pending operation. A stalled
group’s unchanged known-idle/not-loaded snapshot must persist for strictly more
than grace. Busy members and members waiting on ask_user prevent a stall.
Unacknowledged/deferred Telex messages and active watches targeting a member are
wake sources. Exactly this detector’s own watch id is excluded. Other watches
count even when their source depends on another idle member, so circular waits
can hide a stall.
The root’s status view uses these same observations: group/watch id, membership state/reason, classifier state/reason/activity time, message counts, targeting watch ids, background uncertainty, and the last observation time/health. Pair them with fresh App status when acting. Event snapshots can be stale; absence of an event does not mean everyone is busy or monitoring is healthy. A view belongs to the root, not a new always-running agent or service.
Confirm, then ring
Both event kinds deliver a JSON snapshot in the Telex message body. Each member
contains member, optional session, optional wakeSources, and
backgroundWork; inactive members can have null observations. Verify the
registered sender, expected store and metadata.watcher.watchId, then ack and
dedupe by message id using the normal receive procedure.
Call the App’s get_sessions_status before acting on either kind. Use the
root’s local-id-to-App-id mapping; a local CLI id is not an App id. Check current
activity, questions, planned waits and completed work.
copilot.group.stalled: diagnose the confirmed lack of progress, then ring the affected members with a specific next action or a request for their wait reason. Use the App’ssend_session_messageto resume a not-loaded member. For an attended Telex member,sendis also suitable; include a meaningful--subjectand the root’s explicit--session.copilot.group.attention-needed: inspect the named unknown members and their classifier reasons after App confirmation. Ask what stopped progress or what they are waiting for. This snapshot hasdegraded: trueand does not establish a stall. Quiet unknowns with old evidence can trigger it, including unsupported error/start events. Unknowns without usable timestamps may stay degraded without an advisory.
A member rung during a legitimate wait replies with its wait reason and stays
parked. A member resuming work sets itself active. If the App shows progress or a
known gate, record that diagnosis rather than restarting work. Finish with the
appropriate terminal disposition (handle, reject or close); ack alone is
not workflow completion.
Both kinds share receipt-gated episode deduplication: one event per unchanged episode. Activity or membership changes can produce a new episode; transport redelivery still requires message-id deduplication. Do not reset detector state just to obtain another reminder. Observation gaps longer than twice the poll interval can restart grace once per unchanged episode.
Recover degraded monitoring without losing history
An observation failure leaves the watch active/degraded with
diagnosticCategory: "detector-degraded". It emits no event and does not advance
detector state.
Inspect attempts and latestFailure for the bounded, redacted
stderr tail: at most 4096 UTF-8 bytes including [truncated]. Unavailable or
historical capture is null; captured empty stderr is "". Redaction covers
configured sensitive environment values, not arbitrary provider text or
unconfigured credentials; review excerpts before sharing.
Check membership limits/format, local session ids,
backend/environment access and the running Watcher management endpoint. An
unchanged already-advised unknown episode can also remain degraded.
Inspect each active member’s history using the same configured backend:
& $telex --backend $backendProfile --address '<member address>' --json inbox --all
& $telex --backend $backendProfile export --address '<member address>'
The first command lists the newest 50 messages by default, including consumed messages. Export includes older messages with disposition history. Review outstanding/deferred work, including older messages.
While observations are unavailable, mark monitoring coverage incomplete in the root’s view and use App inspection. Keep the existing routing identity and message history. Do not delete the store/logs or bulk-ack work to make the watch green. A real planned wait may be parked, but do not park or finish active work merely to bypass degradation. A missing event is not a clean bill of health.
Coordinate multiple sessions
This is the full two-session pattern: one session serves an address and waits, the other finds it and sends a message that requires disposition.
Session A: attend and wait
export TELEX_SESSION_ID=session-a # PowerShell: $env:TELEX_SESSION_ID = "session-a"
telex attach --address session:a \
--description "session A waiting for coordination" \
--scope project:telex --tags repo:telex,role:worker
telex wait --address session:a
attach is one-shot; only wait blocks. When A’s wait returns (exit 0) with
the message as JSON, A acks and dedupes by id, arms a fresh wait before longer
processing, then handles and replies:
telex ack --address session:a --id <message-id>
telex wait --address session:a # re-arm before longer work
telex handle --id <message-id> --note "status prepared"
telex reply --to-message <message-id> \
--body "Holder is live; continuing work." --attention next-checkpoint
Session B: find A and send
export TELEX_SESSION_ID=session-b # PowerShell: $env:TELEX_SESSION_ID = "session-b"
telex attach --address session:b --description "session B requesting status" \
--scope project:telex --tags repo:telex,role:requester
telex resolve --match "waiting for coordination" --scope project:telex
telex send --to session:a --subject "Status request" \
--body "Please send your current status." --attention interrupt --requires-disposition
B then waits for A’s reply and closes it out:
telex wait --address session:b
telex ack --address session:b --id <reply-id>
telex handle --id <reply-id> --note "reply received"
The re-arm loop
Drive the loop from the agent’s turn cycle, one wait per delivery:
- Arm one
wait(optionally with--min-attention interruptwhile focused). - It blocks until one message, exits, and the runtime wakes the agent.
- Read the result,
ack, dedupe by id, then arm a freshwaitbefore longer processing.
Do not wrap wait in an infinite shell loop.
Two-phase attention
While actively working, arm a phase-1 waiter with --min-attention interrupt; it
wakes only for urgent messages, while next-checkpoint, background, and fyi
messages stay durably buffered. At a checkpoint, inspect
telex inbox --all --address <addr>, disposition what you are ready to handle,
then continue with an interrupt-only waiter or, if idle, an unfiltered waiter.
Only one live waiter per station is permitted. To switch modes, let the current
waiter complete or run telex station stop, then re-attach and arm the new mode.
Networked Postgres backend
Local SQLite is the default. Use a Postgres backend when sessions on different machines need to share one exchange, or to persist an audit trail centrally.
Add a backend
Configure once with telex backend add. Provide the password by reference, never
embedded in the connection string.
Password from an environment variable
telex backend add staging \
--postgres "postgresql://app@staging-db:5432/telex?sslmode=require" \
--password-env STAGING_PG_PASSWORD --schema telex
Azure Postgres with Entra
Telex fetches the token itself. On a laptop it uses your az login; on a devbox
or VM with a managed identity, use --entra-cred managed.
telex backend add prod \
--postgres "host=myserver.postgres.database.azure.com port=5432 user=me@example.com dbname=postgres sslmode=require" \
--entra --schema telex --default
--entra requires a build with the entra feature; the release binaries include
it. On a build without it, supply the token with --password-command (for
example az account get-access-token ...).
Credential commands are one-shot
Use --password-command for a command that prints a complete UTF-8 password or
token and finishes. Telex trims that output. If --password-env is also set,
the environment reference takes precedence.
Telex owns the invocation and its supported in-scope helpers. It cleans up remaining owned helpers after successful status plus complete output, or on error/cancellation. A shell that exits before a finite helper finishes writing does not cause partial credential output to be returned. Do not use this option to start a persistent refresh daemon or detached/broker-owned background job. Querying an existing credential agent is supported without making that agent a cleanup target. Windows jobs and Unix process groups have different escape limits; Telex does not claim universal descendant containment.
On Windows, completion means checked termination of the private job, zero active job processes, completed launched-shell wait, and completion of Telex’s own I/O, handle release, and worker. It does not promise that every former helper’s process handle is already signaled or all kernel/driver rundown and prior external I/O has finished. The invocation limit below bounds Telex-owned work, not every residual Windows kernel object. Cleanup API failures are still failures, not successful receipts.
Each owning process admits at most two credential invocations, and equal configured command sources do not overlap during cleanup. Waiting for admission spends the original caller budget. Cancellation returns control on that budget while the native owner finishes cleanup independently of the async runtime.
Normal owning-host exit can add up to three seconds for one concurrent cleanup
drain. If cleanup cannot be confirmed, Telex reports a named FAILED_HELD
obligation and holds the host rather than silently leaking ownership or claiming
clean exit. The exceptional hold can exceed three seconds and requires operator
intervention. A waiter client does not drain work owned by its daemon. Cleanup
errors report lifecycle stage/status without echoing the command or helper stderr.
Select a backend
telex --backend staging inbox
telex send --to node:x --body "hi" # uses the default backend
telex backend list
The first backend added becomes the default; change it with --default on add or
telex backend default <name>.
Notes
- The connection string is a libpq URI or a key=value DSN.
- Use
--schemato place telex tables in a dedicated schema. - Secrets are referenced (
--entra,--password-env,--password-command) and are never written to the config file.telex backend show <name>redacts them.
Schema and privileges
Set the schema for telex tables with --schema when adding the backend. The
configured database role needs privileges to create objects in that schema on
first use (or to use an existing one). Pre-create and validate the schema:
telex init --backend <name>
This connects with the configured credentials, creates the schema and tables if they are absent, and surfaces connection or permission errors early.
TLS
Request TLS in the connection string with sslmode=require, or a stricter mode
such as verify-full with the appropriate root certificate configured for your
environment. Azure Postgres requires TLS.
Backup
A telex Postgres backend is an ordinary schema in your database. Back it up with standard Postgres tooling:
pg_dump --schema telex "postgresql://.../telex" > telex-backup.sql
Multiple machines
Point each machine’s telex at the same Postgres backend (same connection string and schema) to share one durable store and audit trail. Each machine still runs its own local exchange daemon; the daemon is per user and local, while the store is the shared Postgres schema.
Operating telex
The daemon lifecycle
The exchange auto-spawns on the first daemon-backed command; there is no manual start. Inspect the resolved backend and address projection with:
telex status --address <addr>
The daemon runs per user. Inspect and control it with the telex daemon family
(run telex daemon --help for the full set):
telex daemon status # daemon internals
telex daemon version # running daemon version
telex daemon stop --drain # drain in-flight work; returns once the daemon process has exited
Runtime state (the IPC socket and lease state) lives in a per-user runtime
directory: on Windows under %LOCALAPPDATA%\telex\run, on Unix a per-user socket
directory. The local message store is the SQLite file at ~/.telex/telex.db.
Stopping a station
station stop is the symmetric inverse of the attach and wait loop. It marks the
station non-attending, releases membership durably, and waits for tracked waiters
to exit:
telex station stop --address <addr>
After it returns, a later message to the address stays queued until a future attach or wait; it is not consumed by an orphaned waiter.
Teardown: which command to use
| Command | Effect |
|---|---|
telex detach --address <addr> | Drop this session’s membership of the address, non-destructively. The station and durable buffer remain. |
telex station stop --address <addr> | Mark the station non-attending, release membership durably, and wait for tracked waiters to exit. |
telex address retire --address <addr> | Retire the address so it drops from directory listings. |
telex daemon stop --drain | Stop the local exchange after draining in-flight work. |
telex copilot detach | Copilot push sessions: detach the address and remove the bridge files. |
None of these delete durable messages; a later attach or wait resumes against the retained buffer.
Upgrading the binary
Release installs use a versioned layout instead of overwriting the binary on
PATH in place. A stable launcher lives under the install root’s bin/, immutable
binaries live under versions/<tag>/, and current selects the version new
invocations use. Old in-flight processes keep running on their version while new
shells use the selected one.
<install-root>/
bin/telex(.exe)
versions/<tag>/telex(.exe)
current
previous
Upgrade, roll back, and inspect versions:
telex version --json
# Fetch, verify, and install the latest compatible public release:
telex upgrade
# Install a specific public release by tag:
telex upgrade --version vX.Y.Z
# Install a local/manual build (no download):
telex upgrade --from <path-to-telex-binary> --version vX.Y.Z
telex rollback
telex gc --dry-run
Without --from, telex upgrade discovers a GitHub release (the latest full release by
default, or --version <tag>), selects this platform’s asset, downloads the archive and its
.sha256 sidecar, and verifies the checksum before installing — then installs through the
same versioned layout as the local path. It is fail-closed: a missing or mismatched
checksum, a missing platform asset, an unsupported platform, an incompatible version, or a
network/rate-limit error aborts without changing current. Set GITHUB_TOKEN to raise the API
rate limit. Prebuilt binaries are published for Windows (x86_64, ARM64), Linux (x86_64), and
macOS (Apple Silicon, Intel); on other platforms install from source with
cargo install --git https://github.com/lossyrob/telex --features entra. If telex is already on
the resolved release it reports “already current” and does nothing. --force
may repair a missing target, but it never overwrites an existing immutable
version directory; publish a new version tag instead.
telex upgrade reads the downloaded (checksum-verified) binary’s own metadata by running it
once (telex --json version) before installing; in locked-down environments an OS quarantine
prompt (macOS Gatekeeper, Windows SmartScreen) on that step is the likely cause if an upgrade
stalls. The checksum verifies integrity, not authenticity — it protects against a corrupted
or truncated download, and the trust root is the GitHub repository the asset comes from.
telex upgrade and telex rollback drain the current local daemon before
switching current, unless --skip-drain is passed. Rollback refuses installed
versions whose manifest is incompatible with this build’s protocol/schema floor.
For the schema-3 upgrade, v0.1.2’s retained executable is not a downgrade path:
the new binary rejects its schema-2 manifest, and v0.1.2 itself refuses the
migrated store. There is no automatic reverse schema migration.
Use the old daemon’s owning binary for drain and let that process finish before
starting the selected new image. A draining acknowledgement alone does not
prove exit. Protocol 1.4 and 1.5 share a singleton major; avoid mixed explicit
binary paths and do not treat --skip-drain as ordinary upgrade coordination.
Reattach addresses still wanted after replacement; durable messages survive,
but this does not promise automatic push-intent restoration.
For a manual in-place replacement, drain and replace in this order:
telex station stop --address <addr>
telex daemon stop --drain
# replace the telex binary
telex attach --address <addr> --description "<s>"
telex wait --address <addr> --out-dir <dir>
If a session resumes without an armed waiter, recovery is durable: inspect
telex inbox --address <addr> and telex read --id <id>, then arm a fresh wait.
Auditing
Export messages and disposition history as JSON lines for provenance:
telex export --address <addr>
telex export --thread <id>
telex export --since <id>
Push recovery after a daemon replacement
A push-attended station (a Copilot bridge bound with telex copilot attach --copilot-bridge)
records a durable station intent: the exact desired push registration for that
(store, session, address) binding. When a daemon is replaced — telex upgrade,
telex daemon stop --drain, or a crash — the successor restores the registration by itself, with
no manual telex copilot resume.
Restoration is not “replay a record”. The successor restores a binding only after it has proved the
producer is alive: same user, matching executable, matching pid and process start time, matching
machine and boot, and an authenticated liveness probe whose nonce the producer must echo. A producer
that cannot be proved alive is never restored — messages stay durable and readable with
telex inbox, and the condition is reported rather than silent.
Published recovery bounds
Both bounds are measured from “a compatible successor daemon is running and the producer is live and verifiable”, and both are derived from the reconciler’s constants rather than asserted.
| Situation | Bound |
|---|---|
| Graceful drain or upgrade | ≤ 10 s (one 5 s reconcile tick + a 1 s probe + a 2 s validation/claim allowance) |
| Hard crash | liveness_window_secs() + 10 s (the crashed daemon never released its lease, so the successor waits for it to go stale) |
Both bounds are qualified. They apply only when the automatic enumeration and required reads complete, and to an intent that is:
- in a scope holding no more than 64 live intents (one pass budget), so it is attempted in the first pass after the trigger; and
- not currently in failure backoff, pull-waiter backoff, or quarantine.
They explicitly exclude: no successor daemon exists at all (start one with telex attach), a
backend outage, a competing fresh owner, and a producer too old to answer the liveness probe.
Larger scopes
A scope holding more than one pass budget has a queue delay, not a fixed recovery bound. Each pass
still returns within four seconds and advances a scope-specific cursor, but bounded discovery,
backoff, quarantine, and slow intents mean there is no truthful minimum progress count per pass.
Use successive reconcile reports and telex status to observe progress rather than deriving a
completion time from the scope size.
If a report is truncated or degraded, its count and over-cap indication are lower bounds, not an inventory or capacity-recovery promise. Automatic scans and GC cover records only when their enumeration and read opportunities complete; they do not promise fair stable-tail progress, a complete automatic GC, or exact capacity recovery.
For an exact inventory or eligible reclamation, use the explicitly offline path:
telex daemon stop --drain
# Stop other telex/Copilot processes that can write station intents.
telex --json daemon recover-intents # exact inventory
telex --json daemon recover-intents --gc # exact inventory, eligible GC, exact remaining count
The command refuses rather than guessing if a daemon is still reachable, its stopped state cannot be established, the intent scope is absent, or the scope is not positively recognized as supported local storage. Do not infer completeness from bounded reports.
At the 512-record write cap, existing records can still be updated or explicitly withdrawn. A live
record withdrawn to revoked continues to occupy its slot for the seven-day terminal TTL; daemon
GC frees it only after that TTL. Detach therefore does not free capacity immediately.
Waiting is not failing
telex --address <station> status reports a station-intent row with a state. The one that most
often surprises people is deferred_lease: after a crash, the predecessor’s epoch lease is not
stale yet, so the successor is waiting for it to expire, not failing. It retries at a fixed 5 s
cadence, never backs off exponentially, and never counts toward quarantine. next_attempt_ms says
when it will try again.
A genuine failure takes a jittered 5 s → 10 s → 20 s → … → 5 min ladder, and after ten consecutive
failures drops to an hourly cadence. Repairing the cause does not leave you waiting out the
ladder: any durable change to the record — the turn-boundary producer refresh after a bridge
reload, a copilot resume, a re-attach — clears it, and the next tick attempts the binding again.
Three conditions are named explicitly in status:
live_intent_missing_member— push is desired here but not currently armed.member_missing_live_producer— push is registered but the producer has gone quiet.intent_protocol_incompatible— this daemon cannot reconcile the recorded intent (schema skew, or a producer that predates the liveness probe).
What recovery does not do
- It never silently downgrades push to pull. If a plain
telex attachwould create a pull-only member over a live push intent, the daemon either reconciles it to push or refuses withIncompatible/PushIntentUnrecoverable. It never creates the pull-only member. This guarantee is scoped to the case with no live armed pull waiter: an armed waiter still wins, and the intent waits rather than forcing the conflict. - It never returns an explicitly detached station.
telex copilot detachandtelex station stopwrite a durable tombstone, and the reconciler honors it unconditionally. Explicit attach is the only way back. - It never crosses hosts. Intents are local files bound to this machine and this boot, so a shared Postgres store — or a synced home directory — cannot let one host restore another’s bridge.
- It never re-arms a station you reset.
telex station reset(andtelex daemon reset) withdraws the desired state as well as the membership: the affected intents are revoked, so the reconciler leaves the station idle.telex --address <station> copilot resumeis the way back. - It never arms an attach that was never registered. A running bridge is not by itself permission to turn a half-written attach into a live push binding. Finalizing one requires either a daemon that reports push armed for it right now, or the daemon’s own durable record that it armed the binding earlier.
- It never reports a push registration it cannot prove. For a station that has a station-intent
record, the daemon writes its durable “I armed this” proof before it installs the member, and
refuses the whole registration if that write fails. So there is no state in which push is armed
and nothing on disk says so — you either get a working, recoverable station or a typed refusal
(
PushIntentUnrecoverable) with no member created and any claimed lease released. A station that was already attended is left exactly as it was. A station with no intent record — a pull attach, or a plaintelex attach --on-deliver— has nothing to prove and is never refused by this; that includes on a host where the intent scope could not be created at all. - It never re-uses a torn-down station’s arming. Re-attaching an address you detached (or that a session end revoked) starts a fresh attach: it gets the full attach window to finish, and it has to be armed again by the daemon before anything can promote it. The old record’s proof is not carried over.
- It never treats “I could not read that” as “there is nothing there”. Every recovery decision
that turns on whether a station has a durable record — may this register commit, may this pull
attach downgrade push, has this station’s credential really been deleted — is decided from a
positive answer. If the intent scope, a station’s record, or a bridge credential exists but the
operating system will not report on it (a permissions change, a lock, a profile on a volume that
went away), telex refuses or waits rather than proceeding as if the station were new. You may see
a
PushIntentUnrecoverablerefusal you would not have seen before; the alternative was a registration that reported success and left nothing recoverable behind.
Recovering from a bridge reload
extensions_reload, /clear, and an extension-host restart all give the Copilot bridge a new
process. The daemon proves a producer by executable, pid, and process start time before it sends
anything, so a reloaded bridge no longer matches what the intent recorded, and status shows
producer_identity_mismatch.
This heals on its own: the next Copilot turn boundary re-records the running bridge’s identity — after proving it answers a liveness probe — and the following reconcile tick restores the binding. It does not need a daemon that already has the station armed, which is the case that used to deadlock: a reload followed by a daemon replacement leaves a successor that cannot create the member because the identity is stale, and cannot refresh the identity if refreshing required the member.
If a session ends before a turn boundary happens, telex --address <station> copilot resume does
the same thing immediately.
Before you drain
telex daemon stop --drain, telex upgrade, and telex rollback print a pre-drain intent report:
station intents recoverable 2 pending 0 degraded 0 incompatible 0 unknown 0
recoverable is what a successor is expected to restore automatically. pending is a push attach
that has not finalized yet — it is not restored automatically; it finalizes at the next Copilot
turn boundary (after extensions_reload), and only then becomes recoverable. degraded and
incompatible need action: run telex --address <station> copilot resume after the switch.
Rolling back to a binary that predates this feature returns those stations to manual resume, and the
rollback output warns about it.
The report reads the durable records as well as the daemon’s cached view, so a station you attached
seconds ago is counted correctly, and a station whose recorded producer identity was just repaired
(the usual case after extensions_reload) is counted as recoverable rather than carrying forward the
failure the repair fixed.
telex upgrade and telex rollback then drive one reconciliation pass on the successor by invoking
the binary the switch just selected (telex daemon reconcile on that binary). That indirection is
required, not cosmetic: the daemon only accepts IPC from a client whose executable matches its own,
so a pass requested from the pre-switch process would either spawn the wrong binary or be refused.
The successor summary distinguishes a pass that ran and restored nothing from one that never ran.
The station_intent_reconcile object in --json output always names the binary it is about
(successor_binary, null only for --no-switch), including when the step was skipped or the
successor failed. A successor that ran but could not complete a pass exits non-zero and reports
{"reconciled": false, "error": ...}; that structured reason is carried through verbatim (bounded)
rather than replaced with a generic rejection. A successor CLI child that overruns the bound is
killed and reaped before upgrade/rollback returns; a daemon it successfully spawned is a
separate detached service governed by the normal daemon lifecycle. The result records timed_out,
terminated, reaped, and the direct child’s pid.
Destructive testing
Station intents are namespaced by a hash of your user identity, your canonicalized config root,
and the protocol major. To experiment destructively without touching your real bindings, point
TELEX_CONFIG (and TELEX_HOME) at a scratch directory: the scratch daemon gets its own intent
scope, and nothing you do there can restore or revoke a station in your normal scope.
Recovering from a lost daemon
A wait that finds no daemon exits with a distinct code (see
Exit codes). Run telex attach (the spawning and
recovery verb) and re-arm the wait. If a replacement daemon already exists, a
wait can reconnect during its bounded reconnect grace.
Turn-end and resume reconciliation
For turn-end guards or resume reconciliation, use
telex station status --session <id> to get a compact JSON projection of the
session’s attended addresses, waiter counts, station health, and pending
unconsumed counts.
Uninstall and cleanup
- Stop the daemon:
telex daemon stop --drain. - Remove local state: delete
~/.telex/(the SQLite store and config). - Remove the Copilot plugin, if installed:
copilot plugin uninstall telex@telex. - For a Postgres backend, drop the telex schema in the database if it is no longer needed.
Troubleshooting
no session id available
send, reply, ack, and the disposition verbs need a stable session id. Pass
--session <id> or set TELEX_SESSION_ID. In Copilot CLI, pass
--session "$COPILOT_AGENT_SESSION_ID". Telex fails closed rather than guessing
an identity.
wait exits 3 (daemon gone / not running)
wait does not spawn a missing daemon. Run telex attach --address <addr> ...
(the spawning and recovery verb), then re-arm the wait. See
Exit codes.
one live waiter is already armed
Only one live waiter per station is allowed. Let the current waiter complete, or
run telex station stop --address <addr>, then re-attach and arm the new mode.
Cannot re-arm because the prior message is unacked
Ack the delivered message first (telex ack --id <id> --session <id>), then arm a
fresh wait.
Send refused as ambiguous, or a warning that it is un-repliable
Every send stamps a from. If your session attends more than one address, pass
--from <addr>. If no --from, --address, or attended station is set, the send
warns that it is un-repliable; attach the address first or pass --from.
address <addr> is retired
The target address was retired and dropped from listings. Use a live address, or the owner can attend it again.
Copilot: messages do not arrive as turns
The push bridge may not be loaded; provisioning files and registering push do not
prove it is live. On a host with a supported live loader, re-provision with
telex --address <addr> copilot resume and load the bridge (extensions_reload
where available). On Copilot CLI only, enabling Copilot Extensions under
/experimental may expose that tool. For an App session without a supported live
loader, startup extension discovery is another loading path: provision with
telex --address <addr> copilot attach --copilot-bridge, restart the App session,
then run telex --address <addr> copilot resume. Verify
telex --address <addr> --json status reports station_health: attended_push and
push_registered: true; where available, call telex_bridge_info for supporting
bridge evidence.
Keep an already-loaded, live bridge in push mode regardless of host. A restart is not proof by itself. Issue #230 reports startup loading and pushed delivery on one Windows App build, not every App build or a plain conversation clear. Liveness and end-to-end delivery are separate checks.
When a live bridge is unavailable and a restart is unacceptable or liveness does
not verify, use the supported
Copilot pull fallback with
telex --address <addr> copilot fallback prepare, or detach with
telex --address <addr> copilot detach.
Preparing fallback does not prove a waiter started or woke the App: follow the generated
single-shot launch and completion-artifact procedure, and report any setup/backend
failure rather than treating it as successful delivery.
Backend authentication failures (Postgres / Entra)
Check the connection string and credentials. telex init --backend <name>
validates connectivity and creates the schema, surfacing errors early. For Entra,
ensure az login has run, or use --entra-cred managed on a host with a managed
identity.
Inspecting state
telex status --address <addr>: resolved backend and address projection.telex station status --session <id>: attended addresses, waiter counts, and station health for a session.telex daemon status: daemon internals.
Copilot: push was working, then stopped after an upgrade or daemon restart
This should recover on its own. A push-attended station records a durable station intent, and a
successor daemon restores it within about 10 seconds of being up (or liveness_window_secs() + 10 s
after a hard crash, because the crashed daemon’s lease has to go stale first). See
Push recovery after a daemon replacement.
If it has not recovered, check the station:
telex --address <addr> status
Look at the station_intent line:
| State | Meaning | What to do |
|---|---|---|
deferred_lease | Waiting for a crashed predecessor’s lease to go stale. Not an error. | Wait; next_attempt_ms says when it retries. |
deferred_pull_waiter | A live telex wait owns the address; pull wins. | Stop the waiter (telex station stop) if you want push back. |
legacy_producer | The bridge predates the liveness probe, so it cannot be proved alive. | Follow bridge recovery using live reload or attach/restart/resume and verify liveness; otherwise use pull fallback. |
unverifiable | The producer or its credential could not be resolved (bridge gone, registry stale). | Follow bridge recovery using live reload or attach/restart/resume and verify liveness; otherwise use pull fallback. |
insecure | A permissions check failed on the bridge directory or credential file. | Fix the permissions (owner-only), then resume. |
incompatible | This daemon cannot reconcile the recorded intent (version skew). | telex --address <addr> copilot resume. |
quarantined | Ten consecutive genuine failures; now retrying hourly. | Fix the underlying cause, then resume to reset it. |
revoked | The station was explicitly detached or its session ended. | Intentional. Explicit attach is the only way back. |
If reconciliation reports a lock or local-filesystem failure, keep the intent scope on owner-private fixed local storage. Network shares, NFS/SMB, 9p, and filesystems whose advisory-lock semantics cannot be proven are intentionally refused. A busy live writer is bounded and reported; it is never stolen. Retry after that writer exits.
Station-intent capacity needs an exact inventory or reclamation
An automatic reconcile report marked truncated or degraded is not an exact inventory: its count and
over-cap indication are lower bounds. Do not retry it until it happens to look complete. Stop the
daemon with telex daemon stop --drain, stop other intent writers, then run
telex --json daemon recover-intents for an exact supported-local inventory. Add --gc only when
you want eligible TTL-expired records reclaimed. The command refuses if it cannot establish the
daemon-stopped and supported-local-storage floors; fix that condition rather than trusting a partial
count.
Messages are durable the whole time — read them with telex inbox --address <addr> — and the turn
guard warns rather than blocking, so an unrestored intent never wedges a session.
Copilot: telex attach refuses with PushIntentUnrecoverable
This code covers two refusals, both of which fail toward telling you rather than quietly degrading.
A pull attach over a live push intent. The address has a live push intent that could not be restored, and telex refused to create a pull-only member over it rather than silently downgrading your delivery mode.
Either restore push with an already-live bridge or a supported live loader:
telex --address <addr> copilot resume # use the supported loader if needed
or follow the attach/restart/resume startup-discovery path and verify liveness. When a restart is unacceptable or liveness does not verify, use the prepared pull fallback. To give up attendance explicitly:
telex --address <addr> copilot detach
after which a plain telex attach works normally.
A push registration whose durable proof could not be written. A push attach for a binding that has a station-intent record only succeeds once the daemon has durably recorded that it armed delivery — that record is the only thing that carries push across a daemon replacement, so a registration that cannot write it is refused instead of leaving you with push that works now and no recovery later. The message names the write that failed. Causes are almost always local:
- the station-intent record for this station is present but unreadable (corrupt, or written by a build this one cannot verify), or
- the station-intent record or the scope that holds it exists but the operating system will not report on it — a permissions change, a file lock, a profile on a network volume that went away. Telex refuses here rather than assuming the station is new, because assuming that would commit push with nothing recoverable behind it, and
- the station-intent scope is not writable (check the permissions on the daemon run directory), or
- a concurrent
copilot attach/copilot resumefor the same station raced this one and rolled its own record back.
A station with no intent record at all — a pull attach, or a plain telex attach --on-deliver —
owes no proof and is never refused by this, even if the intent scope cannot be created on this host.
Nothing is left half-armed: no member is created, and any epoch lease the attempt claimed is
released. Re-run the attach — telex --address <addr> copilot resume — once the scope is writable
or the competing attach has finished.
CLI reference
This page is generated from the installed
telexbinary (telex 0.3.0 (build f17bab00a95f29551ad95a155e3d4258fb9d1eb8)) bydocs/guide/generate-reference.sh. Do not edit it by hand; it is regenerated on every docs build so it stays matched to the binary. For the workflow narrative, see the Guides.
telex
Telex lets ephemeral agent sessions attach to durable addresses, exchange typed operational messages with answerback liveness, and leave an auditable disposition record. Run `telex skill` to load agent usage instructions for this build.
Usage: telex [OPTIONS] <COMMAND>
Commands:
init Initialize ~/.telex and the backend schema
status Show config, backend, address, station/occupancy status
version Show telex binary, launcher, protocol, and install metadata
upgrade Install a versioned telex binary and optionally switch current to it
rollback Switch current back to a previously installed version
gc Garbage-collect old installed telex versions
skill Print the agent usage skill (how to use telex) for this build
attach Attach this session to an address and exit
detach Detach this session's address membership
station Station lifecycle operations
wait Block until an actionable message arrives, print it as JSON, and exit
inbox List actionable and recent messages for an address
read Read a message (optionally with thread context)
send Send a message to an address
reply Reply to a message; threads under it
ack Acknowledge a message
handle Mark a message handled (terminal)
defer Defer a message
reject Reject a message (terminal)
close Close a message/thread (terminal)
escalate Escalate a message
address Address directory operations
resolve Resolve target address(es) by description match or tag
backend Manage configured backends (named profiles in ~/.telex/config.toml)
export Export messages and disposition history as JSON lines
help Print this message or the help of the given subcommand(s)
Options:
--backend <BACKEND>
Configured backend to use, by name (default: the configured default backend)
[env: TELEX_BACKEND=]
--db <DB>
Override the SQLite path for this invocation (sqlite backends only)
[env: TELEX_DB=]
--address <ADDRESS>
Address to operate on (default for commands that act on one address)
[env: TELEX_ADDRESS=]
--json
Force JSON output
--text
Force concise text output
-h, --help
Print help (see a summary with '-h')
-V, --version
Print version
telex init
Initialize ~/.telex and the backend schema
Usage: telex init [OPTIONS]
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--json Force JSON output
--text Force concise text output
-h, --help Print help
telex status
Show config, backend, address, station/occupancy status
Usage: telex status [OPTIONS]
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--json Force JSON output
--text Force concise text output
-h, --help Print help
telex version
Show telex binary, launcher, protocol, and install metadata
Usage: telex version [OPTIONS]
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--root <ROOT> Inspect a specific versioned install root instead of inferring it from this executable
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--json Force JSON output
--text Force concise text output
-h, --help Print help
telex upgrade
Install a versioned telex binary and optionally switch current to it
Usage: telex upgrade [OPTIONS]
Options:
--backend <BACKEND>
Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--from <PATH>
Local telex binary or directory containing telex(.exe) to install (manual/local upgrade path). Omit to discover, download, verify, and install the latest compatible public GitHub release
--db <DB>
Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--version <VERSION>
Release tag to install/switch to. Without --from this selects an explicit public release (e.g. v0.2.0); with --from it labels the local install (defaults to this binary's package version)
--address <ADDRESS>
Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--force
Bypass the already-current shortcut to repair a missing immutable version target. An existing version directory is never overwritten
--json
Force JSON output
--root <ROOT>
Versioned install root (default: inferred install root or platform default)
--text
Force concise text output
--no-switch
Install into versions/<tag> but do not switch current
--skip-drain
Skip daemon drain before switching current (not recommended)
--drain-timeout-ms <DRAIN_TIMEOUT_MS>
Bound daemon drain before switching current [default: 10000]
-h, --help
Print help
telex rollback
Switch current back to a previously installed version
Usage: telex rollback [OPTIONS]
Options:
--backend <BACKEND>
Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--version <VERSION>
Installed version tag to switch to (default: previous)
--db <DB>
Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--root <ROOT>
Versioned install root (default: inferred install root or platform default)
--address <ADDRESS>
Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--skip-drain
Skip daemon drain before switching current (not recommended)
--drain-timeout-ms <DRAIN_TIMEOUT_MS>
Bound daemon drain before switching current [default: 10000]
--json
Force JSON output
--text
Force concise text output
-h, --help
Print help
telex gc
Garbage-collect old installed telex versions
Usage: telex gc [OPTIONS]
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--root <ROOT> Versioned install root (default: inferred install root or platform default)
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--dry-run Report what would be removed without deleting anything
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--force Try removal of stale versions even after ordinary removal errors; never removes current, previous, or the active process version
--json Force JSON output
--text Force concise text output
-h, --help Print help
telex skill
Print the agent usage skill (how to use telex) for this build
Usage: telex skill [OPTIONS]
Options:
--address <ADDRESS> Tailor the instructions for a specific assigned address
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--raw Print the embedded SKILL.md verbatim (including frontmatter)
--json Force JSON output
--text Force concise text output
-h, --help Print help
telex attach
Attach this session to an address and exit
Usage: telex attach [OPTIONS]
Options:
--backend <BACKEND>
Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--description <DESCRIPTION>
One-line directory description of what this session is doing
--db <DB>
Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--scope <SCOPE>
Project/workstream scope this address belongs to
--address <ADDRESS>
Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--tags <TAGS>
Comma-separated coarse tags (e.g. issue:215,repo:telex)
--heartbeat-secs <HEARTBEAT_SECS>
Deprecated compatibility flag; the daemon owns lease heartbeat cadence [default: 5]
--json
Force JSON output
--poll-secs <POLL_SECS>
Deprecated compatibility flag; the daemon owns backend polling [default: 1]
--text
Force concise text output
--keepalive-secs <KEEPALIVE_SECS>
Deprecated compatibility flag; daemon waiters use daemon IPC frames [default: 3]
--occupant <OCCUPANT>
Occupant identity recorded on the lease (default: session host/pid)
--session <SESSION>
Stable session identity for daemon membership [env: TELEX_SESSION_ID=]
--push
Deprecated compatibility flag; daemon delivery owns push/poll behavior
--session-pid <SESSION_PID>
Back-compat watch pid. Converted to an anchor watch-pid for daemon liveness
--watch-pid <WATCH_PID>
Watch a pid as a typed liveness predicate. Accepts PID, anchor:PID, required:PID, PID:anchor, or PID:required. Repeat to add multiple watch pids
--session-poll-secs <SESSION_POLL_SECS>
Deprecated compatibility flag; daemon liveness cadence is internal [default: 2]
--no-session-bind
Do not convert `$TELEX_SESSION_PID` into a daemon watch-pid
-h, --help
Print help
telex detach
Detach this session's address membership
Usage: telex detach [OPTIONS]
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--session <SESSION> Stable session identity for daemon membership [env: TELEX_SESSION_ID=]
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--json Force JSON output
--text Force concise text output
-h, --help Print help
telex station
Station lifecycle operations
Usage: telex station [OPTIONS] <COMMAND>
Commands:
status Show this session's attended addresses and waiter state
stop Stop this session's station: release membership and drain its live waiters
help Print this message or the help of the given subcommand(s)
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--json Force JSON output
--text Force concise text output
-h, --help Print help
telex station status
Show this session's attended addresses and waiter state
Usage: telex station status [OPTIONS]
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--session <SESSION> Stable session identity for daemon membership [env: TELEX_SESSION_ID=]
--all-sessions Show all stations in the selected store instead of only this session
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--json Force JSON output
--text Force concise text output
-h, --help Print help
telex station stop
Stop this session's station: release membership and drain its live waiters
Usage: telex station stop [OPTIONS]
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--session <SESSION> Stable session identity for daemon membership [env: TELEX_SESSION_ID=]
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--wait-grace-ms <WAIT_GRACE_MS> How long to wait for live waiter processes to exit after teardown is signaled (ms) [default: 3000]
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--json Force JSON output
--text Force concise text output
-h, --help Print help
telex wait
Block until an actionable message arrives, print it as JSON, and exit
Usage: telex wait [OPTIONS]
Options:
--backend <BACKEND>
Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--session <SESSION>
Stable session identity for daemon membership [env: TELEX_SESSION_ID=]
--db <DB>
Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--timeout-ms <TIMEOUT_MS>
Give up waiting after this many milliseconds (exit code 2); default is no idle timeout
--address <ADDRESS>
Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--min-attention <MIN_ATTENTION>
Only wake for messages at this attention or higher priority
--json
Force JSON output
--wake-on-cc
Also wake for live CC traffic without making CC ack-required
--since <SINCE>
Resume delivery strictly after this message id [default: 0]
--text
Force concise text output
--hang-ms <HANG_MS>
Deprecated idle-wait compatibility watchdog. For daemon waits, only applies after timeout-ms [default: 8000]
--reconnect-grace-ms <RECONNECT_GRACE_MS>
Retry daemon reconnect/re-register for this long after EOF/restart (ms) [env: TELEX_RECONNECT_GRACE_MS=]
--stale-heartbeat-ms <STALE_HEARTBEAT_MS>
Holder DB-heartbeat age beyond which it is considered degraded (ms) [default: 15000]
--out-dir <OUT_DIR>
Write outcome artifacts into this directory so a detached, variable-free invocation can deliver results without relying on captured stdout. Writes `message.json` (on delivery), `status.json` (always), and `exit.code` (always, written last as the completion marker)
-h, --help
Print help
telex inbox
List actionable and recent messages for an address
Usage: telex inbox [OPTIONS]
Options:
--all Include all recent messages, not just actionable ones
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--limit <LIMIT> Maximum messages to list [default: 50]
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--json Force JSON output
--text Force concise text output
-h, --help Print help
telex read
Read a message (optionally with thread context)
Usage: telex read [OPTIONS] --id <ID>
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--id <ID> Message id to read
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--thread Include compact thread context
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--full Include full thread history and dispositions
--json Force JSON output
--text Force concise text output
-h, --help Print help
telex send
Send a message to an address
Usage: telex send [OPTIONS] --to <TO>
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--to <TO> Destination address
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--subject <SUBJECT> Subject line
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--body <BODY> Message body (inline). Body/subject/metadata are capped below the 1 MiB IPC frame
--body-file <BODY_FILE> Read the message body from a UTF-8 file (`-` = stdin); capped below the 1 MiB IPC frame
--json Force JSON output
--body-stdin Read the message body from stdin (UTF-8). Equivalent to `--body-file -`. On Windows / PowerShell, run `$OutputEncoding = [System.Text.Encoding]::UTF8` before piping non-ASCII content, or write a UTF-8 file and use `--body-file <path>` instead
--text Force concise text output
--cc <CC> CC addresses (visible observers). May be repeated and/or comma-separated
--kind <KIND> Message kind/profile label [default: note]
--attention <ATTENTION> Attention level: interrupt | next-checkpoint | background | fyi [default: background]
--requires-disposition Mark that the recipient must disposition this message
--from <FROM> Sender address (defaults to the global --address if set)
--metadata <METADATA> Arbitrary JSON metadata; counted with body/subject against the IPC payload cap
--session <SESSION> Stable session identity for daemon membership [env: TELEX_SESSION_ID=]
-h, --help Print help
telex reply
Reply to a message; threads under it
Usage: telex reply [OPTIONS] --to-message <TO_MESSAGE>
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--to-message <TO_MESSAGE> The message id being replied to
--body <BODY> Reply body (inline). Body/subject are capped below the 1 MiB IPC frame
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--body-file <BODY_FILE> Read the reply body from a UTF-8 file (`-` = stdin); capped below the 1 MiB IPC frame
--body-stdin Read the reply body from stdin (UTF-8). Equivalent to `--body-file -`. On Windows / PowerShell, run `$OutputEncoding = [System.Text.Encoding]::UTF8` before piping non-ASCII content, or write a UTF-8 file and use `--body-file <path>` instead
--json Force JSON output
--subject <SUBJECT> Subject (defaults to "Re: <parent subject>")
--text Force concise text output
--cc <CC> CC addresses (visible observers). May be repeated and/or comma-separated
--attention <ATTENTION> Attention level [default: background]
--requires-disposition Mark that the recipient must disposition this reply
--from <FROM> Sender address (defaults to the global --address if set)
--kind <KIND> Message kind/profile label [default: note]
--session <SESSION> Stable session identity for daemon membership [env: TELEX_SESSION_ID=]
-h, --help Print help
telex ack
Acknowledge a message
Usage: telex ack [OPTIONS] --id <ID>
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--id <ID> Message id to disposition
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--note <NOTE> Optional note recorded with the disposition
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--recipient <RECIPIENT> Recipient address whose disposition this is (defaults to the message's to_addr)
--json Force JSON output
--session <SESSION> Stable session identity for daemon membership [env: TELEX_SESSION_ID=]
--text Force concise text output
-h, --help Print help
telex handle
Mark a message handled (terminal)
Usage: telex handle [OPTIONS] --id <ID>
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--id <ID> Message id to disposition
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--note <NOTE> Optional note recorded with the disposition
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--recipient <RECIPIENT> Recipient address whose disposition this is (defaults to the message's to_addr)
--json Force JSON output
--session <SESSION> Stable session identity for daemon membership [env: TELEX_SESSION_ID=]
--text Force concise text output
-h, --help Print help
telex defer
Defer a message
Usage: telex defer [OPTIONS] --id <ID>
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--id <ID> Message id to disposition
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--note <NOTE> Optional note recorded with the disposition
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--recipient <RECIPIENT> Recipient address whose disposition this is (defaults to the message's to_addr)
--json Force JSON output
--session <SESSION> Stable session identity for daemon membership [env: TELEX_SESSION_ID=]
--text Force concise text output
-h, --help Print help
telex reject
Reject a message (terminal)
Usage: telex reject [OPTIONS] --id <ID>
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--id <ID> Message id to disposition
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--note <NOTE> Optional note recorded with the disposition
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--recipient <RECIPIENT> Recipient address whose disposition this is (defaults to the message's to_addr)
--json Force JSON output
--session <SESSION> Stable session identity for daemon membership [env: TELEX_SESSION_ID=]
--text Force concise text output
-h, --help Print help
telex close
Close a message/thread (terminal)
Usage: telex close [OPTIONS] --id <ID>
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--id <ID> Message id to disposition
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--note <NOTE> Optional note recorded with the disposition
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--recipient <RECIPIENT> Recipient address whose disposition this is (defaults to the message's to_addr)
--json Force JSON output
--session <SESSION> Stable session identity for daemon membership [env: TELEX_SESSION_ID=]
--text Force concise text output
-h, --help Print help
telex escalate
Escalate a message
Usage: telex escalate [OPTIONS] --id <ID>
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--id <ID> Message id to disposition
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--note <NOTE> Optional note recorded with the disposition
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--recipient <RECIPIENT> Recipient address whose disposition this is (defaults to the message's to_addr)
--json Force JSON output
--session <SESSION> Stable session identity for daemon membership [env: TELEX_SESSION_ID=]
--text Force concise text output
-h, --help Print help
telex address
Address directory operations
Usage: telex address [OPTIONS] <COMMAND>
Commands:
list List addresses with description, occupancy, and liveness
show Show detail for one address (uses --address)
retire Retire an address (drops from normal listings)
help Print this message or the help of the given subcommand(s)
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--json Force JSON output
--text Force concise text output
-h, --help Print help
telex address list
List addresses with description, occupancy, and liveness
Usage: telex address list [OPTIONS]
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--scope <SCOPE> Limit to addresses in this scope
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--match <MATCH> Substring match against address or description
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--tag <TAG> Match a tag (substring of the tags field)
--all Include retired addresses
--json Force JSON output
--text Force concise text output
-h, --help Print help
telex address show
Show detail for one address (uses --address)
Usage: telex address show [OPTIONS]
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--json Force JSON output
--text Force concise text output
-h, --help Print help
telex address retire
Retire an address (drops from normal listings)
Usage: telex address retire [OPTIONS]
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--json Force JSON output
--text Force concise text output
-h, --help Print help
telex resolve
Resolve target address(es) by description match or tag
Usage: telex resolve [OPTIONS]
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--match <MATCH> Substring to match against address or description
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--tag <TAG> Tag to match
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--scope <SCOPE> Limit to a scope
--json Force JSON output
--text Force concise text output
-h, --help Print help
telex backend
Manage configured backends (named profiles in ~/.telex/config.toml)
Usage: telex backend [OPTIONS] <COMMAND>
Commands:
add Add (or update) a named backend
list List configured backends
show Show one backend's configuration (secrets redacted)
remove Remove a configured backend
default Set the default backend
kinds List the backend kinds compiled into this build
help Print this message or the help of the given subcommand(s)
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--json Force JSON output
--text Force concise text output
-h, --help Print help
telex backend add
Add (or update) a named backend
Usage: telex backend add [OPTIONS] <NAME>
Arguments:
<NAME> Name (key) for this backend
Options:
--backend <BACKEND>
Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--sqlite
Configure a SQLite backend (path defaults to ~/.telex/telex.db)
--db <DB>
Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--postgres <CONN>
Configure a Postgres backend from this connection string (libpq URI or key=value DSN)
--address <ADDRESS>
Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--path <PATH>
SQLite file path (with --sqlite)
--json
Force JSON output
--schema <SCHEMA>
Postgres schema to isolate telex tables in
--password-env <PASSWORD_ENV>
Read the Postgres password from this environment variable
--text
Force concise text output
--password-command <PASSWORD_COMMAND>
Obtain the Postgres password by running this shell command (its stdout)
--entra
Use Microsoft Entra auth for Postgres (token fetched via the Azure SDK)
--entra-cred <MODE>
Entra credential mode: auto (dev/CLI login), cli, or managed (devbox/VM identity)
--entra-scope <ENTRA_SCOPE>
Override the Entra token scope
--default
Make this the default backend
-h, --help
Print help
telex backend list
List configured backends
Usage: telex backend list [OPTIONS]
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--json Force JSON output
--text Force concise text output
-h, --help Print help
telex backend show
Show one backend's configuration (secrets redacted)
Usage: telex backend show [OPTIONS] <NAME>
Arguments:
<NAME> Backend name
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--json Force JSON output
--text Force concise text output
-h, --help Print help
telex backend remove
Remove a configured backend
Usage: telex backend remove [OPTIONS] <NAME>
Arguments:
<NAME> Backend name
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--json Force JSON output
--text Force concise text output
-h, --help Print help
telex backend default
Set the default backend
Usage: telex backend default [OPTIONS] <NAME>
Arguments:
<NAME> Backend name
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--json Force JSON output
--text Force concise text output
-h, --help Print help
telex backend kinds
List the backend kinds compiled into this build
Usage: telex backend kinds [OPTIONS]
Options:
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--address <ADDRESS> Address to operate on (default for commands that act on one address) [env: TELEX_ADDRESS=]
--json Force JSON output
--text Force concise text output
-h, --help Print help
telex export
Export messages and disposition history as JSON lines
Usage: telex export [OPTIONS]
Options:
--address <ADDRESS> Limit to messages to/from this address (defaults to the global --address)
--backend <BACKEND> Configured backend to use, by name (default: the configured default backend) [env: TELEX_BACKEND=]
--db <DB> Override the SQLite path for this invocation (sqlite backends only) [env: TELEX_DB=]
--thread <THREAD> Limit to a thread id
--since <SINCE> Only messages with id greater than this [default: 0]
--json Force JSON output
--text Force concise text output
-h, --help Print help
Advanced commands
The telex copilot and telex daemon command families serve the plugin
adapter and operators, and are omitted from the list above. Document them
from the binary with telex copilot --help and telex daemon --help.
Delivered message shape
telex wait prints one delivered message as JSON on exit 0. With --out-dir it
writes the same message to message.json, and an envelope form to
delivery.json ({ "message": {...}, "delivery": {...}, "status": {...} }). A
pull-mode script or harness parses this to drive an agent.
Example
Output of telex wait --json for one delivered message:
{
"id": 118,
"thread_id": 118,
"parent_id": null,
"from": "demo:jsondoc",
"to": "demo:jsondoc",
"primary_to": "demo:jsondoc",
"delivered_to": "demo:jsondoc",
"delivery_role": "to",
"cc": [],
"kind": "note",
"attention": "next-checkpoint",
"requires_disposition": true,
"requires_disposition_for_current_recipient": true,
"subject": "Hello",
"body": "world",
"metadata": null,
"sent_at_ms": 1783355088938,
"buffered_at_ms": 1783355089013,
"delivery_id": 321,
"snapshot_version": 44,
"lease_epoch": 1
}
Fields
| Field | Meaning |
|---|---|
id | Message id. Use it with ack, handle, read, and for dedupe. |
thread_id | Root message id of the thread. |
parent_id | Parent message id for a reply, otherwise null. |
from | Sender address; a reply routes here. |
to | The address the message was sent to. |
primary_to | The primary recipient address. |
delivered_to | The recipient address this delivery is for. |
delivery_role | to or cc: this recipient’s role in the message. |
cc | CC (observer) addresses. |
kind | Message kind label (default note). |
attention | interrupt, next-checkpoint, background, or fyi. |
requires_disposition | Whether the sender marked the message as requiring disposition. |
requires_disposition_for_current_recipient | The same, scoped to this recipient. |
subject | Subject, if any. |
body | Message body. |
metadata | Opaque message metadata, if any. |
sent_at_ms, buffered_at_ms | Unix millisecond timestamps. |
delivery_id | Exact per-recipient delivery-row identity for this delivered JSON record. Legacy CLI ack still addresses message plus recipient; supported applications use the typed Application Client ack handle rather than parsing this field from CLI output. |
snapshot_version | Store state fence captured with delivery selection. |
lease_epoch | The recipient station’s lease epoch at delivery. |
Fields whose names end in _ms such as backend_ms, send_to_exit_ms, and
waiter_exit_ms are timing diagnostics, not message content.
Consume by id
Delivery is at-least-once, so dedupe by id and ack it to consume it from the
delivery buffer:
telex ack --address <addr> --id <id> --session <session-id>
Files written by --out-dir
message.json: the flat delivered message (exit 0 only).delivery.json: the envelope{ message, delivery, status }(exit 0 only).status.json:{ outcome, exit_code, detail, quarantine?, ... }(always). Exit 6 stores its structured evidence inquarantineand removes any stalemessage.json/delivery.json; it is not a delivered-message artifact. Exit 7 reports exhausted backend recovery asbackend-unavailable.exit.code: the integer exit code, written last as the completion marker.
Exit 7 applies when both the CLI and daemon support wait-backend-recovery.
Legacy CLI artifacts instead contain outcome: "error", exit_code: 1, and
BackendUnavailable detail. Daemon Status retains classification 7, but omits
the new typed last_waiter_outcome for a requester that lacks the optional
capability. That omission does not remove the detail or change the stored outcome.
See Exit codes.
Exit codes
telex wait uses distinct exit codes so a caller can react without parsing
output. When a waiter writes --out-dir, the integer code is also written to
exit.code (last, as the completion marker); trust that artifact rather than a
detached task’s reported exit code.
| Exit | Meaning | What to do |
|---|---|---|
| 0 | Delivered | Read delivery.json (or message.json), ack and dedupe by id, then re-arm a fresh wait before longer processing. |
| 1 | Error | Unexpected failure. Inspect stderr and status.json. |
| 2 | Idle timeout | Nothing arrived before --timeout-ms. Re-arm if still attending. |
| 3 | Daemon gone / not running | Run telex attach (the spawning and recovery verb), then re-arm. |
| 4 | Daemon hung / no response after the --timeout-ms + --hang-ms watchdog | Re-arm, or restart the daemon if it repeats. |
| 5 | Presence ended | Non-destructive reap. A live session should attach and wait again. |
| 6 | Delivery quarantined | The daemon preserved but could not represent one historical delivery within the frame limit. Read and preserve status.json.quarantine (not message.json/delivery.json), do not ack it, then re-arm immediately; later deliveries can progress. |
| 7 | Backend unavailable | Transient backend recovery exhausted its 3-second budget. Check telex daemon status and backend connectivity, then re-arm after recovery; the station remains registered. |
Exit 7 requires a recovery-aware CLI and a daemon that advertises the optional
wait-backend-recovery capability. A legacy CLI receives BackendUnavailable
detail but still writes and exits 1. Daemon status may record terminal code
7 for that waiter; this is the daemon’s classification, not the legacy
process’s exit code. With an older daemon, a new CLI retains the older errors
and does not claim recovery support.
Transient query resets keep an eligible waiter armed. The original finite
--timeout-ms deadline still wins, including when it coincides with the recovery
deadline: exit 2, not 7. The backend budget is separate from
--reconnect-grace-ms, which handles a lost daemon IPC connection.
wait does not spawn a missing daemon; that is what attach is for. If a
replacement daemon already exists, a wait can reconnect and re-register during
its bounded reconnect grace.
For the authoritative flags of any command, run its --help (see the
CLI reference). The hidden telex copilot and telex daemon families
are documented by telex copilot --help and telex daemon --help.
Watcher runtime host
telex-watcher run (also the default when no subcommand is given) returns 1
when startup or runtime execution fails. With --json, stdout remains one
versioned error envelope. A runtime-failed envelope points to stderr for a
stage-labeled cause chain, including failures before any runtime record exists.
The diagnostic is redacted and capped at 4096 UTF-8 bytes, including the final
newline; [truncated] marks an omitted tail. It distinguishes registry open,
endpoint bind, runtime start/run, heartbeat and management-server failures.
Capture stderr alongside stdout when running the host under a supervisor.
Diagnostics redact secret-named environment values at any length and other
environment values of at least 8 UTF-8 bytes. Static stage labels are preserved.
Connection strings and credential-like text are omitted; review diagnostics
before sharing. They are not persisted startup records.
Specific errors such as registry-already-owned, transport errors and input
errors retain their existing codes and do not gain a runtime-failed diagnostic.
Global options
Global options apply to all subcommands and are set before the subcommand, for
example telex --backend prod inbox.
| Option | Purpose |
|---|---|
--backend <name> | Use a configured backend by name (or $TELEX_BACKEND). Defaults to the configured default backend, or an implicit default SQLite store. |
--db <path> | Override the SQLite path for this invocation (SQLite backends only; or $TELEX_DB). |
--address <addr> | Default address (or $TELEX_ADDRESS) for commands that act on one address; also a from fallback for send and reply. |
--json / --text | Output format. Defaults to JSON when stdout is not a TTY, text when interactive. |
Relevant environment variables
TELEX_SESSION_ID: stable session identity for daemon membership.TELEX_ADDRESS: default address.TELEX_BACKEND: default backend by name.TELEX_DB: SQLite path override.
Postgres connections are configured once as named backends with
telex backend add (see Backends), not through
per-call environment variables.
For the full, version-matched flag set of any command, run its --help; see the
CLI reference.
Design and internals
This guide covers using and operating telex. The design and implementation are documented separately in the repository:
- DESIGN.md: the working design.
- DECISIONS.md: the decision log (ADRs).
- ARCHITECTURE.md: the component architecture.
- daemon.md: the local exchange daemon.
- copilot-bridge-push.md: Copilot CLI push delivery.
The forward-looking discovery and dispatch proposals live in DISPATCH.md and EXTENSIONS.md.