Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Telex

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).

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 interrupt to fyi, 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 example repo: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:

  • attach registers the session and address, then exits.
  • wait blocks 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:

Recipientdelivery_roledelivered_torequires disposition
node:worker (to)tonode:workerset when the sender passes --requires-disposition
node:lead (cc)ccnode:leadno

--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 (uses az login, or --entra-cred managed on a devbox or VM with a managed identity). Requires a build with the entra feature, 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 --db or $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 detach or 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 (--session or $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 wait client blocked for a single delivery.
  • Attention: the urgency of a message: interrupt, next-checkpoint, background, or fyi. 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_id and parent_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 /experimental if needed to expose extensions_reload.
  • App startup discovery without a live loader: provision the bridge, restart the App session, then run copilot resume and verify liveness as described below.
  • Restart is unacceptable or liveness does not verify: use pull fallback. Neither an absent extensions_reload tool nor CLI-only /experimental is 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 mutable bin launcher. copilot stall-detect --help must 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 target so 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_guard from 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 only telex-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:

FieldMeaning
schemaVersion1; unknown versions are refused.
configRootEffective absolute Telex home.
telexExecutableExact trusted selected versioned Telex path.
watcherExecutableTrusted retained Watcher path.
watcherRegistryAbsolute registry for this installation.
installRootTrusted installed-current Telex root used by the Application Client.
backendProfileExplicit existing profile name.
senderDistinct send-only address, not a secret.
pollIntervalSecondsDefault watch cadence, 1-86400; default 30.
environmentNamesRequired launch/detector environment names, never values.
telexBuildIdSelected Telex’s reported build identity.
watcherSha256Hash of the retained Watcher binary.
verificationToolSha256SHA-256 of the owner-only installation.ps1 beside this record.
processGuardSha256Linux: 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.

StateMeaning
activeExpected to make progress; only active members count toward a stall.
parkedWaiting on an identified wakeup: a watch, App automation, background shell or background agent. Put the wakeup in reason.
operator-heldWaiting for an operator decision or gate. Identify it in reason.
doneAssigned 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.

ValueSource
telex executableAbsolute trusted installed binary path in the installation record.
watcherExecutableAbsolute trusted telex-watcher path used to run this Watcher.
watcherRegistryAbsolute registry path used by this runtime and by the registration command below.
backendProfileExisting configured profile holding the members’ Telex addresses; use it for both the registration and detector parameter.
SenderRecord’s sender, distinct from member receive addresses; Watcher attaches it send-only on registration.
pollIntervalSecondsRecord’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’s send_session_message to resume a not-loaded member. For an attended Telex member, send is also suitable; include a meaningful --subject and 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 has degraded: true and 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:

  1. Arm one wait (optionally with --min-attention interrupt while focused).
  2. It blocks until one message, exits, and the runtime wakes the agent.
  3. Read the result, ack, dedupe by id, then arm a fresh wait before 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 --schema to 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

CommandEffect
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 --drainStop the local exchange after draining in-flight work.
telex copilot detachCopilot 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.

SituationBound
Graceful drain or upgrade≤ 10 s (one 5 s reconcile tick + a 1 s probe + a 2 s validation/claim allowance)
Hard crashliveness_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 attach would create a pull-only member over a live push intent, the daemon either reconciles it to push or refuses with Incompatible / 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 detach and telex station stop write 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 (and telex 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 resume is 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 plain telex 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 PushIntentUnrecoverable refusal 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

  1. Stop the daemon: telex daemon stop --drain.
  2. Remove local state: delete ~/.telex/ (the SQLite store and config).
  3. Remove the Copilot plugin, if installed: copilot plugin uninstall telex@telex.
  4. 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:

StateMeaningWhat to do
deferred_leaseWaiting for a crashed predecessor’s lease to go stale. Not an error.Wait; next_attempt_ms says when it retries.
deferred_pull_waiterA live telex wait owns the address; pull wins.Stop the waiter (telex station stop) if you want push back.
legacy_producerThe 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.
unverifiableThe 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.
insecureA permissions check failed on the bridge directory or credential file.Fix the permissions (owner-only), then resume.
incompatibleThis daemon cannot reconcile the recorded intent (version skew).telex --address <addr> copilot resume.
quarantinedTen consecutive genuine failures; now retrying hourly.Fix the underlying cause, then resume to reset it.
revokedThe 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 resume for 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 telex binary (telex 0.3.0 (build f17bab00a95f29551ad95a155e3d4258fb9d1eb8)) by docs/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

FieldMeaning
idMessage id. Use it with ack, handle, read, and for dedupe.
thread_idRoot message id of the thread.
parent_idParent message id for a reply, otherwise null.
fromSender address; a reply routes here.
toThe address the message was sent to.
primary_toThe primary recipient address.
delivered_toThe recipient address this delivery is for.
delivery_roleto or cc: this recipient’s role in the message.
ccCC (observer) addresses.
kindMessage kind label (default note).
attentioninterrupt, next-checkpoint, background, or fyi.
requires_dispositionWhether the sender marked the message as requiring disposition.
requires_disposition_for_current_recipientThe same, scoped to this recipient.
subjectSubject, if any.
bodyMessage body.
metadataOpaque message metadata, if any.
sent_at_ms, buffered_at_msUnix millisecond timestamps.
delivery_idExact 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_versionStore state fence captured with delivery selection.
lease_epochThe 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 in quarantine and removes any stale message.json/delivery.json; it is not a delivered-message artifact. Exit 7 reports exhausted backend recovery as backend-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.

ExitMeaningWhat to do
0DeliveredRead delivery.json (or message.json), ack and dedupe by id, then re-arm a fresh wait before longer processing.
1ErrorUnexpected failure. Inspect stderr and status.json.
2Idle timeoutNothing arrived before --timeout-ms. Re-arm if still attending.
3Daemon gone / not runningRun telex attach (the spawning and recovery verb), then re-arm.
4Daemon hung / no response after the --timeout-ms + --hang-ms watchdogRe-arm, or restart the daemon if it repeats.
5Presence endedNon-destructive reap. A live session should attach and wait again.
6Delivery quarantinedThe 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.
7Backend unavailableTransient 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.

OptionPurpose
--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 / --textOutput 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:

The forward-looking discovery and dispatch proposals live in DISPATCH.md and EXTENSIONS.md.