Operating telex
The daemon lifecycle
The exchange auto-spawns on the first daemon-backed command; there is no manual start. Inspect the resolved backend and address projection with:
telex status --address <addr>
The daemon runs per user. Inspect and control it with the telex daemon family
(run telex daemon --help for the full set):
telex daemon status # daemon internals
telex daemon version # running daemon version
telex daemon stop --drain # drain in-flight work; returns once the daemon process has exited
Runtime state (the IPC socket and lease state) lives in a per-user runtime
directory: on Windows under %LOCALAPPDATA%\telex\run, on Unix a per-user socket
directory. The local message store is the SQLite file at ~/.telex/telex.db.
Stopping a station
station stop is the symmetric inverse of the attach and wait loop. It marks the
station non-attending, releases membership durably, and waits for tracked waiters
to exit:
telex station stop --address <addr>
After it returns, a later message to the address stays queued until a future attach or wait; it is not consumed by an orphaned waiter.
Teardown: which command to use
| Command | Effect |
|---|---|
telex detach --address <addr> | Drop this session’s membership of the address, non-destructively. The station and durable buffer remain. |
telex station stop --address <addr> | Mark the station non-attending, release membership durably, and wait for tracked waiters to exit. |
telex address retire --address <addr> | Retire the address so it drops from directory listings. |
telex daemon stop --drain | Stop the local exchange after draining in-flight work. |
telex copilot detach | Copilot push sessions: detach the address and remove the bridge files. |
None of these delete durable messages; a later attach or wait resumes against the retained buffer.
Upgrading the binary
Release installs use a versioned layout instead of overwriting the binary on
PATH in place. A stable launcher lives under the install root’s bin/, immutable
binaries live under versions/<tag>/, and current selects the version new
invocations use. Old in-flight processes keep running on their version while new
shells use the selected one.
<install-root>/
bin/telex(.exe)
versions/<tag>/telex(.exe)
current
previous
Upgrade, roll back, and inspect versions:
telex version --json
# Fetch, verify, and install the latest compatible public release:
telex upgrade
# Install a specific public release by tag:
telex upgrade --version vX.Y.Z
# Install a local/manual build (no download):
telex upgrade --from <path-to-telex-binary> --version vX.Y.Z
telex rollback
telex gc --dry-run
Without --from, telex upgrade discovers a GitHub release (the latest full release by
default, or --version <tag>), selects this platform’s asset, downloads the archive and its
.sha256 sidecar, and verifies the checksum before installing — then installs through the
same versioned layout as the local path. It is fail-closed: a missing or mismatched
checksum, a missing platform asset, an unsupported platform, an incompatible version, or a
network/rate-limit error aborts without changing current. Set GITHUB_TOKEN to raise the API
rate limit. Prebuilt binaries are published for Windows (x86_64, ARM64), Linux (x86_64), and
macOS (Apple Silicon, Intel); on other platforms install from source with
cargo install --git https://github.com/lossyrob/telex --features entra. If telex is already on
the resolved release it reports “already current” and does nothing. --force
may repair a missing target, but it never overwrites an existing immutable
version directory; publish a new version tag instead.
telex upgrade reads the downloaded (checksum-verified) binary’s own metadata by running it
once (telex --json version) before installing; in locked-down environments an OS quarantine
prompt (macOS Gatekeeper, Windows SmartScreen) on that step is the likely cause if an upgrade
stalls. The checksum verifies integrity, not authenticity — it protects against a corrupted
or truncated download, and the trust root is the GitHub repository the asset comes from.
telex upgrade and telex rollback drain the current local daemon before
switching current, unless --skip-drain is passed. Rollback refuses installed
versions whose manifest is incompatible with this build’s protocol/schema floor.
For the schema-3 upgrade, v0.1.2’s retained executable is not a downgrade path:
the new binary rejects its schema-2 manifest, and v0.1.2 itself refuses the
migrated store. There is no automatic reverse schema migration.
Use the old daemon’s owning binary for drain and let that process finish before
starting the selected new image. A draining acknowledgement alone does not
prove exit. Protocol 1.4 and 1.5 share a singleton major; avoid mixed explicit
binary paths and do not treat --skip-drain as ordinary upgrade coordination.
Reattach addresses still wanted after replacement; durable messages survive,
but this does not promise automatic push-intent restoration.
For a manual in-place replacement, drain and replace in this order:
telex station stop --address <addr>
telex daemon stop --drain
# replace the telex binary
telex attach --address <addr> --description "<s>"
telex wait --address <addr> --out-dir <dir>
If a session resumes without an armed waiter, recovery is durable: inspect
telex inbox --address <addr> and telex read --id <id>, then arm a fresh wait.
Auditing
Export messages and disposition history as JSON lines for provenance:
telex export --address <addr>
telex export --thread <id>
telex export --since <id>
Push recovery after a daemon replacement
A push-attended station (a Copilot bridge bound with telex copilot attach --copilot-bridge)
records a durable station intent: the exact desired push registration for that
(store, session, address) binding. When a daemon is replaced — telex upgrade,
telex daemon stop --drain, or a crash — the successor restores the registration by itself, with
no manual telex copilot resume.
Restoration is not “replay a record”. The successor restores a binding only after it has proved the
producer is alive: same user, matching executable, matching pid and process start time, matching
machine and boot, and an authenticated liveness probe whose nonce the producer must echo. A producer
that cannot be proved alive is never restored — messages stay durable and readable with
telex inbox, and the condition is reported rather than silent.
Published recovery bounds
Both bounds are measured from “a compatible successor daemon is running and the producer is live and verifiable”, and both are derived from the reconciler’s constants rather than asserted.
| Situation | Bound |
|---|---|
| Graceful drain or upgrade | ≤ 10 s (one 5 s reconcile tick + a 1 s probe + a 2 s validation/claim allowance) |
| Hard crash | liveness_window_secs() + 10 s (the crashed daemon never released its lease, so the successor waits for it to go stale) |
Both bounds are qualified. They apply only when the automatic enumeration and required reads complete, and to an intent that is:
- in a scope holding no more than 64 live intents (one pass budget), so it is attempted in the first pass after the trigger; and
- not currently in failure backoff, pull-waiter backoff, or quarantine.
They explicitly exclude: no successor daemon exists at all (start one with telex attach), a
backend outage, a competing fresh owner, and a producer too old to answer the liveness probe.
Larger scopes
A scope holding more than one pass budget has a queue delay, not a fixed recovery bound. Each pass
still returns within four seconds and advances a scope-specific cursor, but bounded discovery,
backoff, quarantine, and slow intents mean there is no truthful minimum progress count per pass.
Use successive reconcile reports and telex status to observe progress rather than deriving a
completion time from the scope size.
If a report is truncated or degraded, its count and over-cap indication are lower bounds, not an inventory or capacity-recovery promise. Automatic scans and GC cover records only when their enumeration and read opportunities complete; they do not promise fair stable-tail progress, a complete automatic GC, or exact capacity recovery.
For an exact inventory or eligible reclamation, use the explicitly offline path:
telex daemon stop --drain
# Stop other telex/Copilot processes that can write station intents.
telex --json daemon recover-intents # exact inventory
telex --json daemon recover-intents --gc # exact inventory, eligible GC, exact remaining count
The command refuses rather than guessing if a daemon is still reachable, its stopped state cannot be established, the intent scope is absent, or the scope is not positively recognized as supported local storage. Do not infer completeness from bounded reports.
At the 512-record write cap, existing records can still be updated or explicitly withdrawn. A live
record withdrawn to revoked continues to occupy its slot for the seven-day terminal TTL; daemon
GC frees it only after that TTL. Detach therefore does not free capacity immediately.
Waiting is not failing
telex --address <station> status reports a station-intent row with a state. The one that most
often surprises people is deferred_lease: after a crash, the predecessor’s epoch lease is not
stale yet, so the successor is waiting for it to expire, not failing. It retries at a fixed 5 s
cadence, never backs off exponentially, and never counts toward quarantine. next_attempt_ms says
when it will try again.
A genuine failure takes a jittered 5 s → 10 s → 20 s → … → 5 min ladder, and after ten consecutive
failures drops to an hourly cadence. Repairing the cause does not leave you waiting out the
ladder: any durable change to the record — the turn-boundary producer refresh after a bridge
reload, a copilot resume, a re-attach — clears it, and the next tick attempts the binding again.
Three conditions are named explicitly in status:
live_intent_missing_member— push is desired here but not currently armed.member_missing_live_producer— push is registered but the producer has gone quiet.intent_protocol_incompatible— this daemon cannot reconcile the recorded intent (schema skew, or a producer that predates the liveness probe).
What recovery does not do
- It never silently downgrades push to pull. If a plain
telex attachwould create a pull-only member over a live push intent, the daemon either reconciles it to push or refuses withIncompatible/PushIntentUnrecoverable. It never creates the pull-only member. This guarantee is scoped to the case with no live armed pull waiter: an armed waiter still wins, and the intent waits rather than forcing the conflict. - It never returns an explicitly detached station.
telex copilot detachandtelex station stopwrite a durable tombstone, and the reconciler honors it unconditionally. Explicit attach is the only way back. - It never crosses hosts. Intents are local files bound to this machine and this boot, so a shared Postgres store — or a synced home directory — cannot let one host restore another’s bridge.
- It never re-arms a station you reset.
telex station reset(andtelex daemon reset) withdraws the desired state as well as the membership: the affected intents are revoked, so the reconciler leaves the station idle.telex --address <station> copilot resumeis the way back. - It never arms an attach that was never registered. A running bridge is not by itself permission to turn a half-written attach into a live push binding. Finalizing one requires either a daemon that reports push armed for it right now, or the daemon’s own durable record that it armed the binding earlier.
- It never reports a push registration it cannot prove. For a station that has a station-intent
record, the daemon writes its durable “I armed this” proof before it installs the member, and
refuses the whole registration if that write fails. So there is no state in which push is armed
and nothing on disk says so — you either get a working, recoverable station or a typed refusal
(
PushIntentUnrecoverable) with no member created and any claimed lease released. A station that was already attended is left exactly as it was. A station with no intent record — a pull attach, or a plaintelex attach --on-deliver— has nothing to prove and is never refused by this; that includes on a host where the intent scope could not be created at all. - It never re-uses a torn-down station’s arming. Re-attaching an address you detached (or that a session end revoked) starts a fresh attach: it gets the full attach window to finish, and it has to be armed again by the daemon before anything can promote it. The old record’s proof is not carried over.
- It never treats “I could not read that” as “there is nothing there”. Every recovery decision
that turns on whether a station has a durable record — may this register commit, may this pull
attach downgrade push, has this station’s credential really been deleted — is decided from a
positive answer. If the intent scope, a station’s record, or a bridge credential exists but the
operating system will not report on it (a permissions change, a lock, a profile on a volume that
went away), telex refuses or waits rather than proceeding as if the station were new. You may see
a
PushIntentUnrecoverablerefusal you would not have seen before; the alternative was a registration that reported success and left nothing recoverable behind.
Recovering from a bridge reload
extensions_reload, /clear, and an extension-host restart all give the Copilot bridge a new
process. The daemon proves a producer by executable, pid, and process start time before it sends
anything, so a reloaded bridge no longer matches what the intent recorded, and status shows
producer_identity_mismatch.
This heals on its own: the next Copilot turn boundary re-records the running bridge’s identity — after proving it answers a liveness probe — and the following reconcile tick restores the binding. It does not need a daemon that already has the station armed, which is the case that used to deadlock: a reload followed by a daemon replacement leaves a successor that cannot create the member because the identity is stale, and cannot refresh the identity if refreshing required the member.
If a session ends before a turn boundary happens, telex --address <station> copilot resume does
the same thing immediately.
Before you drain
telex daemon stop --drain, telex upgrade, and telex rollback print a pre-drain intent report:
station intents recoverable 2 pending 0 degraded 0 incompatible 0 unknown 0
recoverable is what a successor is expected to restore automatically. pending is a push attach
that has not finalized yet — it is not restored automatically; it finalizes at the next Copilot
turn boundary (after extensions_reload), and only then becomes recoverable. degraded and
incompatible need action: run telex --address <station> copilot resume after the switch.
Rolling back to a binary that predates this feature returns those stations to manual resume, and the
rollback output warns about it.
The report reads the durable records as well as the daemon’s cached view, so a station you attached
seconds ago is counted correctly, and a station whose recorded producer identity was just repaired
(the usual case after extensions_reload) is counted as recoverable rather than carrying forward the
failure the repair fixed.
telex upgrade and telex rollback then drive one reconciliation pass on the successor by invoking
the binary the switch just selected (telex daemon reconcile on that binary). That indirection is
required, not cosmetic: the daemon only accepts IPC from a client whose executable matches its own,
so a pass requested from the pre-switch process would either spawn the wrong binary or be refused.
The successor summary distinguishes a pass that ran and restored nothing from one that never ran.
The station_intent_reconcile object in --json output always names the binary it is about
(successor_binary, null only for --no-switch), including when the step was skipped or the
successor failed. A successor that ran but could not complete a pass exits non-zero and reports
{"reconciled": false, "error": ...}; that structured reason is carried through verbatim (bounded)
rather than replaced with a generic rejection. A successor CLI child that overruns the bound is
killed and reaped before upgrade/rollback returns; a daemon it successfully spawned is a
separate detached service governed by the normal daemon lifecycle. The result records timed_out,
terminated, reaped, and the direct child’s pid.
Destructive testing
Station intents are namespaced by a hash of your user identity, your canonicalized config root,
and the protocol major. To experiment destructively without touching your real bindings, point
TELEX_CONFIG (and TELEX_HOME) at a scratch directory: the scratch daemon gets its own intent
scope, and nothing you do there can restore or revoke a station in your normal scope.
Recovering from a lost daemon
A wait that finds no daemon exits with a distinct code (see
Exit codes). Run telex attach (the spawning and
recovery verb) and re-arm the wait. If a replacement daemon already exists, a
wait can reconnect during its bounded reconnect grace.
Turn-end and resume reconciliation
For turn-end guards or resume reconciliation, use
telex station status --session <id> to get a compact JSON projection of the
session’s attended addresses, waiter counts, station health, and pending
unconsumed counts.
Uninstall and cleanup
- Stop the daemon:
telex daemon stop --drain. - Remove local state: delete
~/.telex/(the SQLite store and config). - Remove the Copilot plugin, if installed:
copilot plugin uninstall telex@telex. - For a Postgres backend, drop the telex schema in the database if it is no longer needed.