# Telegram Multi-Instance Bus Architecture

## Status

Implemented for paired bots where Telegram exposes private-chat Threaded Mode. Classic/private-chat mode remains the default whenever Telegram threads are unavailable, and live Telegram client smoke remains the release gate for client-visible thread UX.

This document uses **thread** as the canonical product term because Telegram clients present the tabbed UI as threads. The Bot API calls the underlying primitive a `Topic` / `ForumTopic`, but project language follows user-perceived client reality rather than API naming. Use **topic** only when discussing Bot API method names, service-message names, or transport-level evidence. User/operator UX and product docs should say thread.

This document supersedes the narrower API-topic framing. Telegram threads are a UI/routing substrate, but the deeper design problem is multi-instance coordination: one bot token has one Telegram API update bus, while multiple live Pi agent instances may want to expose their own Telegram workspace through that bus.

Named bot profiles are optional and orthogonal to this design. The ordinary unnamed profile keeps the existing setup, connect, lock, state, log, Unix socket, and Windows named-pipe paths. When operators configure additional profiles, each profile is an independent bot runtime with its own lock key, observable state, thread ownership, and leader/follower IPC endpoints; leader election and follower routing never cross profile boundaries.

## Problem

Classic `pi-telegram` mode binds one private Telegram DM to one live Pi instance through one bot token and one singleton polling owner. The lock currently answers: "which Pi instance owns Telegram control/polling?"

That model is safe, but it leaves concurrency on the table:

- Only one live Pi instance can receive Telegram updates for a bot token.
- Moving `/telegram-connect` changes the active Telegram control owner instead of letting several instances coexist.
- Multiple projects, tmux panes, remote workers, or long-running Pi instances require separate bot tokens or manual ownership switching.
- A thread workspace is only useful if it routes to a live agent instance, not to a dead session record that can no longer answer.

Telegram itself has one relevant constraint: for a bot token, `getUpdates` must be owned by one poller. The architecture must embrace that by electing one local Telegram bus leader and routing work to follower instances.

## Goal

Support a Threaded Mode multi-instance runtime where:

```text
one bot token -> one local Pi organism -> ephemeral bus leader -> many live Pi instances -> many Telegram targets
```

The leader is a temporary transport role, not the ontological owner of the system. A terminal-visible Pi instance may become the initial leader because it is the operator's visible harness, while additional terminal-visible Pi instances can explicitly register as followers through `/telegram-connect` and one of them can later take over bus leadership if the leader exits.

A practical Telegram UI can then use threads:

```text
one private bot chat -> one thread per live Pi instance
```

The operator experience:

1. Start one Pi instance; it becomes the Telegram bus leader and polls Telegram.
2. Start another Pi instance with `pi-telegram` and run `/telegram-connect`; the follower registers instead of fighting for `getUpdates`. Use `/telegram-connect as=Navigator`, or `/telegram-connect work as=Navigator` for a named profile, to name a fresh Workspace Thread. On later process reopen, a profile-scoped exact-`cwd` Workspace with a remembered Thread reconnects as a follower automatically while the leader remains live.
3. The leader provisions or reuses a Telegram thread target for that instance. Use `/name Navigator` in that Telegram Thread later to change its display name through leader-authorized persistence.
4. Messages, callbacks, reactions, files, voice, previews, and menus in that target route to the owning live Pi instance.
5. If the leader exits, remaining followers elect/promote a new leader, which resumes polling and keeps the registered target routes alive where possible.

## Non-goals

- Do not let more than one process call `getUpdates` for the same bot token.
- Do not treat Telegram as a raw terminal, PTY, or process supervisor.
- Do not couple the first design to Pi sessions if live instance ownership is the better runtime truth.
- Do not expose arbitrary group participants to prompts, controls, or artifacts.
- Do not require Threaded Mode for classic private-chat users; classic private-chat mode remains valid and should not receive slot/thread-name guidance.
- Do not implement leader election through unsafe lock stealing without heartbeats or stale-owner checks.

## Terms

- `Telegram bus`: The singleton local capability to poll Telegram updates and send Telegram API calls for one bot token.
- `Leader`: The live Pi instance that currently owns the Telegram bus and calls `getUpdates`; this is an ephemeral role transferable after stale heartbeat detection.
- `Follower`: A live Pi instance that wants Telegram presence but routes Telegram API access through the leader.
- `Bus lifecycle`: Transient recovery state only. Stable identity is the bus role (`leader` / `follower`); lifecycle surfaces exceptional handoff states such as `electing`, not duplicate roles with labels like `leader-active`.
- `Agent instance`: A running Pi process/session with its own extension state, queue, active turn, model, tools, and lifecycle hooks.
- `Telegram target`: The concrete Telegram destination for an instance, represented as `{ chatId, threadId? }`.
- `Thread target`: A Telegram UI thread destination, represented as `{ chatId, threadId: message_thread_id }` over Bot API topic transport.
- `Classic target`: The existing private-chat target, represented as `{ chatId: allowedUserId }`.

## Core Shift

In Threaded Mode, the lock means "this instance is the current Telegram bus leader" rather than "this instance is the only usable Telegram extension".

Classic ownership meaning:

```text
tmp/telegram/owners.json / <profile-slot> -> polling/control owner
```

Threaded Mode meaning:

```text
tmp/telegram/owners.json / <profile-slot> -> bus leader identity + heartbeat
```

Followers do not poll. They register with the leader and receive routed inbound updates from it. Followers still own their local queue, active-turn state, previews, final delivery planning, model switches, and Pi lifecycle. The leader owns only Telegram transport and update fanout. Pi session replacement (`new`) changes follower agent context, not bus membership: a registered follower preserves its registration and refreshes the live context instead of disconnecting. A new follower process also attempts a restore-only registration at session startup when the selected profile's local state contains an exact-`cwd` Workspace binding. The Telegram bus belongs to the local set of cooperating visible Pi instances rather than to the first terminal session forever: if the visible terminal leader exits, a live registered follower can take over leadership.

A follower may route Bot API work or capability checks only after authenticated registration; an unregistered process that does not own the direct transport lock remains transport-passive. Follower request settlement remains owned by the existing local IPC operation budget rather than being inferred from Bot API method names; followers never invoke `getUpdates`.

## Target Abstraction

The bridge uses a first-class target abstraction:

```ts
type TelegramTarget = {
  chatId: number;
  threadId?: number;
};
```

Private-chat mode uses `{ chatId: allowedUserId }`.

Threaded Mode uses `{ chatId: privateChatId, threadId: messageThreadId }` over Bot API topic transport.

Every session/instance-scoped path carries or preserves a target:

- Inbound update routing.
- Queue item identity.
- Active turn state.
- Preview draft state.
- Final replies and reply deduplication.
- Voice and attachment uploads.
- Menu/status/settings/queue/section messages.
- Button callback ownership.
- Reactions.
- Typing/record-voice chat actions.
- Direct local/TUI Telegram delivery.

## Binding Model

A thread maps to a currently running Pi instance, not to a historical session file. `instanceId` is the live routing owner, while `instanceProfileKey` is a reuse hint for reclaiming a compatible current thread across process replacement.

```text
runtime owner: live instance id
reuse hint: cwd/profile/user-chosen alias/session id when available
```

This keeps thread liveness honest: if an instance is registered, there is a live owner to answer. It also avoids coupling `/new`, compaction, and session-file internals to Telegram routing. Restarted projects may still reclaim previous current bindings through profile-aware reuse when that does not conflict with live ownership.

## Instance Identity

A registered instance exposes:

```json
{
  "instanceId": "uuid-or-runtime-id",
  "pid": 12345,
  "cwd": "/home/user/project",
  "startedAt": "2026-05-20T10:00:00.000Z",
  "owner": { "kind": "leader", "cwd": "/home/user/project" },
  "threadName": "<valid-instance-identity>",
  "target": { "chatId": 123456789, "threadId": 42 },
  "status": "idle|active|queued|compacting|disconnected",
  "lastHeartbeatAt": "2026-05-20T10:00:05.000Z"
}
```

`instanceId` is liveness identity. `owner` is explicit current binding identity (`leader`, `manual-follower`, or `pending-topic`). Internal compatibility keys may be derived, but `state.json` should not hide ownership direction inside legacy string keys. `threadName` is the stable named-mode and restoration identity; `displayTitle` is the separately acknowledged user-facing projection. Fresh threads receive a compact palette name from the assigned slot, while Letters and Directories project other titles without replacing the saved name.

## Approved Next Contract: Directory Names And Reclaimable Slots

Status: approved design with a locally tested pure selection policy in `lib/workspace-slots.ts` and profile-isolated display preference persistence/default resolution in `lib/config.ts`. Workspace claims now reserve global letters before provisioning and preserve legacy binding keys. An exact claim assigns the first free letter to a missing-slot binding or the selected member of a duplicate-slot set, but persistence waits for successful target recovery; unresolved duplicates block unrelated fresh allocation. Sticky suffix metadata and acknowledged `displayTitle` persist in Workspace bindings. `lib/thread-display.ts` provides the three-mode projection plus serialized title reconciliation wired into leader startup and follower registration. Heartbeat ACKs carry acknowledged display titles to followers and the current-thread/TUI projection uses them without changing restoration identity. Settings now exposes Letters, Names (default), and Directories; follower changes use the capability-gated leader-owned setting path. Live bot chooser/notice labels and cross-instance agent-target resolution use acknowledged titles without granting routing authority. Confirmed owner cleanup now persists the first proven `inactiveSinceMs` transition atomically with target invalidation; successful active provisioning clears it. Pressure selection, intents, mocked execution, and recovery are implemented. A 2/2 same-model independent post-fix quorum cleared the admission-composition blocker at 0.96 confidence per reviewer, but production deletion remains disconnected by this release scope; `BACKLOG.md` owns operator smoke and release readiness.

The pure policy distinguishes a free letter, a proposed pressure-reclamation victim, and protected/invalid capacity. Its caller must supply a validated profile-wide snapshot, reservations, proven inactivity start, and explicit protection classification; duplicate legacy letters block selection. The policy performs no filesystem or Telegram operations and does not establish liveness or deletion authority. It proposes a victim only when every profile-wide letter is occupied or reserved; elapsed time alone never triggers retirement.

### Cross-Process Admission Fence

Production retirement requires a durable profile-scoped reader/writer ledger owned by a dedicated `workspace-admission` domain. The ledger uses the existing atomic file-transaction primitive but does not hold a filesystem mutex across asynchronous work. It stores versioned admission leases and at most one destructive fence per exact Workspace target. Every record carries bot/profile identity, exact or conservative scope, a collision-resistant retry-stable operation id, operation kind, process id plus process-birth identity, and acquisition time. A destructive fence additionally carries the exact retirement-intent id, binding key, slot, target, and leader epoch. Missing, malformed, unreadable, ambiguously published, or unverifiable-owner state fails closed.

- Journal append, target-scoped Bot API issuance, provisioning, registration, and binding/target replacement must first transact against the ledger. They prune only leases whose process-birth owner is proven dead, reject a matching destructive fence, durably add an admission lease, perform the asynchronous or durable operation, then remove the exact lease transactionally. Exact-target leases conflict with that target; chat-only API work uses chat-wide scope; undecodable journal admission uses profile-wide scope. Duplicate release and process-crash recovery are idempotent, while an unknown release outcome remains protective.
- Retirement acquires its fence transactionally only after exact profile/epoch/intent validation and only when no exact, chat-wide, or profile-wide lease conflicts. Once the fence is durable, new matching admission is rejected. After the final protection recheck, the ledger durably advances `fenced` → `deletion-issued` and returns the sole deletion permit; a retry after an ambiguous publication or crash observes the issued phase but receives no second permit. Exact confirmed deletion or already-absence advances to `commit-ready`, and only durable binding plus intent removal permits fence completion. A protection change before permit issuance releases the fence; an unknown deletion, ambiguous commit, authority loss after issuance, or crash retains it.
- A successor may adopt a retained fence only together with the exact durable retirement intent and the existing profile/binding/leader-epoch adoption checks. It preserves original request/acquisition time and phase, replacing only the fence owner/epoch. An adopted `deletion-issued` fence must resolve through exact already-absence or retained unknown outcome and can never issue another deletion request. A different intent never clears or steals the fence. Startup restore-only and fresh capacity paths cannot bypass it; they return a truthful temporary-unavailable result without allocation or eviction.
- The isolated ledger and process regressions now cover admission-before-fence, fence-before-admission, chat/profile-wide conflicts, concurrent contenders, proven-dead lease cleanup, unverifiable-owner protection, malformed-state rejection, exact release idempotence, ambiguous publication recovery, one-shot deletion permits, and successor fence adoption. Production journal stores resolve an admission adapter that conservatively derives batch scopes, holds leases through atomic publication, and releases partial acquisitions on rejection. The production common JSON/multipart client likewise resolves its adapter per current bot/profile: target/chat scopes span all transport retries through settlement, malformed declared targets use profile scope, targetless methods bypass the ledger, and release failure cannot replay a settled request. One production Workspace operation runtime resolves admission before a shared local gate. Leader assembly uses it around chat-wide leader provisioning and profile-wide follower provisioning/registration, disconnect/dead cleanup, leader/follower rename, and display preference/reconciliation; Sync topic lifecycle and routing restore/reclaim use the same profile-wide boundary, so fence rejection precedes store reads and mutation. The isolated retirement executor now requires the matching durable fence, closes admission before its final protection recheck, persists `deletion-issued` before calling an executor-only deletion port with the sole permit, retains unknown outcomes without reissue, and commits only from confirmed `commit-ready`; successor recovery is process-tested. Retained fences now project their uppercase slot into the Thread store's optional external reservations, blocking generic allocation, Workspace claims, and reclamation selection even after binding commit; malformed or unreadable evidence fails closed. The profile runtime resolves a separate `workspace-admission[.<profile>].json`, stores only token-hash authority, preserves named-profile identity across switching, permits changed-token rebind only from a provably empty ledger, and rejects rotation while leases or a fence remain. The admission side is production-wired through leader/follower mutations, topic lifecycle, reroute restore/reclaim, manual disconnect/session-restart cleanup, and exact stale-target recovery; journal-evidence pruning also requires exact-target admission through durable publication. Disconnect/restart cleanup acquires profile admission before the shared leader gate and holds it across cleanup intent, API, binding, and durable settlement, so both retained fence phases reject it before state access. The common async admission runner and API adapter reject concurrent reuse of one live operation ID before lease acquisition, while clearing that process-local guard after acquisition failure or complete settlement so retry-stable durable recovery remains separate. A 2/2 same-model post-fix quorum independently verified the complete production mutation map, delayed reconciliation fix, fence-first and lease-held schedules, and absence of nested-gate deadlock at 0.96 confidence per reviewer. Destructive retirement remains disconnected by release scope; the PASS clears this blocker but does not authorize live deletion.
- Production mutation coverage is intentionally caller-owned rather than embedded in generic Thread-store primitives.
- The shared profile-wide Workspace operation runtime covers observed topic lifecycle, the complete unbound-target and reroute restore/reclaim handlers, manual disconnect/session-restart cleanup, and leader assembly provisioning, registration, cleanup, rename, and display operations. Detached post-provision reconciliation reacquires a fresh profile lease through the same runtime before delayed API/store work. Every admission lease precedes one process-local gate and lasts through API plus durable settlement.
- Journal append and common JSON/multipart transport use their own exact/chat/profile leases through publication or settlement. Follower promotion/target replacement, exact stale-target recovery, and journal-evidence pruning likewise hold dedicated admission through their complete mutation. These operations cannot overlap retirement because fence acquisition conflicts with the retained lease even when they do not need the shared local queue.
- Retirement intent preparation/adoption/execution owns its exact gate-and-ledger protocol but remains absent from production composition. Status projection and polling/routing bot-mode writes change only diagnostic or capability metadata; they neither create nor remove Thread/Workspace authority and serialize through the store's local persistence queue.
- Production callers of reservation, provision/cleanup intent, target-record, Workspace-binding, and display mutators are contained by the owners above. The generic store remains policy-free for isolated tests and domain composition; calling a primitive directly is not production retirement authority.
- Workspace identity remains the selected bot profile plus normalized exact full `cwd`; directory basenames are presentation, never routing keys. Each concurrent binding receives one profile-wide unique lowercase slot from `a` through `z`, persisted on the wire/store as its uppercase equivalent, independent of directory and leader/follower role. This replaces the two competing displayed allocation identities; immutable legacy `instanceSlot` and `bindingKey` remain recovery keys, not another displayed pool.
- Automatic display mode is a bot-profile setting shared by Telegram Thread titles and Pi TUI status. The selector offers `letters` then `directories`; absent or invalid values resolve to `letters`. Letters show `A`, `B`, `C`; directories use the directory basename. A durable per-Workspace `manualThreadName`, set through Telegram `/name`, overrides either automatic projection until explicitly reset. Bare `/name` immediately enters exact-target rename input: cancel is always available, while reset is shown only when a manual override exists; no intermediate action-selection step exists. Legacy persisted `names` configuration is read compatibly but resolves to Letters; it is no longer an effective or offered automatic choice. Store the automatic preference at `profiles.<name>.threadDisplayMode`, not as a process-local choice or a setting shared by unrelated bots.
- In directory mode, a singleton may hide its suffix; once another retained binding for that Workspace exists, all its labels expose their globally assigned suffixes (for example `extensions_a`, `skills_b`, `extensions_c`). Persist the decision to show suffixes so later closure does not make names oscillate. Equal basenames from different paths require a deterministic parent-path qualifier. Preserve the existing `threadName` as generated/recovery identity while automatic modes are selected. New manual names live only in `manualThreadName`; do not guess manual provenance from a legacy name or palette membership. `showSlotSuffix: true` is sticky binding metadata; sibling creation and legacy multi-binding loads expose it, and later upserts that omit it cannot reset it. Telegram `/name Name` changes the manual override and displayed title only for its exact originating target. Leader and follower requests carry that target through final generation/binding checks, so replacement cannot redirect a stale dialog mutation. Reset uses the same negotiated `workspace-thread-rename-v1` capability and exact follower generation; the leader computes the current automatic projection, edits the exact target, clears only `manualThreadName`, and persists before acknowledging. Follower metadata refresh preserves an acknowledged display title only while target and registration generation stay unchanged. Named-profile setup preserves the latest saved automatic preference even if another instance changes it while the token form is open.
- Fresh provisioning projects the candidate together with retained bindings and sends the active mode's title in `createForumTopic`. The exact targeted provision retains creation-title evidence until the Workspace commit publishes the binding and consumes that evidence together. Recovery preserves it even when a starting record already exists; an untargeted or unknown creation never authorizes a title commit. Proven deletion removes exact-target pending creation evidence, including when no current record was committed. Older contradictory pending/deleted snapshots settle that evidence durably before replacement; closed targets and pending cleanup block recovery until reconciliation, rather than becoming active again. The same exact-target check protects follower reconnect/carried-target shortcuts and the final Workspace commit, before creation evidence can be consumed. A matching carried pending target resumes through the provisioner that owns its reserved slot and acknowledged title instead of allocating that slot again. The shared provision-commit helper first commits the claim, then applies the acknowledged title with exact-binding comparison, preserving generic stale-title rejection on target replacement. Switching display mode changes projection only: preserve Thread ID, binding identity, slot, queue ownership, and routing. `displayTitle` records a successful Telegram edit independently of `threadName`, survives same-target registration updates, and is cleared on target replacement. The title reconciler captures profile, mode, leader epoch, and exact live-binding authority before each edit, rechecks after ACK and persistence, and skips dormant bindings. Failed persistence retains acknowledged dirty metadata for a later persist without repeating that API edit; a late or unknown ACK never commits a title to a replacement binding. Keep the stable palette/manual name separate from the current display title so switching back does not generate a different name. The leader owns Telegram title edits and acknowledged follower/TUI convergence, with generation/profile fencing and truthful partial-failure recovery. Successful registration ACKs optionally carry the acknowledged `displayTitle` with the exact target and registration generation, making it available before the initial status refresh. Heartbeats carry later title changes; stale generations cannot update display state. Connected notices use acknowledged titles while runtime `threadName` remains the stable restoration identity. Live bot chooser/notice labels, prompt attribution, and cross-instance agent-target name selection use the same acknowledged projection, but candidate liveness and the captured numeric `{chatId, threadId}` remain authoritative. Ambiguous projected names fail closed. Older peers can ignore the optional field, and no separate polling connection or follower snapshot-read loop is needed; do not expose a setting control that merely stores a preference without updating its promised surfaces.
- Reopening a retained inactive binding restores its slot and name without taking leadership. Explicit connection from the same directory may allocate a second binding. Startup restore remains restore-only: it must not evict another Workspace or allocate a fresh binding merely because all remembered bindings are owned. Explicit connection already skips a live peer's migrated binding instead of attempting to adopt it; that admission rule is independent of the display redesign.
- Prefer free letters in deterministic order. Only under full slot exhaustion may retirement choose the eligible binding with the oldest proven inactivity start, not necessarily slot `a` after `z`. Time alone never retires a binding. If every slot is protected or its ownership is unverifiable, reject new allocation with a truthful capacity explanation; never evict a live owner or silently expand into `aa`.
- Inactivity begins at a verified transition to no live owner; an idle but connected process is active. Do not derive it from last user message, heartbeat silence alone, snapshot-write time, file age, or any elapsed-time threshold. Legacy records without sufficient evidence have no `inactiveSinceMs` and remain ineligible rather than inheriting an invented old timestamp. Malformed inactivity values are ignored without dropping the binding. Repeated inactive observations retain the first timestamp for pressure ordering; stale same-target upserts cannot postpone it, while target replacement or successful active provisioning clears it.
- Eligibility excludes live or unverifiable owners, exact transient claims, accepted pending/active work, unresolved delivery authority, and pending provisioning, handoff, or cleanup operations. The store can capture one candidate snapshot from bindings, live records, process-local exact claims, reservations, and persisted provision/cleanup intents, but requires the caller to supply separate tri-state evidence for external live ownership, accepted work, and delivery authority. Only three explicit `clear` results plus valid continuous inactivity produce `eligible`; any protected or unknown dimension fails closed. Before future deletion, `workspace-retirement` selects the oldest eligible pressure victim only after all slots are occupied or reserved. It captures the profile and leader epoch, rechecks protection, then persists one durable Workspace retirement intent containing the complete expected binding, pressure reason, leader epoch, and request time. The intent itself protects that binding from competing selection; only the exact captured intent may be excluded during a fenced recheck. Duplicate intent ids, bindings, targets, or letters fail closed. Each new Workspace binding durably accumulates the registration/profile keys used to route its historical follower journals and marks that set complete. An existing legacy binding may learn current keys but cannot claim its unknown historical set is complete without separate discovery proof. Accepted-work policy treats an exact local queued/active target or any unsettled entry in a binding-specific follower journal as protected. Shared leader-journal entries protect only a decoded exact target; undecodable relevant entries, unreadable sources, and incomplete source enumeration remain unknown. The read-only evidence capture resolves the shared leader journal and every recorded follower key; missing resolvers, read errors, or an incomplete legacy key set make coverage unknown. A complete readable capture may prune a known empty follower-journal key only when a separate process/writer check is explicitly clear and the exact binding, profile, and leader epoch remain current; nonempty, unreadable, writable, or unknown keys remain. For incomplete legacy metadata, `journal` can discover canonical profile-exact follower snapshot and segment roots. Matching paths with unexpected/unreadable shape make discovery incomplete; successfully read discovered journals are target-scoped evidence because their one-way filename hash cannot recover the historical routing key. Legacy completeness remains unset, so later retirement repeats discovery rather than forgetting that historical domain. Live composition includes decoded process-birth proof, registry liveness, queue/journal state, and delivery-authority discovery.
- Reclamation deletes the exact inactive Telegram Thread and therefore removes its history; this is distinct from dropping a local label. The leader owns a durable, exact-binding-and-epoch-fenced retirement intent. The store validates and persists this contract, and preparation resumes an exact current-epoch intent after persistence failure or reload. The isolated executor requires both the caller-owned exclusive gate and durable admission ledger. It acquires or exactly adopts the matching fence, rechecks protection after admissions close, persists `deletion-issued`, and invokes an executor-only `deleteForumTopic` port with the sole non-retried permit. Unknown/rejected results keep the binding, intent, slot, and issued fence; retries and successors cannot issue again and require exact already-absence proof. Success or confirmed absence advances to `commit-ready`, then a store-owned commit hides the free slot while persistence is in flight, restores in-memory authority on known failure, and removes binding+intent before exact fence completion. One failure-resilient Workspace operation runtime owns the shared gate for topic lifecycle, reroute restore/reclaim, leader/follower provisioning, delayed post-provision reconciliation, disconnect and confirmed-dead cleanup, rename, and display reconciliation; registration cannot enter the live registry until its gated provisioning completes. Timer-owned mutation never inherits expired registration authority: it reacquires profile admission before cleanup and remains blocked without store/API effects behind either retained fence phase. The executor consumes that runtime's exposed gate rather than nesting another lock. A successor leader cannot execute the old epoch directly. It may durably adopt exactly one intent into its current epoch only when the profile, complete binding snapshot, and fresh protection recheck still match; known persistence failure restores the old intent and request time. Leader composition now exposes external protection from exact live registry targets, active/queued local targets, shared and historical follower journals, and profile-exact discovery for incomplete legacy history. An accepted item without an exact target for the candidate chat remains unknown. Exact JSON and multipart Bot API operations are counted at the shared direct-client boundary through final settlement; a message-scoped edit/delete without thread identity conservatively protects all bindings in its chat. Historical follower owner keys are decoded before process-birth liveness checks. Exact durable intents block matching claims, activation, title/journal commits, and binding upserts, preventing normal mutation from making their snapshots stale. Closed-but-existing topics do not count as confirmed absence. The former process-local TOCTOU is closed in the isolated ledger/executor and production journal/API/mutation/allocation composition, including delayed post-provision cleanup. Independent post-fix review passed, but production pressure retry and leader execution remain disconnected by release scope; capacity still fails closed instead of evicting. Reuse changes only the letter assignment: routing remains exact numeric target authority, so traffic for the retired target cannot reach the replacement. Recheck eligibility before Telegram mutation and serialize against re-registration. Release the letter only after confirmed deletion or exact already-absent evidence and a successful durable retirement commit. The current confirmed cleanup path records inactivity only after its exact deletion/already-absent result; an unconfirmed API outcome records no inactivity. Unknown API outcomes, failed persistence, owner replacement, and interrupted cleanup retain the reservation and recovery evidence, preventing reassignment or deletion of a replacement target.
- A retired binding no longer promises its former name/letter on reopen; the Workspace may be provisioned anew. Retained message ownership and journal evidence must never route old work through a recycled letter: full binding generation and target remain mandatory independently of display labels.
- Implement and validate using isolated stores and mocked Telegram APIs. Do not delete live Threads, migrate live journals/locks manually, publish, or restart operator instances as part of local preparation. Operator smoke on disposable Threads is a separate gate.

## Leader Election

Leader election is heartbeat-gated and lock-backed. The polling owner checks exact lock ownership every second, refreshes its durable lease every two seconds, and becomes stale after eight seconds; the serialized expected-owner transaction remains the final cross-platform election authority.

1. On startup, read the Telegram lock.
2. If no leader exists, acquire leadership and start polling.
3. If a live leader exists, restore a remembered profile-scoped exact-`cwd` follower binding automatically; otherwise wait for explicit `/telegram-connect` rather than provisioning a new Thread from startup alone.
4. If the leader heartbeat is stale, attempt an atomic leadership takeover; ordinary `/telegram-connect` on a follower is not a leadership move while the leader is live.
5. Heartbeat acknowledgements carry the authenticated live follower-slot roster. If several followers detect stale leadership, the lowest observed live slot attempts promotion immediately; higher slots defer one bounded election grace and re-check the lock. Atomic compare/write acquisition remains the final ownership authority, and a missing lower-slot follower cannot block a higher survivor beyond that grace.

Followers first try to re-register after leader reload or unknown-heartbeat responses, carrying their last known target, slot, and thread name so the new leader can reuse the same binding. Follower Bot API calls already admitted by the active Pi turn wait for that bounded re-registration and capture its new exact generation before entering transport; they do not fail merely because recovery temporarily cleared local registration, and they never replay after an ambiguous transport commit. After the grace window followers promote only when the exact observed leader lease has become stale or inactive; an unavailable IPC endpoint never authorizes replacing a still-live owner. If the exact carried target is absent from persisted bindings, the leader first runs the same synchronous visibility probe: success recovers it instead of creating another Telegram thread, explicit stale evidence provisions a replacement, and ambiguous failure rejects registration. An ambiguous absent-target probe persists only non-routable `probe-required` restoration evidence, so targetless retries, successor follower processes with the same exact Workspace claim, and leader reloads probe that exact target again instead of activating it or provisioning a speculative replacement. A carried slot survives only when that slot remains free. Every successful reuse refreshes the binding timestamp. The leader never restores persisted followers into the live registry speculatively. Absent follower records remain durable restart hints until explicit stale, deleted, offline, or reconciliation evidence invalidates them; only fresh authenticated registration creates live routing authority. This preserves real thread bindings through reload and process-absence gaps without allowing historical records or competing pollers to masquerade as live state.

## Leader/Follower Communication

### Protocol identity and compatibility

The local wire contract has protocol version `1`, independent from the npm package version. Follower registration and the leader acknowledgement carry `{ protocolVersion, runtimeBuild, capabilities }`. Capability names are canonical, unique, and sorted. A leader rejects missing or mismatched protocol identity before provisioning a target or publishing the follower into live routing; a strict follower likewise rejects an acknowledgement without compatible leader identity. Different package builds remain compatible when their protocol versions agree. `durable-follower-admission-v1` gates source forwarding, while `queue-handoff-v1` independently gates live semantic queue transfer; every participant in a routed handoff must advertise it. `workspace-follower-auto-connect-v1` gates restore-only startup admission. `thread-display-mode-v1` gates exact-generation follower display-setting requests and Letters/Directories require compatible connected followers; returning to Names allows legacy peers. Registration checks the current display-mode requirement before provisioning and again before live publication. The leader serializes config persistence and title application. `workspace-thread-rename-v1` independently gates follower rename requests whose exact registration generation is checked before the leader mutates Telegram and persists the Workspace binding.

Negotiated identities remain on the live follower registry and appear in `/telegram-status --debug` plus the observational state snapshot. The Threaded Mode capability monitor owns one in-flight probe across lifecycle generations: stop/restart invalidates a late read, and a replacement monitor waits for the previous request to settle instead of creating overlapping transport transitions. Durable follower admission is authorized only when both peers advertise `durable-follower-admission-v1`, never inferred from package version: a capable runtime rejects missing support before provisioning, inbound routing, or election-roster eligibility. Authentication and exact registration generation remain mandatory independently of protocol compatibility. `follower.register` is the explicit bootstrap request and may provision; capability-gated `follower.restoreWorkspace` is startup-only and may claim, probe, or replace a remembered binding but returns `workspace-binding-unavailable` without allocating when no binding or claim-fenced legacy exact-`cwd` record exists. Both carry a fresh generation; every later request is exact-generation-fenced against the live registry entry. `bus.ack` is response-only and is rejected if submitted as a server request. Leader forwarding never synthesizes authority for an unknown recipient and preserves the follower's exact durable receipt end to end.

Foreign update forwarding returns an explicit `accepted`, `retryable`, or `terminal-rejected` settlement. Acceptance requires an acknowledgement for the exact request whose receipt contains the expected stable `deliveryId` and source `update_id`; a callback error popup never substitutes for that receipt. Missing or negative acknowledgements, stale registrations, absent follower context, binding rejection, journal admission failure, and missing or mismatched receipts all retain the leader source. Message, edited-message, reaction, and callback paths share this contract.

The delivery id excludes the replaceable runtime instance and registration generation: it derives from envelope kind, source `update_id`, and the stable manual-follower binding. Stored message ownership carries that binding and may rebind to the current authenticated registration after follower replacement, preserving one retry identity while still fencing each attempt by the current generation. A lost acknowledgement retries with the same delivery identity and becomes accepted only when the exact durable receipt returns. Journal deduplication covers retained entries, not lifetime completion history: after ordinary receipt completion removes an entry, cursorless admission can accept that source again. Stable delivery IDs therefore do not by themselves establish exactly-once execution. Owned local IPC evidence reproduces the gap: a proxy drops actual ACK bytes; real forwarding, authenticated receiving, paired admission, journals and admission workers leave the leader source in retry-wait after follower completion. The next attempt reaches an injected authorized callback handler twice in total, then both journals settle. A queued-message variant likewise re-admits the source under a fresh acquisition after a real worker receipt handoff. This proves repeated handler/queue-admission invocation in the composed local pipeline, not actual Pi queue dispatch, live Telegram effects or model execution. Delivery custody, including role-dependent local replay, must be reconciled before a consume-once guarantee can be claimed; the [design counterexamples](./architecture.md#busjournal-design-acceptance) rule out treating origin disappearance as proof of a specific handoff.

Transport leadership does not own already-queued semantics. Each queued journal receipt binds the acquiring runtime instance, OS process birth, session generation, and acquisition. A replacement leader or follower process sees another live process's receipt as foreign and cannot replay or settle it; the original process may complete its local Pi queue after transport moves. Startup preserves foreign and legacy unowned receipts. Before a replacement admission worker starts, tri-state pid/process-birth proof: an absent PID or mismatched stable Linux/macOS birth identity permits recovery, while a live matching owner stays `alive` and Windows or inaccessible birth metadata stays `unverifiable`; both non-dead outcomes preserve the receipt may transactionally recover a dead owner's complete receipt to pending, after which replacement replay creates fresh queue authority. Registration publishes the replacement's exact pid/process-birth identity first; a concurrent recovery that observes it returns `owner-alive`, while live or unverifiable owners remain untouched.

Live-process handoff combines journal CAS with authenticated bounded bus payloads. A donor-generated one-time token is hashed together with the exact receipt, donor acquisition, and recipient runtime/process/session identity. Offering retains donor ownership but freezes donor completion/discard and dead-owner recovery. Prompt payloads serialize queue data; control payloads serialize only stable `status`/`model` identity and rebuild closures at the recipient. Leader and follower receivers require exact live donor/recipient registration generations, reject malformed/oversized payloads, stage one complete receipt idempotently, accept its journal authority, and acknowledge only that exact receipt plus newly minted owner. Receipts name their source journal binding, while the donor derives the recipient follower-journal binding from the authenticated stable profile. The recipient accepts only that matching active lifecycle. Production advertises `queue-handoff-v1` with this exact role/path composition; legacy-unbound or unavailable bindings fail closed.

As part of staging, the exact recipient presents the token and atomically receives a fresh acquisition carrying the handoff digest; donor settlement is then stale, and the donor trusts only an ACK carrying that accepted owner. Registration carries recipient process-birth and session generation so journal authority matches the live runtime exactly. The coordinator contract shares one ordering across leader→follower and follower→follower paths: offer, route bounded payload, require the exact staged-and-accepted receipt-and-owner ACK, remove donor work, then publish recipient dispatch readiness. Negative or mismatched acknowledgement cancels the still-unaccepted offer and retains donor work. A lost acknowledgement after recipient acceptance cannot revoke the accepted owner; cancellation fails closed and donor memory remains frozen for exact accepted-owner reconciliation. Repeated exact acceptance is idempotent; a different token cannot claim the accepted receipt. Queue authority has no elapsed-time lease: only exact handoff, owner action, or transaction-rechecked PID/process-birth death proof may move it.

Implemented transport:

### Local IPC endpoint with bounded native paths

Leader opens a local Node `net` endpoint: a Unix-domain socket under the agent temp directory on Unix-like platforms, or a deterministic Windows named pipe (`\\.\pipe\pi-telegram-...`) on native Windows. A filesystem-style endpoint supplied by legacy state or a transport harness normalizes deterministically to the same Windows pipe boundary before listen/connect. On Unix, an endpoint that would exceed conservative domain-socket pathname limits maps to a private user-scoped, hash-derived path under the OS temp directory; ordinary agent paths remain under `tmp/telegram`. On Unix, each server listens on a private generation socket and atomically publishes the stable profile path as a relative symlink; delayed shutdown closes only its private path and cannot remove a replacement generation's link. Followers register, heartbeat, and exchange routed events. The transport boundary owns endpoint derivation, socket-vs-pipe detection, bounded operation-aware retry policy, timeout/transient IPC error classification, endpoint reachability probes, and request-scoped transport events. Follower registration uses a longer registration-specific response timeout than ordinary heartbeat/forwarding calls because the leader may need to provision a Telegram thread before it can return the assigned target; timing out that handshake leaves a visible tab with no follower heartbeat. Keep this handshake to the true critical path: create/reuse the target, persist the live binding, and return it. Connected notices and replaced-thread reconciliation cleanup are non-critical and should run after registration so a follower becomes routable before Telegram client/server UI convergence work finishes.

Pros:

- Natural request/response for sending Telegram API calls through the leader.
- Can route inbound updates to followers while preserving one poller.
- Good fit for live process membership.

Cons:

- Adds IPC lifecycle and security concerns.
- Cross-machine workers need tunneling or a different transport.

Alternative transports such as file-backed mailboxes or an external daemon remain out of the current product boundary. Local IPC is the default internal bus while the public design stays compatible with a future daemon if deployment needs outgrow one host.

## 0.45.0 Disposable Operator Acceptance

Local evidence is not this operator gate: the initial-title/pending-recovery fixes have passing persisted-store/caller regressions and two successful same-model independent PASS reviews of the final recovery guards (`run:telegram-follower-pending-guard-verification`). Both raw reports and terminal branch evidence were inspected. Review scope covers generic recovery, follower shortcuts, and final Workspace settlement; broader cross-session/persistence conclusions remain bounded by the inspected paths and existing tests, not exhaustive fault injection. Model diversity, live-client behavior, and native Windows evidence are absent.

Run this only with an operator-approved disposable bot/profile and disposable Threads. Do not reuse production journals, locks, admission ledgers, accepted work, or retirement intents. Record the package commit/build, Pi version, OS, Telegram client/version, profile name, exact working directories, and test Thread IDs before starting.

1. Start one Pi in directory A, enable private-chat Threaded Mode, and run `/telegram-connect`. Confirm leader ownership, one Thread created directly with the selected display-mode title, the same title in the connected notice and initial Pi status, and no generated-name flash or second polling owner.
2. Start a Pi in directory B and connect it. Confirm follower registration rather than takeover, a distinct globally ordered slot/name, the selected title in creation/notice/initial status without waiting for a heartbeat, exact prompt/reply routing, and no traffic in the leader Thread.
3. Explicitly connect a second Pi from directory A. Confirm it receives a separate binding/slot without copying the first target. Restart each follower independently and confirm restore-only startup reuses its remembered exact-directory binding without allocating a new Thread.
4. Rename the leader and a follower from their respective Telegram Threads with `/name Name`. Confirm each manual override converges in Telegram, Pi status, choosers, notices, and agent-target labels under both Letters and Directories. Reset each override to the current automatic projection; confirm target IDs never change and same-basename directory suffixes remain sticky after a sibling disconnects.
5. From leader and follower Threads, exercise ordinary prompts, callback buttons, one file, and one voice response. Confirm each result remains reply-anchored to the originating numeric Thread and no upload, notice, or final is duplicated.
6. From `All`, create an unbound disposable Thread. Test forward and Replace/restore separately. Confirm accepted content reaches only the selected live instance, Restore carries the selected identity onto the source target, and only the confirmed old/chooser targets are deleted.
7. Replace a follower session, then stop the leader and allow follower promotion. Confirm profile, target, slot, saved name, acknowledged title, accepted queue work, and routing survive without a new Thread or competing poller.
8. With cleanup enabled, run confirmed `/telegram-disconnect` and one session-restart cleanup on disposable Threads. Confirm cleanup intent/API/store settlement completes before transport release; on an induced transient failure, the exact intent/slot remains pending rather than deleting or reusing another target.
9. Leave pressure retirement untouched: this release intentionally keeps automatic retirement disconnected. Confirm capacity failure is truthful and no Thread is deleted or slot reused merely from pressure, elapsed time, or heartbeat silence.
10. Capture `/telegram-status --debug`, relevant redacted runtime logs, screenshots of Telegram titles/routes, and the disposable state snapshots after each role/mode transition. Stop immediately on wrong-target delivery, duplicate publication, unexpected deletion, slot reuse, stale-title authority, split-brain polling, or loss of accepted work.

Native Windows must additionally complete the named-pipe and process-lifecycle checklist below. A PASS requires every applicable step with captured client/platform evidence; local tests and mocked Telegram do not substitute for this acceptance.

## Native Windows Smoke Plan

Native Windows support should not require WSL. The baseline transport uses Windows named pipes for leader/follower IPC, but live verification still needs an operator with a native Windows Pi install.

Manual smoke checklist:

1. With Threaded Mode disabled, connect one Pi, attempt a second classic connection, confirm the ownership-handoff prompt, complete takeover, and verify the displaced process loses Bot API mutation authority without losing accepted local queue state.
2. Stop all owners, corrupt only a disposable `owners.json`/state snapshot, reconnect, and verify guarded stale recovery quarantines the damaged file while preserving `telegram.json`; inspect the replacement snapshots to confirm complete atomic JSON rather than partial writes.
3. Enable Telegram private-chat Threaded Mode for the paired bot.
4. Start Pi in one Windows terminal and run `/telegram-connect`; verify it becomes the leader, gets a named Telegram thread, and publishes a native `\\.\pipe\...` endpoint in explicit diagnostics.
5. Start Pi in a second Windows terminal and run `/telegram-connect`; verify it registers as follower rather than offering takeover, creates/uses its assigned thread, terminal status shows `<ThreadName> Follower` while idle, and a follower prompt flips it to `<ThreadName> Active` while work is running.
6. From the follower thread, send a prompt that requests inline buttons; tap a button and verify the follow-up prompt queues in the follower instance.
7. From the follower thread, request a voice reply and/or attachment; verify upload routes through the leader transport into the follower thread.
8. Force a live Threaded Mode capability downgrade and verify the current leader keeps classic polling while followers disconnect instead of attempting takeover; restore capability and reconnect explicitly.
9. With `🧹 Thread cleanup` enabled (default), quit the follower Pi normally without an explicit disconnect; verify graceful shutdown deletes its current tab through the leader before local suspension. Repeat with a double `Ctrl+C`; if Pi misses the graceful envelope, verify stale-heartbeat recovery deletes the same exact tab only after the OS confirms that follower PID has exited. Disable the setting, quit another follower normally or abruptly, and verify its tab remains as a restart hint. Reconnect, run `/telegram-disconnect`, confirm the prompt, and verify the leader confirms deletion before local polling stops.
10. With `🧹 Thread cleanup` enabled, quit the leader normally and verify it deletes only its own tab before releasing transport; a remaining follower may then promote without recreating the deleted leader tab. If deletion is interrupted after intent persistence, start or promote a successor and verify it completes the exact pending cleanup before publishing its follower endpoint or provisioning its own tab; when the successor has the same stable leader profile and its old binding remains active, it must adopt that binding first and cancel the superseded cleanup without calling Telegram close, delete, or create APIs.
11. Generate enough diagnostics to cross the rotation threshold, reload the leader, and verify `logs.jsonl`/`logs._prev.jsonl` preserve complete ordered records. Confirm ordinary status hides raw pipe internals while explicit debug diagnostics show the active `pipe` endpoint and classified request failures.

If any step fails, capture `telegram-status --debug`, `tmp/telegram/state.json`, `tmp/telegram/logs.jsonl`, and, after a reload, `tmp/telegram/logs._prev.jsonl`. Debug status prints local leader/follower endpoints with their active transport kind (`pipe` or `socket`), while the runtime log records request-scoped transport failures with envelope kind, request id, retry attempt, endpoint, and classified IPC error. Reloads preserve the prior JSONL log as `logs._prev.jsonl` so the evidence that caused the reload is not immediately overwritten.

### Native Windows Assumption Audit

Current portability audit:

- Local bus transport: adapted. Unix-like platforms use filesystem socket paths; native Windows uses named pipes so no POSIX socket pathname is required.
- Bus endpoint permissions: Unix sockets/directories use `chmod`; Windows named-pipe endpoints skip POSIX chmod/unlink path handling because the pipe is not a filesystem node.
- Ownership/config/state/temp files: path construction uses `path.join`/`path.resolve` under the Pi agent directory. File permission calls remain best-effort private-mode hardening; native Windows may emulate POSIX modes, so broad Windows ACL auditing is outside this extension's current local-bus baseline.
- Process liveness: lock ownership uses `process.kill(pid, 0)`, which Node supports on Windows for existence checks. Cross-user permission failures are treated as alive, matching Unix semantics.
- Shell/provider commands: outbound handler command templates remain operator-configured and platform-dependent; Threaded Mode bus portability does not guarantee every configured STT/TTS/shell provider is Windows-native.
- Manual follower identity: process ids are local liveness hints, paired with OS process-birth metadata where available rather than treated as cross-machine identifiers. Linux uses `/proc` start ticks and macOS uses the parent process start time. Fallback generation strings identify a runtime but are not independent death proofs: Windows or inaccessible process-birth metadata is `unverifiable` while the PID remains live. A fresh authenticated session handoff carries the previous runtime identity and exact target through initial registration, allowing fallback identities to migrate without provisioning another thread.

Remaining risk is live native Windows behavior: named-pipe creation/connect timing, antivirus/firewall/ACL interference, and provider command availability need operator smoke evidence.

## Telegram Thread UX

In Telegram private-chat Threaded Mode:

- The private bot chat is a tabbed instance workspace, not a classic `General + threads` forum.
- `All` is an aggregate view, not a process launcher. Explicit new instances use live Pi follower registration: the operator starts Pi in a terminal and runs `/telegram-connect`; owner-created empty threads are observed but not treated as a Pi instance until the user chooses a route or restore action.
- The leader proactively creates or reclaims its own thread on startup/activation when Threaded Mode is available, so the visible leader has the same two-way binding as followers.
- **Unbound thread detection**: when the owner writes in an unknown `message_thread_id`, the bridge checks effective Threaded Mode state. If the current leader has no active bound thread, that new thread is reclaimed for the leader and the prompt is served locally. Otherwise the bridge preserves prompts and commands in that Telegram thread and shows the complete forward plus replace/restore chooser. Successful forward deletes the chooser and confirmed temporary source through `thread-reconciler`; successful restore always deletes the chooser, rebinds the source, and deletes only the selected instance's replaced old thread. Partial foreign batch delivery retries only remaining messages; incomplete thread or chooser deletion retains a cleanup-only retry control without redispatching routed content or leaving an expired visible button.
- Unknown later threads and threadless prompt messages are not silently routed to the leader and never launch hidden Pi processes. The default and only operator path for a new visible instance is starting a visible second Pi process and letting it register as follower through `/telegram-connect`. Returning to the same normalized working-directory Workspace reclaims its persisted Thread after an authenticated process claim and required visibility proof; the dormant binding alone is never routing authority. Explicit stale/deleted observations invalidate that restoration hint before a fresh Thread is provisioned.
- Thread lifecycle service messages (`forum_topic_created`, `forum_topic_closed`, `forum_topic_reopened`, deletion/stale send errors) update observations and binding state. Closed/deleted leader or follower threads can be reclaimed or recreated deliberately. Current same-process targets remain quiet across reload; reclaiming a dormant Workspace target sends the truthful connected notice as its visibility probe, marks a proven stale target deleted, and provisions a fresh Thread. Unknown `forum_topic_created` service events are observation-only and are not destructive cleanup proof.
- Bidirectional binding is a core UX requirement, not an implementation detail: Pi instances actively advertise/remember their thread identity, while the bot observes Telegram-client thread state and reflects it back into instance state. This keeps the system responsive, recognizable, and controllable even when the operator closes tabs, writes from `All`, or a follower later becomes leader.

In Telegram private-chat Threaded Mode:

- The private bot DM becomes the operator's multi-instance dashboard.
- Each live bound instance gets one visible thread.
- Each instance has a durable single-letter slot (`A`-`Z`) assigned by the extension and a bridge-authored `threadName`.
- New slots advance through the alphabet and wrap after `Z` only to a free slot, intentionally capping concurrent visible instances to the alphabet without duplicating occupied letters. The compact `bot.lastSlot` cursor persists while its binding remains live or recoverable, including true `Z → A` wraparound. Pending provisions, reservations, and retained restart bindings occupy their slots until explicit stale/deleted evidence invalidates them.
- Workspace identity is profile-scoped normalized exact `cwd`, independent from process role and lifetime. Readable bounded directory keys are collision-verified against the exact path. Workspace claims reserve a profile-wide letter before asynchronous Bot API work, across both same-directory and different-directory instances; dormant bindings and transient claims reserve their letters. The pure policy uses lowercase `a`–`z`, while the existing transport `slot` field carries the same letter in uppercase. Legacy `instanceSlot` suffixes remain immutable binding-key components, not a second display-slot allocator. Fresh Workspace allocation prefers the first free letter and fails closed at protected capacity; pressure retirement is not yet connected. Claims are transient; successful target/name/slot bindings persist as restart hints. On upgrade, an exact-`cwd` leader record or a manual-follower record matching the current/previous authenticated process is claim-fenced into the first compatible Workspace slot, preserving its target, name, and ordering slot without creating a replacement Thread. After a fully cold restart with several dormant bindings for the same directory and no exact session handoff, the bindings are reclaimed in new claim/opening order; process labels or prior terminal opening order do not identify a particular lowercase slot.
- A follower that later becomes leader keeps its Workspace target, uppercase slot, and thread name; leadership changes are transport role changes, not identity resets. Immediately after follower promotion succeeds, the new leader retains a short-lived process-local handoff bound to its exact Telegram profile owner key and refreshes it before session replacement. The replacement session consumes it only after acquiring leader authority, converts any surviving manual-follower record for that target into the current leader binding, persists the Workspace target/slot/name under its exact claim, and only then runs ordinary topic provisioning; neither handoff nor dormant binding is live routing authority.
- Instance-thread names are short and recognizable. Default provisioning chooses an unused baked 4-6 letter single-word Latin name, excluding current bindings, dormant Workspace hints, and in-flight provisions before creating the Telegram Thread. If the assigned slot's five-name palette is occupied, selection continues through the remaining palettes rather than duplicating an identity. The uppercase slot remains separate ordering metadata. Bare slot titles are fallback/legacy state only. Existing human-named Threads are preserved across reloads and leadership changes; a concurrent process in the same Workspace receives the next Workspace suffix and a distinct Thread.
- A thread-local `/start` opens that instance's menu.
- Prompts typed in a thread route to the owning instance.
- Replies, previews, files, voice, and buttons stay in that thread.
- Queue controls and reactions affect only that instance target.
- Telegram's native `…typing` indicator for real agent work is sent to that instance thread and mirrored to `All`; `All` is the aggregate surface and should show activity when any bound instance is running a Telegram turn, local prompt, or autonomous continuation. Terminal `Active` remains Telegram-turn-specific. Startup/connect/reload/recovery must not send activity by themselves.
- Generic heartbeat pruning remains silent and preserves the thread as a restart hint. With cleanup enabled, a later exact-PID death confirmation may delete it without posting an `Instance offline` notice. Confirmed deletion removes live routing authority but retains the Workspace binding's friendly name and uppercase ordering slot; a later authenticated reopen probes the old target, replaces it only on exact stale/deleted evidence, and carries that name and slot onto the new Thread.
- If the same binding identity returns, authenticated registration can reclaim the thread after the required visibility proof.

## Bot API Evidence For Private-Chat Threaded Mode

The local Bot API reference in [`../.agents/skills/telegram-bot/api.md`](../.agents/skills/telegram-bot/api.md) supports private bot Threaded Mode through bot capability fields and thread-target transport:

- `User` returned by `getMe` can include `has_topics_enabled` and `allows_users_to_create_topics`; these are the private-chat Threaded Mode capability fields and are the startup/runtime probe source for this extension.
- `createForumTopic` works in a private chat with a user and returns a `ForumTopic`, so the returned `message_thread_id` is persistable as an instance thread target.
- Private-thread management uses Bot API methods such as `editForumTopic`, `closeForumTopic`, `reopenForumTopic`, `deleteForumTopic`, and related unpin methods. Thread-unavailable errors from these methods are degradation evidence when Threaded Mode is disabled or unavailable for the bot.
- `Message` exposes `message_thread_id` and `is_topic_message`; an incoming private-chat message with `message_thread_id` is a live Threaded Mode observation and can trigger progressive upgrade.
- Topic lifecycle service messages include `forum_topic_created`, `forum_topic_edited`, `forum_topic_closed`, `forum_topic_reopened`, `general_forum_topic_hidden`, and `general_forum_topic_unhidden`.
- `message_thread_id` is supported by the send/upload methods the bridge uses or may need: `sendMessage`, `sendPhoto`, `sendDocument`, `sendVoice`, `sendMediaGroup`, `sendSticker`, `sendRichMessage`, `sendMessageDraft`, `sendRichMessageDraft`, and `sendChatAction`.

Non-goal: group detection is not the control-plane model for this extension. Threaded Mode lives in the private bot chat, so startup and runtime switching must not depend on group chat metadata or group admin capability fields.

Remaining live-verification points:

- Callback query messages can be `InaccessibleMessage` without `message_thread_id`; reroute controls use stored source/chooser identity, and ordinary generated buttons retain their message-ownership routing. Client-visible Restore and cleanup still require live smoke evidence.
- Whether message-reaction updates carry thread identity in the current Bot API shape. The reference exposes chat id and message id for reactions, so routing may need stored message ownership.
- Live client evidence now covers the probe-confirmed single-artifact multipart Rich final through both direct leader and registered follower transport: an assigned follower Telegram turn produced one reply-anchored PNG plus final text without a duplicate upload or notice. Deterministic bus tests additionally cover target-scoped multipart authorization, envelope preservation, and replacement-generation fencing.

Implemented behavior stays evidence-gated: when Telegram client or Bot API behavior differs from the contract above, capture a minimized fixture or documented client caveat before changing routing.

## Inbound Routing

The leader polls all updates for the bot token. It classifies each update into a target key:

```text
targetKey = chatId + ':' + (threadId ?? 'private')
```

Then it dispatches:

- If target belongs to the leader instance, handle locally.
- If target belongs to a follower, forward the normalized update/event to that follower.
- If target is unknown but authorized and setup allows provisioning, offer or create a binding.
- If target is unknown or unauthorized, ignore or send a safe denial.

Follower instances receive normalized events, not raw Telegram transport internals where possible. The follower still runs the same queue/routing logic, but Telegram API calls go back through the leader transport port.

## Outbound Routing

Followers do not call Telegram Bot API directly for routed Telegram work. Instead, they call a leader-owned transport port:

Explicit `telegram_message(..., thread)` delivery also uses the bus as an agent-message plane. The leader resolves the case-insensitive name or numeric id against live leader/follower registrations, rejects ambiguous, stale, same-instance, cross-chat, and replayed requests, then coordinates visible Bot API delivery with one source-attributed synthetic turn routed through the destination instance's ordinary queue. The destination sees `[telegram|thread:<destination>|from-thread:<source>]`, not an impersonated user message. Generation fencing and request-ledger deduplication apply to both resolution and routing.

```text
follower reply/preview/upload/chat-action/download/callback-answer -> leader IPC -> Telegram API
```

This preserves one API bus and one set of rate-limit/retry diagnostics. The current local bus routes JSON calls, multipart uploads, chat actions, message deletes, callback/guest answers, and file downloads through the leader when a follower is registered.

Every outbound request carries its target. The leader injects `message_thread_id` when `target.threadId` exists.

## Leader/Follower Capability Parity Matrix

Threaded Mode should make follower threads behave like normal Telegram instance surfaces, with the leader acting only as transport owner. Any feature in the matrix below that works for the leader must either work for followers or have an explicit documented exception.

| Surface                          | Leader behavior                                                                                                                                                                                                                                       | Follower requirement                                                                                                                                                                                                                                            | Routing/ownership invariant                                                                                                                                                                                                                         | Regression evidence                                                                                                       |
| --- | --- | --- | --- | --- |
| Prompt intake                    | Thread prompt queues locally                                                                                                                                                                                                                          | Thread prompt is forwarded and queued by the owning follower                                                                                                                                                                                                    | Target ownership routes by `{ chatId, threadId }` before local handling                                                                                                                                                                             | Routing tests for foreign target message forwarding                                                                       |
| Queued-message removal reactions | 👎/👻/💔/💩/🗑 marks a pending prompt/media turn for deletion when it reaches dispatch                                                                                                                                                                            | Same reaction on a queued follower prompt marks that follower's pending turn for deletion before model dispatch                                                                                                                                                                  | When the leader forwards a prompt to a follower, it records `chatId/messageId -> follower instance` because Bot API reaction updates expose chat/message but not thread id                                                                          | Update runtime regression records forwarded message ownership and forwards the later reaction                             |
| Queue priority reactions         | 👍/⚡/❤/🕊/🔥 prioritizes queued prompts                                                                                                                                                                                                               | Same reactions prioritize follower queued prompts                                                                                                                                                                                                               | Reaction forwarding uses stored message ownership, then follower mutates its local queue                                                                                                                                                            | Reaction mutation tests plus forwarded-reaction coverage                                                                  |
| Message edits                    | Edits update matching queued prompt text                                                                                                                                                                                                              | Edits in a follower thread update that follower's queued prompt                                                                                                                                                                                                 | Message target ownership forwards edits to the owning instance; stored message ownership is the fallback when Telegram edit payloads omit thread id                                                                                                 | Update routing tests for foreign target and message-owned edited-message forwarding                                       |
| Callbacks/buttons/menus          | Callback handled by the owning instance/menu state                                                                                                                                                                                                    | Follower callbacks are forwarded to the owning follower; follower menu sends/edits/deletes route through leader transport                                                                                                                                       | Leader records ownership for follower-sent Bot API messages so callbacks can route by message id even when Telegram omits thread id; Bot API edit/delete lacks thread id, so follower bus allows validated same-chat message operations             | Callback forwarding, generated-button target, bus follower-sent ownership, and bus edit/delete allowlist tests            |
| Replies/finals                   | Final replies land in the same thread                                                                                                                                                                                                                 | Follower finals go through leader transport into follower thread                                                                                                                                                                                                | Outbound calls carry target and inject `message_thread_id`                                                                                                                                                                                          | Reply delivery and bus API tests                                                                                          |
| Previews/Rich Drafts             | Draft previews use the active thread target                                                                                                                                                                                                           | Follower previews use the same native draft lifecycle through the leader                                                                                                                                                                                        | Preview transport preserves target and draft id                                                                                                                                                                                                     | Preview thread-target tests                                                                                               |
| Attachments/voice                | Files and voice upload in the instance thread                                                                                                                                                                                                         | Follower uploads route through leader multipart transport                                                                                                                                                                                                       | Multipart calls are target-scoped and follower-authorized                                                                                                                                                                                           | Bus allowlist and outbound delivery tests                                                                                 |
| Native activity status           | `sendChatAction(typing)` renders Telegram's native `…typing` indicator in the assigned thread and mirrors aggregate `All` for every agent run, including local and autonomous work; terminal status changes to `active` only for Telegram-owned turns | Followers route one thread action and one aggregate action through leader transport for any agent run while retaining their stable terminal role during non-Telegram work                                                                                       | Agent lifecycle targets the active Telegram turn when present and otherwise the instance binding; one keyed loop avoids duplicate aggregate sends/rate-limit pressure                                                                               | Agent-start binding, typing-loop, target-routing, and terminal-status regressions                                         |
| Leader election / promotion      | Current leader keeps its thread across reload                                                                                                                                                                                                         | A promoted follower keeps its existing thread, slot, and name when elected and after later reload                                                                                                                                                               | Promotion converts the current follower binding into the leader profile before forced lock acquisition, so leader startup reuses it instead of provisioning a new thread                                                                            | Follower heartbeat recovery passes binding snapshot into promotion; own-topic provisioner reuses promoted bindings        |
| `/start` command/menu bootstrap  | Registers visible bot commands and opens the menu                                                                                                                                                                                                     | Follower `/start` can refresh the bot command menu through the leader and open its local menu without warnings                                                                                                                                                  | Bot command registration is a validated global Bot API call allowed through trusted follower bus transport                                                                                                                                          | Bus allowlist regression for `setMyCommands`                                                                              |
| Follower reconnect               | Existing leader binding is reused only when still usable                                                                                                                                                                                              | Same-process `/new` or `/reload` suspends the old follower socket/context and automatically re-registers the new session to the exact prior target; explicit reconnect to a genuinely closed/stale Telegram tab still recreates a visible thread before success | A short-lived handoff carries the assigned target across session replacement, and the leader transfers that binding to the new runtime instance id by stable manual-follower identity; stale Bot API errors remain the proof for fresh provisioning | Session handoff/refresh, leader binding-transfer, persisted leader-reload reuse, and stale-target replacement regressions |
| Unbound thread reroute/restore   | Prompt- and command-created temporary threads expose forward plus replace/restore; forward deletes the chooser/source, while restore deletes the chooser, rebinds the source, and removes only the replaced old thread                                  | Same complete chooser exposes all currently live bus leader/follower targets; restore is offered from concrete unbound threads, not historical snapshots                                                                                                                 | Live bus roster plus active target bindings define the selectable set; history/state snapshots are not authority                                                                                                                                    | Routing chooser regressions for live target filtering and restore rows                                                    |
| Status/menu diagnostics          | Status reflects leader role, queue, and target                                                                                                                                                                                                        | Follower status reflects follower role, thread name, queue, and bus health                                                                                                                                                                                      | Status is local runtime truth plus bus registration state, not leader queue state                                                                                                                                                                   | Status and bus diagnostics tests                                                                                          |

## Queue And State Scoping

Each instance owns its own queue and active turn state. The leader does not become a central queue scheduler for all agents; that would be a separate daemon-mode architecture.

Target-scoped state requirements:

- Queue item identity includes target plus source message id.
- Reply deduplication is keyed by target, not just chat id.
- Preview draft state is keyed by target.
- Button callbacks store target and owning instance id.
- Reactions resolve to target/instance before mutation.
- Attachments generated by a follower are uploaded by the leader into the follower's target.

## Configuration

There is no public `telegram.json` switch for the bus. Telegram private-chat Threaded Mode is the runtime switch: when Telegram exposes threads for the bot, the bridge enables the local bus; when Telegram runs as an ordinary private DM, the bridge uses classic private-chat flow as the base mode.

Typical config remains just bot identity and authorization, stored in the canonical default profile:

```json
{
  "profiles": {
    "default": {
      "botToken": "...",
      "allowedUserId": 123456789
    }
  }
}
```

Named bots use sibling `profiles.<name>` entries. Shared bridge settings remain top-level.

Rules:

- Classic mode is selected by Telegram capability: when private-chat threads are unavailable or disabled, the polling owner uses ordinary single-DM behavior and blocked instances do not register as followers. During a live downgrade from Threaded Mode, the current bus leader becomes the classic polling owner after two 2.5-second capability-monitor probes and followers disconnect; if classic polling restore fails transiently, later monitor ticks retry the restore instead of allowing a follower takeover. Followers must not turn the downgrade into a takeover while active thread bindings prove the singleton owner was already established by the bus leader.
- Telegram private-chat Threaded Mode enables local leader/follower behavior automatically. The leader owns `getUpdates`; registered followers route Telegram API work through the leader. `/telegram-connect` registers as follower when a live leader exists and does not offer manual takeover in that state. The TUI status bar reports `telegram leader` or `telegram follower` while idle so transport role is visible without opening diagnostics, and both roles switch to `active`/`compacting` processing labels during local Telegram work. Follower registration is unique by live profile/target: a reload or session replacement must replace stale registry entries rather than leaving multiple routable ids for one Telegram thread, and fallback target ownership must not classify leader records as followers.
- The thread chat is the owner's private bot DM (`allowedUserId`); no `topics.chatId` config is needed. Thread names are assigned by the bridge from a baked compact per-slot palette. There is no agent-facing `telegram_rename_thread` tool and no separate user-facing slash command for manual thread renames.
- Thread reuse is extension-owned through profile-scoped Workspace bindings; there is no separate `topics` config surface in the active private-chat thread model. Leader/follower record keys remain live role projections, while normalized exact-`cwd` bindings preserve identity and deterministic same-directory process suffixes across role and process changes.
- Thread cleanup remains conservative and centralized: destructive close/delete actions are planned and applied through `thread-reconciler` with proof-before-delete checks, leader-epoch fencing, and retry-preserving failure semantics.
- `allowedUserId` remains the primary authorization boundary unless explicit allowlists are added. Forum/group membership alone must not grant control.

## Runtime State

Current state under the agent dir:

- `tmp/telegram/owners.json`: authoritative extension-local transport owners keyed by `default` or named profile. Each owner contains the bus leader identity, capability secret, heartbeat, generation, and cleanup fencing epoch. Mutations serialize through `owners.json.transaction`; followers never write owner slots. The local bus endpoint is derived from the agent directory by default; legacy `busSocketPath` entry fields are tolerated inside current owner records but are not required.
- `tmp/telegram/state.json`: volatile extension+bot observable/debug snapshot, not routing authority. It writes `source: "snapshot"` and `writtenAtMs` so consumers do not confuse it with an authoritative database. Every process on one Telegram profile reads this shared path, but only the active transport owner may persist it; followers become writers only after promotion. Status-only persistence refreshes disk-backed bindings before serialization so an already-loaded stale view cannot erase newer leader records. It mirrors `/telegram-status`-style projections: top-level `bot` stores bot-wide capability state such as `threadMode: "unknown" | "enabled" | "disabled"`; `runtime` identifies leader/follower role, lifecycle activity, and the exact polling phase/progress snapshot; `liveRoster` mirrors followers/current targets/reservations; `diagnostics` mirrors status/debug signals; `threads` stores current routeable bindings; `workspaceBindings` stores dormant exact-`cwd` target/name/slot reuse hints; `bot.lastSlot` stores the compact slot cursor used when all current threads are gone; and `reservations` records short-lived slot collision guards.
- Local bus endpoints: Unix-like platforms expose stable `tmp/telegram/bus.sock` and `tmp/telegram/followers/*` symlinks backed by private generation sockets; native Windows uses deterministic named pipes under `\\.\pipe\pi-telegram-...`. These are transient IPC endpoints, not durable routing state.

If an unclean host shutdown truncates `owners.json`, a profile `state*.json`, or the ownership transaction guard, `/telegram-connect` classifies the damage before recovery. With no verifiable live owner, one cross-process recovery winner quarantines only those damaged disposable artifacts and startup retries once; followers or leaders appearing during the final guarded reread stop the reset. `telegram.json`, `logs*.jsonl`, other profiles' valid state, and unrelated extension data remain untouched. A blocked or failed reset reports which Pi must restart instead of emitting repeated raw parse/transaction errors.

The bridge must not keep a separate durable `telegram-targets.json` history. Profile-specific `state.json` retains exact-`cwd` Workspace bindings as restart hints, but they never authorize routing without a matching authenticated process claim and role-appropriate liveness/visibility proof. Stale/offline/failed observations are not reusable delivery authority. `sync` remains event-driven assumption reconciliation rather than a full Telegram bot-state mirror because Bot API exposes no complete thread listing surface. Non-current routeable thread bindings are pruned during load/persist; old session records must not be retained merely to drive allocation. Workspace allocation uses retained binding/claim reservations; `bot.lastSlot` remains a compatibility cursor only for provisioning without Workspace identity. Previous-process leader bindings are treated as occupied TTL-bounded reservations until Telegram confirms deletion: reload/startup may close/delete/probe the old thread, known reservations are retried proactively on leader startup, and if Telegram still accepts the old thread id, the new leader should provision the next free slot (`B`, `C`, …) rather than creating a duplicate same-letter tab or blocking startup on Telegram UI convergence. Routing must use live current threads/follower registry, never reservations. The bus leader provisions its own thread during bus startup/connect and provisions follower threads on `follower.register`; registered followers also live in the leader's in-memory registry and communicate over the local bus socket. The live follower registry can resolve a follower by exact `{ chatId, threadId? }`; the leader uses that target ownership to forward message and edited-message updates to followers, and the follower receiver accepts those updates in addition to callbacks and reactions. Terminal status and `[telegram|thread:name]` resolve the matching current-instance identity through the same target-aware path, preferring registered local metadata over stale shared bindings. Media album grouping and split-text coalescing keys include the thread target, queue reaction mutations can scope by chat/thread to avoid cross-target message-id collisions, active-turn target is exposed for lifecycle cleanup and local direct-tool defaults, transport reply dedup is chat/thread-scoped, stored menu state is keyed by chat/message so callback state lookup cannot collide across chats, and generated button turns plus section prompt/open actions preserve the callback thread target. `telegram_message` and immediate `telegram_attach` delivery can also carry an explicit `thread_id` with `chat_id`; when a follower is registered, their default direct-tool target is the assigned thread target and the bus-aware API runtime routes the send through the leader instead of calling Bot API transport locally.

All files containing routing, chat ids, thread ids, or process details use private permissions and represent current state rather than historical target caches.

## Failure Modes

### Leader exits cleanly

- Leader stops polling and marks itself offline.
- Followers detect missing heartbeat.
- One follower promotes itself after jitter/tie-break.
- New leader resumes `getUpdates` from the persisted offset if safe.

### Leader crashes

- Followers detect stale heartbeat.
- One follower promotes itself.
- Some updates may be delayed or skipped depending on offset persistence; dispatcher design must define this explicitly.

### Follower heartbeat is missed

- Leader tolerates up to 15 seconds without a follower heartbeat before pruning it from the live registry, so short local event-loop or IPC stalls do not create false routing gaps. A follower response deadline defers its final timeout through one socket poll phase: an acknowledgement already buffered while the Pi/TUI event loop was blocked wins, while a genuinely silent peer still fails in the same event-loop turn. Heartbeat pruning remains liveness bookkeeping. One leader generation owns at most one prune operation; stop makes late endpoint, policy, and cleanup settlement inert, while durable-profile mutation serialization prevents replacement registration from crossing confirmed-dead cleanup.
- A missed heartbeat does not delete, close, mark offline, or send a disconnected notice for the follower's Telegram thread binding because the common cause may be leader reload, IPC handoff, or transient reconnect rather than a dead follower.
- Followers treat rejected/missing heartbeat acknowledgements as registration loss: retain the last known target locally, clear registered truth, show `reconnecting` instead of a healthy `follower` status, try to re-register with the current leader, wait a short leader-reload grace window, and retry. They promote only after the exact leader lease becomes stale or inactive; a live owner with an unreachable endpoint leaves the follower disconnected/retrying rather than creating a competing poller.
- Persisted current manual-follower bindings survive abrupt process absence as restoration hints when Thread cleanup is disabled. When enabled, graceful Pi quit requests exact-generation teardown before lifecycle suspension; if that envelope is missed, stale pruning may delete only after the leader's OS confirms the exact registered PID has exited.
- Fresh registration sends one compact connected notice in the assigned thread. An exact immediate session handoff uses a target-scoped `sendChatAction` as its synchronous visibility probe, avoiding a duplicate notice while retaining stale/ambiguous recovery; other cross-session restoration keeps the connected notice as its probe.
- Registration requires a present generation, and explicit disconnect requires that same exact live generation. Leader-side registration and disconnect mutations serialize per durable follower profile across old and replacement runtime instance IDs, so a replacement registration cannot overtake awaited destructive cleanup and an old disconnect cannot remove its successor's routing authority.
- Successful forwarded updates and follower-originated API calls refresh liveness, so active followers are not pruned only because the interval heartbeat tick lagged. Each follower lifecycle owns at most one in-flight heartbeat for its exact registration generation; stop/replacement makes late settlement inert, and recovery or diagnostic failure cannot escape as an unhandled interval Promise.
- Destructive follower thread teardown belongs to confirmed `/telegram-disconnect`, graceful Pi quit, or confirmed reconciliation actions, not generic heartbeat pruning. Manual disconnect retains its destructive confirmation and clears restart ownership; quit deletes the tab without prompting when Thread cleanup is enabled (default) but preserves the owner slot independently so a same-directory restart can reclaim leadership. Confirmed leader/follower teardown first persists an exact target/runtime-generation cleanup intent. The active leader attempts deletion under its current epoch; interruption preserves the intent so that leader or a successor can replay it under current authority, and confirmed deletion removes the binding plus intent in the same persisted state transition. If the graceful request is missed, stale heartbeat plus OS-confirmed absence of the exact registered PID may authorize the same cleanup while enabled; this action serializes ahead of replacement registration. Disabled cleanup, silence, heartbeat expiry alone, IPC/auth failure, and live or unknown process liveness remain non-destructive. Incomplete cleanup preserves durable intent for retry. A promoted leader uses its current owned leader epoch even when the inherited record still carries a historical `manual-follower` owner label.
- Explicit stale/deleted/offline observations invalidate reuse. Process absence affects reuse only through the enabled, exact-PID confirmed-dead cleanup path.

### Stale thread delivery

Local regression evidence covers continuity and cleanup authority; operator-coordinated live Restore verification remains tracked in [BACKLOG.md](../BACKLOG.md).

- Direct replies, menus, activity, target-aware edits, and multipart transport capture the request target and local authority before sending. Exact typed HTTP 400 stale-thread evidence stages invalidation of only the matching unchanged binding. The thread store rechecks leader/session/profile authority, binding identity, snapshot revisions, and destination path at the synchronous owner-fenced rename; no staged invalidation enters the live projection before durable commit, and a rejected commit preserves newer state.
- Shared recovery marks topic, target-binding, and transport freshness suspect and schedules a diagnostic snapshot. Accepted local work and its active-turn target remain unchanged; a failed send is not replayed or redirected, and recovery does not create a replacement thread or probe on every send. Errors without a proven request target do not authorize invalidation.
- Reconnect and explicit Restore remain separate authority-bearing operations. A stale API response proves target failure, not who deleted or closed the thread.

### Restore controls and cleanup

Operator-confirmed client flow: send ordinary text from the **All** tab; Telegram creates a new thread containing that text. Choose **Replace/restore thread…** and the existing Pi instance. Successful leader Restore binds and renames the new thread, dispatches the original message to the same Pi, and removes the old thread. The resulting prompt's thread-name label reflects the restored destination, not proof that the user typed in the old thread. A threadless `/start` is not equivalent to this ordinary-text flow.

Unselected All-tab command choosers expire 60 minutes after the original Telegram message timestamp. Expiry reports terminal settlement only for the exact still-deferred update and makes its callback inert; it does not execute the command or discard an accepted queue receipt. A valid destination selection pauses expiry during dispatch and suppresses concurrent repeat clicks. Failed attempts return to the original deadline without extending it, matching the replay expiry policy after restart. Successful local command dispatch settles a still-deferred source without waiting for background menu delivery; an accepted Pi queue receipt retains its own settlement contract. Follower command forwarding retires a still-deferred source only after confirmed acceptance; thrown or retryable transfer outcomes retain it. Expired replay completes without recreating the chooser. Worker stop invalidates pending chooser authority even without a usable timestamp. The expiry timer rechecks and rearms against its original deadline after backward wall-clock changes. Missing/invalid timestamps are not guessed, and `/thread`, bound-thread messages, ordinary prompts, and classic mode do not use this policy. Journal-write failure preserves the source for recovery; an expired visible chooser may remain, but its button cannot route the command. At pending capacity, new chooser admission fails retryably rather than silently evicting unresolved All commands.

For repeated unselected All-tab `/start`, a newer durable source supersedes older exact-text equivalents only after its chooser message ID is returned. Coalescing requires the same chat, user, and active admission worker; different arguments, other commands, and previously selected intents are not combined. Superseded chooser messages may remain visible, but their callbacks are inert. Storage failure retains the source according to the worker's fail-closed settlement contract. Restart may recreate a chooser for a still-unexpired source using currently routable targets; it does not automatically forward that command into a restored thread. This does not purge historical Telegram messages or promise that no fresh chooser appears after restart.

The bounded pending reroute owns its original source target and the returned chooser message ID independently from remaining messages. Authenticated callbacks must match the stored chat/chooser and any supplied thread field. `InaccessibleMessage` may omit the thread field; a missing callback message, unknown chooser, conflicting identity, threadless source, or already-owned Restore source fails closed rather than forwarding to the old target. Restart or expiry without that pending identity does not authorize reconstruction from callback data alone. When the original input itself has no thread ID (including an All-tab `/start`), the chooser offers routing only, not Restore; old Restore callbacks explain that a plain message must first be sent in the destination thread. Client tab selection is not inferred from recent topic creation or from the chosen Pi instance.

After dispatch, cleanup retries retain source identity but never redispatch accepted messages. Confirmed typed HTTP 400 `message to delete not found` completes message deletion idempotently; permission and transient failures remain errors. Reroute cleanup rechecks current bindings, live targets, reservations, and pending provisions before close, before delete, and on retry. Other destructive cleanup origins use the shared synchronous target-protection policy: explicit retirement permits only its unchanged departing binding, persisted shutdown intent permits only its original pre-intent binding, and reservation/provision cleanup permits only the corresponding unchanged claim. Protection checks also guard post-API local invalidation, reservation, and disconnect completion. A newly protected target cancels remaining cleanup. Already-issued remote operations cannot be undone by a later local ownership change; checks prevent subsequent effects, not retroactive cancellation.

Follower Restore requires exact registration generation and expected old target both before and after awaited IPC acknowledgement/store loading. Completion also rechecks the restored target and generation after persistence before publishing status or acknowledging success. A mismatched, replaced, or same-target request cannot overwrite the current registration.

### Split brain

- Two leaders calling `getUpdates` is the main safety failure.
- Lock heartbeat/takeover must be atomic enough to prevent this under normal local concurrency.
- Persistent competing `getUpdates` clients trigger a full transport stand-down even when the local lock still appears owned; the stopped runtime preserves accepted local work and releases only its exact ownership. See [Runtime Ownership](./architecture.md#runtime-ownership) for the threshold, diagnostic, and reconnect contract.

## Security Boundaries

- Messages, edits, callbacks, and reactions check user authorization, not only chat/thread membership.
- Followers authenticate to the local leader IPC with a leader-minted capability secret carried in the active lock entry; registration, heartbeat, forwarded updates, and follower API calls without the secret are rejected. Registration rejections are surfaced verbatim in the follower `/telegram-connect` result, registration waits through leader-side Telegram thread provisioning, and successful registrations send an immediate heartbeat before the interval ticker so the leader does not prune a live follower before its first scheduled heartbeat. The local bus socket is also created under a private `0700` directory with `0600` socket permissions as a first local-only boundary.
- Follower Bot API proxying is allowlisted and target-scoped where applicable. Ordinary sends remain confined to the follower's assigned thread; trusted runtime-marked `telegram_message` cross-target sends may address only a different thread inside the same paired chat, and the leader strips the internal marker before calling Telegram. This preserves requested inter-thread delivery without granting arbitrary bot or cross-chat control.
- Button and section callbacks verify authorized `from.id` and owning target/instance.
- Generated artifacts stay scoped to the owning thread after leader failover.
- Diagnostics redact bot tokens, large prompts, attachment paths, and handler output.

## Acceptance Criteria

- [x] The lock semantics are redesigned as Telegram bus leadership with heartbeat and stale takeover rules.
- [x] A first-class `TelegramTarget` can represent classic private chats and thread destinations.
- [x] The bridge can run in classic mode with unchanged private-chat behavior.
- [x] A live Pi instance can register as a follower when another live instance is leader.
- [x] Followers never call `getUpdates` for the shared bot token.
- [x] Followers can send replies, previews, voice, attachments, menus, and chat actions through the leader transport.
- [x] The leader can route inbound messages, edits, callbacks, reactions, media groups, and split text to the owning instance by target. Message/edit and callback/reaction routing is authorized by user id; media and split-text coalescing are target-keyed locally.
- [x] Telegram UI thread targets can be provisioned as current state bindings; stable manual-follower identities reclaim current bindings across process restart, authenticated registration generation gates routing, and stale/deleted observations or explicit disconnect/reconciliation remove unusable bindings.
- [x] Leader failover promotes one remaining follower without creating competing pollers.
- [x] Queue, active turn, preview, reply deduplication, menu, section, button, reaction, and attachment state are scoped by instance/target. Queue reaction mutations and transport reply dedup are chat/thread-scoped; active-turn target is available to lifecycle cleanup; stored menu state is chat/message-keyed; generated button turns and section prompt/open actions preserve callback targets; preview and attachment delivery already carry targets.
- [x] Authorization prevents arbitrary Telegram users or local processes from controlling agents or receiving artifacts.

Live client and native Windows evidence gates are tracked in `BACKLOG.md`; this architecture document records the implemented contract, not the active smoke queue.

## Implemented Shape

- Bus semantics are the feature frame: Telegram threads are one Telegram UI substrate for a local multi-instance bus.
- `TelegramTarget` and target-key helpers represent classic private chats and thread destinations.
- Outbound ports, previews, replies, voice, attachments, chat actions, menus, sections, buttons, queue mutations, and direct local delivery carry target metadata where needed.
- The transport lock distinguishes live bus leadership from ordinary classic ownership through heartbeat, leader epoch, and stale takeover rules.
- The leader records live follower registration, heartbeat, thread identity, slot, and target mapping; followers do not poll `getUpdates`.
- Local IPC is the default internal bus. Registered followers receive normalized inbound updates and send allowlisted, target-scoped Bot API calls through the leader.
- Thread targets are current-state bindings, not historical delivery addresses. Stable restart hints require a fresh authenticated follower registration before they become live routing authority; stale/offline/failed entries remain reconciliation evidence only.
- Failover promotes a remaining follower after dead or clean-disconnected leaders without creating competing pollers; follower heartbeat recovery owns re-register → grace → promotion while preserving thread bindings across transient leader reload gaps.
- Thread cleanup is centralized in `thread-reconciler`, fails closed without a leader epoch while leadership exists, revalidates that epoch immediately before every close/delete call and local cleanup-state mutation, and requires confirmed delete/stale evidence before state is marked deleted. Manual leader and promoted-leader disconnect persist cleanup intent first but always stop polling and release the owner lock even when Telegram cleanup is incomplete; the successor replays the retained intent, so a Bot API or epoch failure cannot pin leadership. Manual followers still require a live leader acknowledgement for destructive teardown.
- Stable docs/UI now describe classic mode, opt-in Threaded Mode, manual follower registration, status/diagnostics, unbound-thread reroute/restore UX, and operator recovery boundaries.

## Evidence Gates

Open live/client questions belong in `BACKLOG.md` until confirmed. Capture confirmed quirks as focused regressions or documented caveats, not broad speculative matrices.
