# Telegram Bridge Architecture

## Purpose

`pi-telegram` is a session-aware Pi runtime extension that binds Telegram destinations to running Pi instances and routes each accepted prompt into the assigned instance's currently active session. It owns the Telegram bridge boundary:

- Poll Telegram updates and enforce single-user pairing.
- Translate Telegram text, callbacks, media, and files into Pi turns.
- Stream previews and deliver final Pi responses back to Telegram.
- Provide Telegram-native controls for queueing, model/thinking/settings menus, compaction, abort/stop, prompt templates, reactions, and outbound artifacts.

The bridge is a mobile companion for a live Pi runtime, not a remote terminal or session browser. It should let an operator start work in the TUI and continue supervising the instance's active session from Telegram, while staying inside Pi's extension-facing contracts.

This document is the architectural map. Focused behavior standards live in sibling docs:

- [Public API](./public-api.md) — stable commands, config, package entrypoints, assistant markup, extension APIs, and compatibility boundaries.
- [Telegram Delivery API](./delivery.md) — target-aware operational views, logical message handles, lifecycle fencing, and leader/follower transport.
- [Telegram Activity API](./activity.md) — normalized Pi lifecycle events, activity/source identity, non-blocking extension dispatch, and delivery contexts.
- [UI Style](./ui-style.md) — inline UI labels, navigation, state markers, cards, and dialogs.
- [Callback Namespaces](./callback-namespaces.md) — callback prefix ownership and fallback rules.
- [Sections](./sections.md) — structured Telegram menu sections.
- [Updates](./updates.md) — update classification, default-routing plans, and raw Telegram update interception.
- [Voice Integration](./voice.md) — voice reply policy and STT/TTS provider surface.
- [Command Templates](./command-templates.md) — shell-free command-template contract.
- [Generative Apps](./generative-apps.md) — managed reusable application identity, state, generated button views, hybrid action routing, replacement, and bounded execution contract.
- [Telegram Multi-Instance Bus](./multi-instance-bus.md) — Threaded Mode bus leadership, Telegram UI thread targets, instance identity, and leader/follower routing.

## Runtime Topology

`index.ts` is a thin package entrypoint that re-exports the default extension from `lib/extension.ts`. `lib/extension.ts` is the only composition root. It wires live Pi ports, Telegram Bot API ports, session-local stores, lifecycle hooks, and domain runtimes. It should operate at high-level domain-runtime boundaries: non-trivial Threaded Mode capability decisions, leader/follower recovery, sync-slice bookkeeping, manual thread cleanup, and bus routing policies belong in their owning `/lib` domains. Reusable logic lives in flat `/lib/*.ts` domain modules rather than a deep local module tree.

### Extension Boundary Vs Supervisor Control

`pi-telegram` runs inside the current Pi process as an extension. That gives it safe access to public extension APIs such as aborting work, compacting, dispatching queued prompts, observing lifecycle events, and rendering Telegram-native controls. It does not own the terminal, the interactive-mode chat transcript, or the process lifecycle.

Keep this boundary explicit:

- Do not use raw TTY injection, ANSI terminal clearing, private TUI container mutation, or a shadow `pi` subprocess to simulate interactive commands.
- Do not treat Telegram as a generic remote shell for every Pi slash command.
- Commands that require interactive session replacement or TUI rerendering, such as a true Telegram `/new`, need a public Pi API that invokes the same runtime path as the terminal command.
- A separate PTY supervisor or daemon could choose to own those risks, but that would be a different product mode rather than this extension's runtime contract.

### Instance, Session, And Context Cost

A Telegram destination follows a Pi instance, not an immutable Pi session file. Ordinary Telegram prompts enter whichever session is active in that assigned instance when dispatch occurs. If the operator replaces or resumes a session locally, pi-telegram rebinds its session-scoped runtime state while preserving the instance's Telegram target where supported. Telegram currently exposes compaction for the active session, but not new-session, resume, fork, tree navigation, session switching, or full reload; those operations require safe public Pi extension APIs.

`/telegram-connect` never launches a hidden or headless Pi process. A long-lived background Pi process can own Telegram only when something else explicitly launched that process and it satisfies the normal lock/runtime rules. Pi `print` and `json` modes stay passive and exit rather than becoming hidden polling owners.

Pi session JSONL and pi-telegram runtime JSONL serve different purposes. Pi session files contain model conversation, tool, usage, branch, and compaction entries. Profile-scoped `logs.jsonl` / `logs.<profile>.jsonl` contain redacted bridge operations from one or more instances and never become model context. Sharing a Telegram profile or working directory does not by itself merge Pi session identities or model histories.

A Telegram prompt is a normal Pi model turn. It inherits the active post-compaction context just like a TUI prompt in the same session; pi-telegram does not promise context isolation or token cost proportional only to the new message. The bundled `telegram-bridge` Skill owns agent operation, while `show-me` owns portable evidence-honest explanation shape plus Telegram phone-width Markdown and browser-artifact adaptation; neither owns the other's transport boundary. A small authority-aware system note routes applicable turns to these and the other bundled Skills. Existing session files created by older versions may still contain historical repeated guidance until session replacement or compaction removes it from active context.

The repository uses a **Flat Domain DAG**:

- Local imports must form a directed acyclic graph.
- Cohesive domain files are preferred over atomizing every helper.
- Shared buckets such as `lib/constants.ts` or `lib/types.ts` are avoided.
- Constants and state types live with their owning domain.
- Narrow structural projections are allowed when they avoid importing broader runtime or wire DTOs.
- Source file headers include `Zones:` tags so cross-cutting responsibility stays visible without folder nesting.

### Domain Ownership Map

- `lib/extension.ts`: composition root for live ports, domain runtime construction, cross-domain port wiring, and lifecycle registration. It exposes wiring but owns no process identity, journal-binding selection, mutable late-binding state, admission lifecycle selection, reusable policy, or low-level adapter mechanics; those belong to `bus`, `journal`, `prompts`, `activity-verbosity`, `updates`, and other named domains.
- `api`: Bot API helpers, retries, uploads/downloads, temp cleanup, byte limits, chat actions, lazy token clients, and API error recording. Its optional Workspace-admission client classifies valid threaded requests exactly, chat-only requests chat-wide, malformed declared targets profile-wide, and targetless methods as outside the fence; JSON and multipart leases span internal retries through final settlement. Admission blocks prevent issuance, while release errors remain protective diagnostics and never convert an already-settled non-idempotent request into replay. Production composition resolves this adapter from the current bot/profile admission runtime.
- `config` / `setup`: `telegram.json`, bot token setup, named bot/session profiles, first-user pairing, authorization, env fallback, atomic persistence, effective config views, and live config accessors.
- `locks` / `polling`: extension-local transport owner storage, exact-owner epoch exposure, process-global reload generations, owner-aware polling lifecycle/takeover/follower registration, and classic-vs-Threaded capability orchestration. Polling owns long-poll state, worker-before-poller startup, strict batch validation, journal-before-offset admission, one offset commit per response, and non-awaited worker signaling.
- `journal`: private profile/bot-scoped raw-update authority with strict v1 identity/schema validation, exact deduplication, bounded transaction-serialized `0600` publication, process/session/acquisition-bound prompt/control receipts, owner-fenced completion, durable retry state, and generic removal that rejects queued or legacy failed authority. Runtime/recovery identity separates token rotation from future proof-gated queue-owner recovery. Read-only follower-journal discovery enumerates canonical profile-exact snapshots and segment roots and reports incomplete evidence for unexpected matching paths. Its optional Workspace-admission adapter conservatively classifies each append batch as exact-target, chat-wide, or profile-wide, holds all leases through publication, and releases acquired partial batches on rejection; production journal bindings resolve that adapter for leader, follower, recipient, and historical-path stores.
- `bus` / `bus-api` / `bus-leader` / `bus-follower` / `ownership` / `target`: Threaded Mode multi-instance bus contracts, profile-scoped process/endpoint identity, local leader/follower IPC, leader-only orchestration, follower-side manual registration/session runtime, follower-routed Bot API calls, live message ownership, and `{ chatId, threadId? }` target identity. `bus` owns protocol v1 independently from package build, canonical capabilities, compatibility, process identity, profile-aware endpoints, and IPC primitives. Registration rejects missing/mismatched protocol before provisioning while preserving compatible package skew; negotiated identity reaches status/state. Leader runtime, leader envelope handling, follower assembly, and follower registration construction require explicit protocol identity, preventing identity-less composition at both production and low-level runtime boundaries. `bus-leader` owns leader envelope handling and polling/server/prune orchestration; its Workspace-admission assembly holds a chat-wide lease across leader provisioning and profile-wide leases across follower provisioning/registration, disconnect/dead cleanup, leader/follower rename, display preference/reconciliation, and detached post-provision cleanup through its delayed API/store settlement. It consumes the profile operation runtime shared with Sync and routing; admission precedes that process-local mutation gate, fence rejection occurs before mutation, and release follows complete settlement. `bus-follower` owns registration/ack negotiation, active leader-auth/election state, heartbeat, authenticated clients, forwarded receiving, and recovery. Its production assembly holds profile-wide admission across follower-to-leader promotion and acknowledged target replacement, so a retained fence rejects both before leadership acquisition or store mutation. Production leader composition resolves the admission assembly at each operation boundary.
- `sync`: demand-driven Telegram reconciliation, mutable sync-slice state, nested provisioning activity, and local assumption policy. It does not own a complete Telegram bot read-model; Bot API lacks a complete topic/thread listing surface. It owns sync slices, invalidation triggers, config-persist invalidation sequencing, exact-target admitted stale-topic API recovery, observation intake, status/debug freshness, paired manual-disconnect/session-restart cleanup assembly, and reconciliation scheduling across bot identity, pairing assumptions, live target bindings, reservations, and transport health after meaningful observable signals. Production topic lifecycle and disconnect/restart cleanup enter profile-wide admission before the shared Workspace operation gate. Lifecycle admission spans observation-driven store settlement; cleanup admission spans intent publication, Telegram cleanup, binding mutation, durable settlement, and transport release. It should call narrower domain primitives rather than letting `index.ts`, `threads`, or `status` accumulate cross-cutting reconciliation policy.
- `thread-reconciler`: Threaded Mode control-plane planning for Telegram thread/tab lifecycle. It owns the reconciliation state machine (`stable`, `provisioning`, `sync-required`, `cleanup-required`), pure plans, proof-before-delete rules, pending-provision protection, fresh-creation grace windows, leader-epoch checks, and the single policy authority for destructive thread cleanup actions. It excludes live Telegram API calls, inbound routing, menu rendering, and direct persistence.
- `thread-cleanup-manager`: Disconnected proof-only admission planning for manual inactive-Workspace cleanup. It emits exact profile/binding/target snapshots only when durable inactivity exists, all live-owner/accepted-work/delivery evidence is `clear`, identities are unique, and no reservation, provision, or cleanup competes. Missing, malformed, duplicate, or `unknown` evidence returns no candidates; age and ordering never create authority. Its disconnected bounded profile/token-scoped work store atomically persists exact candidate snapshots, records one exact Workspace-issued deletion permit as outcome-unknown, rejects mismatched permits, and confirms deleted state idempotently; strict reads reject malformed/private-file ambiguity. A disconnected executor requires an injected exclusive Workspace deletion boundary across fresh planner evidence, exact retained-snapshot comparison, permit acquisition/recording, delete callback, and confirmation. The future fence owner must acquire and recheck in admission-ledger order; a retirement fence cannot be nested inside an active ordinary admission lease. Regressions prove drift stops before permit, unavailable/already-issued state cannot fabricate authority, and ambiguous delete remains outcome-unknown without replay. A production-shaped adapter preserves full Workspace records through external-protection resolution, then snapshots exact cleanup fields, reservations, provisions, and cleanup intents; protection exceptions become `unknown`, while source failures propagate fail-closed. Production Settings exposes **Review inactive tabs** only: profile-wide admission surrounds fresh evidence capture, one canonical 128-bit-digest work-set is retained, and a separate summary reports proven count, explicit no-deletion state, and Back navigation. The exact confirmation callback fits Telegram's 64-byte bound and accepts only canonical work-set IDs. **Clean inactive tabs** renders only when composition supplies a destructive port; production intentionally omits it, so malformed/stale callbacks fail closed and review remains non-destructive. Permit composition must not call `acquireRetirementFence()` because retirement remains pressure-only. The one admission-ledger fence now carries discriminated `pressure-retirement | manual-thread-cleanup` authority, treats legacy missing kind as pressure, exposes `acquireThreadCleanupFence()`, includes kind in exact comparison/permits, and remains profile-singleton. Cleanup-specific adopt/issue/absence/release/complete APIs preserve existing retirement callers and reject cross-kind use. Review admission releases before cleanup-fence acquisition; that fence then spans exact full-record/protection revalidation, sole permit issuance, work-set recording, one delete attempt, absence confirmation, binding/work-set commit, and fence completion. The v1-compatible schema and kind-specific acquire/adopt/issue/absence/release/complete methods are implemented; pressure methods reject manual fences and cleanup methods reject pressure fences. A disconnected permit runtime acquires the cleanup fence, revalidates under it, releases drifted unissued fences, refuses already-issued replay, and retains `commit-ready` until an injected durable commit succeeds. `threads.commitInactiveWorkspaceCleanup()` now removes only an exact full inactive binding after rechecking local records, claims, reservations, provisions, cleanups, and retirement intents; the retained cleanup candidate carries sufficient exact cwd/workspace/instance/global-slot/binding/target/inactivity/update commit identity, and under the retained fence exact absence closes commit-unknown retry without reconstructing the deleted full binding. Commit composition removes the binding before confirming the work-set, and failure retains `commit-ready`. A hidden coordinator validates canonical review identity, resolves the full binding, records the sole permit before one injected delete call, then commits binding, work-set, and fence. A `commit-ready` retry finishes without another delete; ambiguous deletion remains `deletion-issued` and is never replayed. A typed Settings-port adapter exposes this coordinator only when explicitly supplied and reports deleted, outcome-unknown, and blocked counts. Fake-port tests exercise the callback lifecycle and successor recovery: takeover requires injected proof, exact fence identity is preserved, live/unverifiable predecessors fail closed, and `commit-ready` resumes without deletion replay. Cross-process workers prove stale `prepared` contenders call fake transport once: deletion requires successful work-set permit CAS, and redundant fences over deleted entries settle without replay. Work-set pre-rename failure and lost post-rename acknowledgement retain recoverable fence truth; durable `deleted` completes that fence without another transport call. Binding-snapshot pre-rename ownership loss reloads and restores the exact binding, while post-rename acknowledgement loss reloads durable absence as successful commit. Cleanup runtime composition additionally requires candidate/work-set/runtime/fence profile equality before mutation and captures one stable successor owner snapshot for adoption. The optional Settings port reports only redacted exact-authority recovery classes: commit pending, deletion outcome unknown with no retry, or unavailable authority. It exposes no target, path, token, or transport details. Production still omits `cleanInactiveThreads`, so Bot API activation remains separate.
- `thread-display`: Shared Letters/Names/Directories projection, initial-create title selection, and serialized leader-owned title application. Owns bounded path disambiguation, ambiguous-label rejection, and profile/mode/epoch plus captured live-binding fences. Acknowledged `displayTitle` remains distinct from stable `threadName`; the caller owns triggers and live-owner discovery. Leader provisioning applies the configured projection before creation; registration ACKs deliver the acknowledged title before initial follower status, and heartbeat ACKs carry later changes. Stable runtime names remain restoration identity rather than presentation. Current-thread/TUI display identity is separate from restoration identity. Live bot choosers, notices, prompt labels, and cross-instance agent-target resolution use acknowledged display titles while captured numeric targets and live registrations remain the routing authority. Settings routes direct-owner changes locally and follower changes through an authenticated capability-gated envelope. The leader serializes preference writes and application.
- `workspace-slots`: Pure bounded global-letter selection and pressure-reclamation proposals. It consumes explicit protection/inactivity evidence and never discovers owners, persists state, or performs deletion.
- `workspace-admission`: Durable profile-scoped reader/writer ledger for cross-process exact-target, chat-wide, and profile-wide admission leases plus one destructive retirement fence. It owns atomic lease/fence transactions, proven-dead process-birth recovery, conservative malformed/ambiguous-state handling, exact successor adoption, retained-slot projection, and the durable `fenced` → `deletion-issued` → `commit-ready` phases that emit at most one deletion permit. Its runtime binding resolves `workspace-admission[.<profile>].json`, stores only the token SHA-256 profile authority, preserves separate named-profile identities across switching, permits changed-token rebind only when the prior ledger is provably empty, and fails closed while foreign leases or a fence remain. Issued fences cannot be released before confirmed absence and durable retirement commit; callers own journal/API/provisioning operations and retirement policy. Production composition supplies admission to journals, JSON/multipart API, leader/follower mutations, topic lifecycle, reroute restoration/reclamation, manual disconnect/session-restart cleanup, exact stale-target recovery, and Thread-store slot reservations; journal-evidence pruning also requires caller-supplied admission. Common async runners and the API adapter reject concurrent reuse of a live operation ID before a second caller can share or release its lease; once the first invocation exits, retry-stable recovery remains available. A 2/2 same-model independent post-fix quorum verified complete production-mutation composition at 0.96 confidence per reviewer. Destructive retirement remains disconnected by release scope; this verification does not authorize live deletion or replace disposable operator smoke.
- `workspace-retirement`: Profile/leader-fenced pressure preparation over the store snapshot. It counts standalone reservations, selects one candidate only at full slot capacity, rechecks protection, and persists/resumes an exact durable intent. Workspace bindings accumulate their historical follower-journal routing keys and distinguish complete fresh metadata from incomplete legacy evidence. Its read-only accepted-work policy resolves those binding-specific journals plus the shared leader journal and combines them with local exact targets, failing closed when source coverage or target decoding is incomplete. It can prune a known empty follower-journal key only from complete readable evidence plus explicit writer quiescence under exact binding/profile/epoch fences and an exact-target admission lease held through durable publication. Incomplete legacy bindings consume discovered hashed journals as target-scoped evidence but remain incomplete so every later retirement repeats discovery. The shared Workspace operation runtime serializes topic lifecycle, reroute restoration/reclamation, provisioning, delayed post-provision reconciliation, follower/manual cleanup, rename, and display mutation through one exposed gate. Detached mutation work must reacquire fresh admission rather than inherit a lease already released by its caller. A successor may durably adopt one exact stale-epoch intent after profile/binding/protection revalidation; direct old-epoch execution remains blocked. The isolated executor consumes that gate and requires the durable admission ledger. It acquires or exactly adopts the matching fence, rechecks protection after admissions close, advances to `deletion-issued` before invoking an executor-only `deleteForumTopic` port with the sole permit, and never reissues from that phase. Success or exact absence advances to `commit-ready`; store commit failure retains the fence, and exact completion follows durable binding+intent removal. A successor resolves an issued unknown outcome only through a separate exact-absence probe. Leader composition exposes exact registry, active/queued work, known journals, and profile-exact legacy discovery as protection evidence. Missing queue targets and incomplete reads remain unknown. The common direct Bot API client counts exact JSON/multipart targets until settlement; message-scoped edits/deletes without a thread conservatively protect every binding in their chat. Known historical follower owner keys decode to process-birth identity before liveness checks. Durable intents block matching claims and binding mutations. Preparation/adoption/execution remains disconnected from leader runtime by release scope. Independent review cleared the admission-composition blocker; live deletion and operator acceptance remain separate gates.
- `threads`: Telegram UI thread/tab binding state mapped to Bot API `message_thread_id` / `ForumTopic` transport. Owns exact-`cwd` Workspace bindings and transient claims, first-proven inactivity metadata, fail-closed retirement occupancy snapshots, and exact durable retirement intents, leader/current-instance identity state, active-turn → follower → leader target preference, matching status projection assembly, profile-bound same-process handoff, exact-claim global-slot allocation and conservative missing/duplicate legacy migration, collision-safe compact thread-name selection, Workspace-aware rename persistence, and primitive provision helpers. Its optional external-slot source makes every generic allocation, Workspace claim, and occupancy snapshot reserve retained admission-fence slots; malformed, unreadable, or non-uppercase evidence fails allocation closed. Its synchronous provision-commit helper transfers exact targeted creation-title evidence into the claim-committed Workspace binding and consumes matching pending evidence; callers retain admission, epoch checks, and durable publication. It should not turn dormant bindings into routing authority, own destructive cleanup policy, or grow into the general Telegram synchronization domain.
- `updates` / `routing`: update classification, authorization, callbacks, edits, reactions, forwarding, and inbound composition. `updates` owns production journal workers, leader/follower admission lifecycle construction, binding and settlement selection, queue-handoff projection across recipient journals/admission/IPC/live queue state, process/session queue-owner projection, post-public source binding, exact-signal late settlement, durable receipt readiness, same-process claim reconstruction, and structural worker state. `routing` converts message, callback, guest, section, reroute, and control admissions into exact receipts; its complete unbound-target and reroute restore/reclaim handlers run under the shared profile-wide Workspace operation boundary before store access.
- `media` / `text-groups` / `time-injection` / `turns` / `inbound`: inbound extraction, rich reply plaintext, grouped debounce, split-text coalescing, optional time context, handlers, and prompt assembly/editing, including the `[guest]` Guest Mode speed note appended to guest turn text. Group replay replaces stale generation-local message/report bindings without duplicating content.
- `queue`: queue contracts, transport stamps, lanes, readiness, mutations, dispatch, enqueueing, and lifecycle sequencing. Durable admission uses deterministic receipts, canonical source sets, replay dedupe, multiple folded-history receipts, append-before-dispatch reporting, exact handoff/control/discard settlement, and a readiness gate. Receipt-bearing inactive-profile work is preserved after current-profile work rather than dropped.
- `runtime`: session-local coordination primitives: counters, flags, setup guard, abort handler, typing timers, dispatch flags, and reset binding.
- `model` / `menu-model` / `menu-thinking` / `menu-status` / `menu-queue` / `menu-settings` / `menu` / `commands`: model identity, thinking levels, scoped model handling, menu render/callback behavior, slash commands, bot commands, and interactive controls.
- `sections`: Telegram menu-section registry, opaque section callback tokens, render/callback dispatch, safe section ports, and diagnostics.
- `keyboard`: shared inline-keyboard reply-markup shape only; feature domains own labels, callback data, and behavior.
- `preview` / `replies` / `rendering`: throttled native Rich Markdown draft delivery, native final reply delivery, reply parameters, transport-limit chunking, and remaining Telegram HTML rendering (bold, italic, strikethrough, spoilers, code, links) for bridge-owned UI/compatibility surfaces.
- `delivery`: public extension operational-view delivery, active-turn/instance/aggregate/authorized target policy, logical chunk handles, per-target ordering, runtime generation fencing, and the process-local runtime membrane. Its bridge adapter composes the established UI/compat reply renderer with narrow bus-aware Telegram API and ownership ports; it never exposes bot clients or Pi contexts.
- `activity`: public normalized Pi lifecycle registration, activity/source identity, assistant segment and reasoning normalization, executed-tool events, non-blocking per-handler queues, delivery contexts, compatibility adapters, and shutdown fencing. The same domain extends assistant-output observation for connected companion projection: eligible completed local/autonomous public segments retain source order and deduplicate event identity. `bindings` assembles observation, authority, sender, and failure-projection ports; routing owns exact delivery authority, outbound composes established transformations and reply delivery, and Bot API domains implement transport. No separate proactive state-machine domain exists.
- `outbound-markup`: top-level assistant action comment/fence parsing, shared JSON/CML grammar, attribute parsing, voice reply planning, and preview/delivery stripping.
- `outbound`: outbound text transformations, voice/button artifact delivery, and generated callback actions.
- `generative-apps`: managed deterministic application identity, canonical installation and explicit replacement, content-addressed module loading, state timelines, cross-process transition serialization, bounded executable-plus-argv adaptation, `telegram_bind`, and pre-model-queue `app::method` invocation. It does not own Telegram transport, arbitrary shell execution, or the external application adapted by one Generative App.
- `outbound-attachments`: `telegram_attach`, queued outbound files, stat/limit checks, ordinary photo/document delivery, and narrow single-artifact Rich Message planning/sending for probe-confirmed photo/video/audio formats. It owns known-failure fallback eligibility and ambiguous-send no-replay classification through structural error contracts without importing Bot API helpers.
- `channel-posts`: profile/token-bound authority for agent-authored channel publication intents, one-shot send/edit/delete fencing, outcome-unknown retention, exact successful post identity, capacity refusal, and bounded local listing. It never reads Telegram history; the composition root supplies direct-leader Bot API effects only after its durable grants.
- `status` / `logs`: status bar/status-message rendering, queue-lane summaries, the structural redacted event ring, profile-aware JSONL scope/reset/append behavior, exact-owner destructive commits, fail-soft synchronous and queued diagnostics persistence, status snapshot scheduling, and grouped diagnostics. `status` remains a structural leaf; `logs` composes filesystem evidence with status projections and contains every persistence failure so diagnostics cannot terminate or poison the runtime queue.
- `bindings` / `lifecycle` / `prompts` / `prompt-templates` / `pi`: Pi-facing command/tool/hook registration and cohesive cross-domain binding assembly, including queue mutation/dispatch/watchdog composition over admission and transport ports; session-generation fencing and start/shutdown sequencing across Queue, grouped input, Delivery, polling, capability monitor, follower refresh, and assistant-output projection; Telegram prompt guidance; prompt-template discovery/expansion; and centralized direct Pi SDK imports.
- `command-templates`: shell-free command-template helpers, composition expansion, placeholder substitution, executable resolution, warnings, and retry/timeout semantics.

### Host Compatibility Boundary

Pi is the primary and only officially supported host. `pi-telegram` may still accept narrow, host-neutral representation differences at its existing Pi-facing boundary when they preserve native Pi behavior and do not create a second runtime policy layer:

- `prompts` preserves either Pi's plain system-prompt string or an ordered block array supplied by a compatible host, appending Telegram guidance without collapsing host-owned blocks. An absent/null host system prompt is treated as empty, including the disconnected metadata-stripping path.
- `pi` normalizes settings-manager construction that is either synchronous or asynchronous, then adapts either Pi's legacy enabled-model methods or a generic `get` / `set` settings service before model-menu reads and scoped-model persistence use it. Hosts without an explicit reload method rely on fresh asynchronous construction; durable writes still require `flush`.
- `lifecycle` continues to require Pi's semantic `agent_settled` boundary. It does not infer terminal settlement from host-specific `agent_end`, retry, or stop events; a compatibility shim must reproduce that contract before it can safely support activity identity and unrecovered-error finalization.

This boundary uses no host-name detection, host package dependency, prototype patching, hidden agent process, PTY, or terminal forwarding. Representation adapters are best-effort compatibility rather than an OMP support guarantee. Alternate hosts and community contributors own validation of their compatibility shims and must supply every lifecycle semantic that the bridge requires.

### Guarded Invariants

Architecture invariant tests protect:

- Acyclic local imports.
- Direct Pi SDK imports centralized in the `pi` adapter.
- A thin `index.ts` package boundary and `lib/extension.ts` as the composition root without local runtime adapter logic.
- Runtime state isolation from local domain imports.
- Structural leaf-domain isolation.
- Menu/model boundary direction.
- API/config separation.
- Media/update/API decoupling.
- Outbound attachment isolation from queue, inbound media, and API helpers.

Mirrored domain regressions live in `/tests/*.test.ts`. Shared test fixtures should exist only when multiple suites genuinely reuse them.

## Configuration And Ownership

Telegram configuration lives in `~/.pi/agent/telegram.json`. Bot/session identity (`botToken`, `botUsername`, `botId`, `allowedUserId`) persists only under `profiles.default` or `profiles.<name>`; shared handlers and assistant/voice/time settings stay top-level. Per-profile polling/admission state lives only in the durable update journal as `acceptedThroughUpdateId`. Authoritative transport ownership lives separately in the pi-telegram-private `~/.pi/agent/tmp/telegram/owners.json` store. Its top-level slots are `default` and validated named profile names; unrelated extensions never read or write this file.

`telegram.json` is one global cross-instance configuration document. Ordinary reads rely on atomic publication and do not take the mutation guard. Every cooperating Pi instance persists only its recursive delta from the snapshot it loaded, merges that delta into the latest disk document inside `telegram.json.transaction`, and publishes atomically only when the semantic result differs; a no-op merge adopts the newer disk snapshot in memory without replacing the file. Unrelated global and profile changes therefore survive stale writers. Two serialized writers changing the same leaf use commit order, so the later local delta wins. A non-transactional external editor cannot participate in that conflict protocol: it should write through same-directory atomic replacement while Pi is idle, then let instances reload; an editor racing the transaction may lose its same-leaf change and must retry from the resulting file.

### Setup Flow

First-contact pairing publishes through a profile-exact config-store operation, not a live setter followed by a save. Inside the existing config transaction, it rechecks execution/profile/token identity, reads the current disk owner, and writes only an unpaired profile; a different configured owner denies the candidate without overwrite. Only successful publication or confirmation of the same disk owner updates local authorization. Failure leaves the candidate unpaired for retry, and adopting disk evidence preserves unrelated local settings. Queued writes retain their request-time baseline for the requested disk delta, but cache adoption compares local deltas against the latest observed persisted state. An observed owner is not a local grant edit; later local unpair and external revocation must survive queued completion and subsequent saves. Message/edit/callback pairing and denial admission precede foreign message/target routing and unbound-Thread delegation, so fallback dispatch and ownership recording cannot bypass publication. `/start` menu side effects also stop when pairing returns false; configured matching owners remain authorized. Reactions require an existing positive safe-integer owner and that exact human sender before ownership lookup, forwarding, group flush or queue mutation—even in private chats. Unpaired, foreign-user, bot, missing-user and actor-chat reactions do not initiate pairing or perform those effects. This is publication safety, not operator-confirmed pairing: the first-contact UX is unchanged.

`/telegram-setup` progressively resolves the bot token:

1. Use the locally saved token when present.
2. Otherwise use the first supported Telegram token environment variable, prefilled as an exact `$NAME` reference instead of the resolved secret.
3. Otherwise show the example placeholder.

`profiles.<name>.botToken` may hold a literal token (compatibility) or an exact `$NAME`/`${NAME}` environment reference. References are resolved only at validation and activation boundaries: setup validates the resolved value while persisting the alias, the config store exposes the resolved token to transport and identity hashing, and every other boundary keeps the stored reference. An unresolved reference fails closed with a redacted named-variable diagnostic, and a `$`-prefixed value that is not a valid reference is malformed rather than a literal secret.

`ctx.ui.input()` only supports placeholder text, so setup uses `ctx.ui.editor()` when a real default must appear already filled in. Bare and explicit `default` setup/connect commands address the same `profiles.default` entry. Persisted config is written through a private temp file plus atomic rename and left with `0600` permissions. On first load, legacy root identity moves into `profiles.default` in that same serialized atomic transaction when no conflicting canonical value exists; identical duplicates collapse, complementary fields merge, and conflicts reject the load without modifying the file.

### Automatic Pairing Confirmation Design

**Status: approved UX direction; isolated storage preparation implemented, production unchanged.** The operator selected an automatically presented confirmation in trusted Pi UI, detached from the journal worker. Current runtime behavior remains the publication-safe automatic first-contact flow described above until implementation and review are complete. Existing configured owners and manual numeric-ID preconfiguration remain compatible.

#### Admission And UI

- A valid unpaired private human message creates a bounded candidate, not an authorized prompt. Edits and callbacks cannot create independent approval requests; they remain non-executable while unpaired. Guest messages are not a pairing surface.
- Return a distinct `pending` admission outcome before foreign routing, ownership recording, unbound handling, downloads, inbound handlers, menus, or model dispatch. The worker must not await either the dialog or approval-time config publication. Pending is not a transient execution error that retries the original prompt into authorization.
- Schedule the dialog through the session-owned pairing runtime after candidate admission. Use native `ctx.ui.confirm` with an AbortSignal and timeout; no custom editor, shadow process, model turn, or `/telegram-pair` command is needed. Initial support is terminal UI (`ctx.mode === "tui"`); `hasUI` alone also admits RPC and is insufficient for this terminal trust boundary. Without that surface, remain unpaired and retain manual preconfiguration as the fallback.
- Show the exact bot profile and numeric Telegram user ID, plus a bounded, control-character-safe display name as untrusted context. Example title: `Allow Telegram account?`; body explains which account gains access to the running Pi session and that earlier input will not be executed. Neither message text nor credentials enter the dialog or model history. No/ESC, timeout, UI error, or missing UI means no grant.
- Permit one candidate and one dialog per active profile/runtime. The candidate lasts 60 seconds from admission; duplicate input does not extend it. Keep the remaining window as a profile-wide cooldown after rejection/error/timeout to prevent immediate dialog flooding. A different requester cannot replace the visible candidate; operators can reject and request a fresh `/start` after the window. Keep this bounded trade-off explicit rather than adding an unbounded waiting list. This limits dialog frequency, not repeated first-requester denial of service; restart also resets ephemeral cooldown.

#### Lifecycle And Publication

- Candidate states are `pending → publishing → authorized`, or `pending/publishing → denied/cancelled`. UI acceptance starts publication; it is not itself authority. Recheck candidate identity, lifetime, active profile/bot, session generation, and exact current transport owner at the final commit boundary. Reuse the config owner's atomic existing-owner comparison; never clear or replace a configured account as part of pairing.
- The approval task owns fresh lifecycle/transport authority. It must not borrow an update execution fence, Workspace lease, or gate that ended when the triggering journal handler returned. No filesystem mutex, Workspace operation gate, or journal worker slot spans the dialog wait. Publication must use a reviewed lock order and a synchronous final ownership check/commit, without holding a filesystem lock across asynchronous work.
- Session replacement, shutdown, disconnect, profile/token change, or transport ownership loss cancels the pending dialog through its own AbortController. Late Yes, late UI failure, or stale notification cannot grant, restart, or mutate a successor candidate. A publication failure remains unpaired; retry needs a new request/approval. If publication may already have committed, inspect durable exact-owner evidence rather than issuing a second blind grant.
- Pending/UI state is ephemeral and never restored as consent. Do not use `pi.appendEntry`, model history, or a new shared sidecar as an authorization ledger. Any completion notice is best-effort, target/profile/generation-fenced, and outside journal-worker completion; notice failure cannot revoke a committed owner or replay old input.

#### Durable Admission Exclusion

The independent design review rejected a memory-only pending flag and approval-time backlog deletion: the worker already snapshots multiple entries before awaiting their execution. Instead, persist an immutable journal-owned `preApprovalExcluded` boolean with each newly admitted entry. It is a permanent execution veto, never a grant; `false` still requires ordinary current sender authorization. The candidate's triggering entry and every entry serialized into the journal while the exact persisted profile is unpaired receive `true`. Duplicate append preserves the original classification even if config has since changed. This metadata belongs beside `entry.update`, never inside caller-supplied Telegram payload fields.

The worker now requires a versioned snapshot and validates the complete exclusion evidence before reconstructing any queue authority. Missing v2 bits, malformed bits, unknown/missing versions, or excluded queued entries block the snapshot. Excluded executable entries bypass execution preparation, registered handlers, and default routing and proceed only to fenced journal settlement; a held snapshot retains its veto across an earlier handler's await. The durable polling adapter now prepares only contiguous non-excluded runs from the journal's `nonExcludedUpdateIds` result, synchronously after publication and before yielding to the local worker. Excluded positions remain boundaries, so filtering cannot invent a new comment/forward pair across them. The low-level poll loop no longer performs preparation. Failed publication performs no preparation; a preparation/diagnostic failure cannot withhold the wakeup of already-admitted work. Group plans remain ephemeral, not crash-restored journal authority. Candidate offering is still pending and may reach only a narrow path that checks the current unpaired state and exact local polling-owner authority. Only valid private human messages can offer a candidate; excluded edits, callbacks, reactions, deletions, and lifecycle/service payloads settle without their normal effects. After approval, excluded entries settle without execution or another dialog. Include the evidence in every worker snapshot/copy and preserve it across segments, compaction, retry, and restart. `markQueued` rejects excluded sources. Trusted internal events outside external journal admission retain their existing separate authority checks; they are not relabeled by Telegram payload fields.

This proves exclusion by **serialized durable admission**, not by network receipt time or the remote sender's clock. A fetched/buffered response admitted after the grant follows post-grant classification. No stronger promise about physically earlier sends is made. No new polling cursor, approval-time watermark, backlog purge, or cross-file grant ledger is introduced.

#### Transaction Order And Crash Boundaries

The inspected implementation offers synchronous journal append (`appendBatch` wraps its `runMutation`) and exact synchronous transport commit (`lockRuntime.commitIfOwned`). Config persistence queues a promise, so wrapping that promise-returning method in `commitIfOwned` would not protect its eventual write. The preparatory store now supplies `withPairingAdmission(profile, tokenSha256, publish)` for trusted synchronous publication under exact persisted config evidence and an optional per-call `commitIfOwned` port inside `persistAllowedUserId`'s queued write. A real lock-runtime regression proves the latter encloses the config rename and rejects an owner replaced before queue execution. New confirmation callers must require that guard; its omission preserves only the existing automatic first-contact path until integration. The observation seam is now exercised through opt-in journal admission; production adapters and UI remain disconnected. Do not await inside the observation callback or hold ownership while awaiting the persistence queue.

- Admission: acquire existing Workspace admission first; inside that operation acquire the config transaction, read exact persisted profile/token/owner evidence, then enter the journal transaction and atomically append entries plus their exclusion bits. Release both filesystem transactions before preparing groups or signaling the worker. After append returns, the durable polling adapter consumes the journal-owned non-excluded ID projection without an intervening await; moving preparation merely after an awaited append would race an already-draining local worker. The projection is not sender authorization. The config-owned observation callback wraps the synchronous append; it must not reacquire Workspace admission from inside config/journal locks.
- Approval: wait for UI and the config persistence queue without locks; then enter exact transport-owner transaction → config transaction → final candidate/session/profile/token/deadline checks → owner comparison → atomic config rename. The rename linearizes the grant. Use the same bounded transaction primitives; no UI, network, awaited operation, or diagnostics publication runs while these filesystem locks are held.
- Existing code has config-only publication, journal-only mutation, and short owner-fenced Thread/log/endpoint commits. Before activation, verify every added adapter preserves the proposed graph: Workspace admission precedes config; config may enter journal, never journal → config; owner-fenced grant may enter config, never config → owner. A source-level callback/lock-order inventory and contention tests are required, not inferred from the acyclic import graph. An isolated two-process config regression now holds the observation transaction while a separate grant process holds exact owner authority and waits for config; the observer sees exclusion, the grant completes only after release, and a later observation sees the disk owner despite a stale local cache. This proves the config/grant seam, not the still-unwired Workspace → config → journal composition.
- Before journal publication there is no durable admission. After excluded-entry publication, restart retains the veto. After Yes but before config rename there is no grant and consent is not restored. After rename but before local status/memory update the disk owner remains authoritative and old exclusions remain. An unknown publication outcome is reconciled through exact disk evidence; it never authorizes blind grant reissue or revocation.

#### Schema And Migration Gate

Preparatory storage protection is implemented independently of the future veto schema: snapshot and segment parsers classify unsupported integer versions before applying the current shape rules; recovery scans the retained files for unsupported versions before any repair/quarantine/reset, and non-corruption probe errors propagate. Receipt-scope and binding-key codecs use a separate fixed v1 identity version, verified by golden encodings. Production construction still uses v1. Explicitly supplying the synchronous `withPairingAdmission` store port selects v2: every persisted entry requires its immutable exclusion boolean, preserved through failure/retry, segments and compaction. Mixed queue receipts containing an excluded source fail atomically. V2 refuses legacy files, implicit identity rebinding, and corruption/missing-snapshot recovery without repair or reset; migration is not implemented.

The opt-in snapshot/segment schema v2 carries the mandatory boolean; v1 continues to reject that field. V2 append requires the existing admission cursor and suppresses absent IDs at or below its previously committed value, preventing a settled excluded entry from being re-admitted after approval. Segment replay rejects exclusion changes and below-cursor resurrection. This mode is for cursor-ordered polling only: do not enable it on out-of-order follower inboxes by inventing a maximum cursor. Their source-evidence/forwarding contract remains an integration gate. No second cursor or tombstone ledger is added. Separate the storage-schema version from stable receipt scopes, queue-owner identities, and journal-binding key codecs: those immutable recovery identities must not change merely because the file format changes. Missing/malformed evidence in the new schema fails closed, and unsupported versions must not enter automatic quarantine/reset as if they were disposable corruption.

Before starting a worker or enabling candidate grants, prepare its source under config → journal serialization with no surviving legacy worker snapshots. For an already configured polling profile, preserve existing accepted work and mark legacy entries non-excluded; current sender checks still apply. For an unpaired polling profile, stamp pending/retry legacy entries excluded. The proposed follower policy below instead blocks unpaired retained follower entries; it does not invent an ordered cursor for them. Unexpected queued/claimed/handoff authority in an unpaired legacy journal blocks migration and approval for explicit reconciliation; never silently discard or reinterpret it. Preserve revisions, update IDs, cursor, receipts, owner births/generations, failures, and handoff evidence. A failed migration does not start a worker or UI candidate. Regression tests must prove the unchanged identity codecs and guarded startup ordering.

Automatic approval stays disconnected until schema/migration, lock-order, and worker-copy proofs pass. Mixed-version owners/consumers and downgrade remain an explicit rollout gate: do not imply that an older runtime understands the new veto or human-approval requirement. No running instances or live journals are migrated by this design work.

#### Follower Source Policy Candidate

**Reviewed design; implementation and activation gates remain open.** Keep cursor-ordered polling on v2 and out-of-order follower inboxes on v1. This is narrower than adding another storage mode or consent ledger: excluded polling entries must never reach forwarding, while new follower admission must require an already persisted exact user owner. Retained unpaired follower work is a reconciliation gate, not permission to discard it.

The config prerequisite is implemented as `withPairedUserAdmission(profile, tokenSha256, userId, publish, assertExecutionCurrent?)`: it returns explicit denial for an absent/different owner or invalid sender ID, observes exact persisted authority under config transaction, and refreshes an unpaired cache before the trusted synchronous callback without writing config. Changed local profile/token, a conflicting cached owner, or an unpublished local unpair refuses instead of silently switching authority. Default/named-profile tests cover peer grants, queued settings and local edits, stale guards, rejected publication, and unchanged config bytes. Combined interleavings also cover local unpair after observation and external revocation before queued completion, including a subsequent save that must not recreate the owner. This method is exercised by isolated journal and receiver tests, not by production follower factories, and does not replace receiver provenance or publication-time fences.

The journal prerequisite now exists as the optional `withPairedAdmission` port. It preserves v1 and runs after Workspace admission but before journal locking or recovery-capable reads; explicit denial becomes `sender-denied` without publication. Inputs are normalized once before deriving Workspace scopes, checking sender authority and persisting the same canonical data. The paired-only port and v2 exclusion port are mutually exclusive. Tests prove peer-grant cache refresh, `20 → 10` admission without a cursor, duplicate preservation, canonical-input scope consistency, and guard release after denial/stale context/publication failure. This guards append only: production wiring and startup/read/recovery consumers remain separate cutover work.

`createTelegramBusFollowerPairedAdmission()` binds the config port to the journal hook for one canonical message/edit/callback/reaction carrier with an exact source ID. It selects message/edit/callback `from` or reaction `user`, requires a positive safe ID and explicit `is_bot: false`, and rejects defined `sender_chat`/`actor_chat`, ambiguous carriers and invalid grouping metadata. Original forwarding authors and callback-message authors do not authorize the sender. Bot API evidence: [`User`](../.agents/skills/telegram-bot/api.md#user) requires the boolean flag; [`Message`](../.agents/skills/telegram-bot/api.md#message) may expose a fake `from` for chat senders, while [`CallbackQuery`](../.agents/skills/telegram-bot/api.md#callbackquery) and [`MessageReactionUpdated`](../.agents/skills/telegram-bot/api.md#messagereactionupdated) identify the acting user separately. An isolated real IPC receiver/config/journal composition proves provenance checks precede config admission, peer grants refresh the receiver cache, all four kinds survive unordered v1 delivery, and denial/staleness/publication failure does not append or wake the worker. Wakeup follows guard release; config bytes remain unchanged. The required execution assertion and publication-boundary fences remain caller-owned; the helper does not capture or invent receiver lifetime authority.

The source constraints are concrete. `createTelegramBusFollowerDurableAdmissionRuntime()` appends each authenticated delivery without a cursor; its receiver checks secret, registration generation, recipient binding and source ID, but production journal factories do not yet select the paired-only port. A synthetic store witness admits IDs `20 → 10` into v1 and retains both; substituting cursor-ordered v2 with a fabricated maximum cursor suppresses `10`. The lifecycle's `bind()` currently constructs the worker, resumes terminal entries, and performs dead-owner cleanup before `worker.start()`, so checking migration only at `start()` is too late.

- **New deliveries:** Preserve transport authentication, exact generation/binding/delivery identity and current-session checks. Before append, acquire Workspace admission → config transaction → journal transaction. A config-owned paired-only seam must confirm the envelope sender is the exact persisted owner of the expected profile/token and refresh that authority in the receiving process before worker signaling. It must never create an owner. Boolean "profile is paired" alone is insufficient; a follower cache loaded before another process's grant may still be unpaired. The guard belongs inside the journal's already-Workspace-admitted synchronous append path, not around public `appendBatch()` or async `admit()`: those wrappers would reacquire Workspace under config or span the wrong boundary. Release these admission/transaction guards before worker signaling and ACK. The existing polling observation port selects v2, so neither it nor the automatic first-contact publisher substitutes for a paired-only v1 seam.
- **Source guarantee:** Only the updated leader's exclusion-gated, sender-authorized path may forward content. Followers cannot offer pairing candidates. Bind this contract to the negotiated producer/consumer capability and exact registration; an envelope assertion is not consent. Do not advertise the capability while using an ungated producer or receiver. Mixed/old runtimes remain an operator-coordinated activation prohibition, not a software-enforced downgrade claim.
- **Legacy followers:** A configured profile preserves validated v1 accepted work, receipts, births/generations and handoffs without a schema rewrite. If the profile is unpaired, any retained follower source entry—pending, retry, terminal or queued—blocks source startup and a new UI grant until exact operator-authorized reconciliation. Preserve bytes; do not auto-clear, quarantine, assign a maximum cursor, or silently relabel the work. This deliberately narrows automatic legacy migration. Reconcile such state before manual preconfiguration too; no retroactive consent guarantee is inferred from an operator editing the config file.
- **Readiness and migration:** Use complete, bounded, strict read-only inspection of the current profile's polling journal and discovered follower snapshots/segments, not recovery-capable `read()` as a preflight. Unknown identity, unreadable/corrupt evidence or incomplete inventory blocks readiness. Stop and await the old local worker, then re-resolve/revalidate profile/token/registration binding before preparation; the binding captured before the await cannot authorize successor startup. Place preparation before worker construction, terminal retry, queue-owner cleanup and handoff consumers, and publish lifecycle availability only after it succeeds. Also cover non-worker readers: Workspace journal protection capture currently calls recovery-capable `read()` and must not repair/reset evidence ahead of readiness. Discovery alone is insufficient: strict inspection must reject symlinks/unknown identities, bound the inventory, and close namespace creation, recovery and handoff paths through config serialization or explicit quiescence. Operator-coordinated quiescence of other legacy consumers remains necessary. Preserve fixed recovery identities; no "ready" sidecar or cached boolean grants authority.
- **Final grant:** Recheck source readiness inside the queued publication's existing config transaction, under exact transport ownership, only when creating a previously absent owner. New follower writers must use the same config serialization. The check cannot reacquire config or await; journal locks follow config. Recheck candidate/session/profile/token/deadline immediately before rename after inspection. A startup-only inventory or checks outside that transaction do not close the append/grant race.

Required witnesses before accepting this policy: out-of-order first deliveries survive; a legitimate forwarded message after a peer grant refreshes a stale follower cache without writing a grant; unpaired/different-owner/stale-generation admission has no publication or wakeup; unpaired retained follower data survives restart and blocks UI publication; configured legacy receipts remain intact; preparation precedes every recovery consumer; inventory/append/grant contention cannot produce a ready-but-unexcluded path. If these cannot be proved without another durable authority, reopen the source design rather than widening v2 by assumption.

#### Strict Journal Inspection Candidate

**The isolated reader and local implementation review are complete; consumer/activation gates remain open.** `inspectTelegramUpdateJournalFamily({ directory, path, profile, botIdentity, limits })` returns absence or a validated file plus file/byte/work accounting. Its optional `knownBotId` result is a validation constraint, possibly inherited from the caller, not enrichment of the stored identity. It requires canonical paths and available `O_NOFOLLOW`/`O_NONBLOCK` flags, refusing unsupported platforms rather than weakening acquisition. Linux fixtures cover metadata, resource and mutation boundaries; canonicalized fixture roots also pass with a symlink-backed temp directory. On unavailable open flags, tests assert refusal and unchanged evidence instead of expecting successful decoding, with an explicit coverage diagnostic. Synthetic load-time variants cover each missing flag and both together; these are not native Windows or whole-profile readiness evidence. Isolated filesystem probes establish why wrapping the ordinary reader is insufficient: reading an empty foreign-profile journal rewrites its identity; revisioned snapshots skip redundant retained segments even when those segments declare an unsupported schema; follower discovery follows canonical snapshot symlinks outside the scanned directory. Source inspection also shows unbounded `readdirSync()` allocation and separate snapshot/unapplied-segment byte budgets. These are counterexamples to reuse as strict preflight, not evidence that normal legacy recovery has changed.

- `Owner and result`: Keep the inspector in `journal`, reuse its schema/entry/receipt validators, and extract shared pure replay only when needed. Inspect one exact journal family first; profile inventory is a later caller. Return validated evidence or absence, not permission, a cached ready flag, or a synthesized empty journal. Do not call `read()`, the current `readCurrentStrict()` closure, recovery, identity rebinding, compaction, publication, or lock-file creation from the inspector.
- `Acquisition`: Accept an approved canonical directory anchor, a journal path within it, expected profile/token identity, and explicit positive safe-integer file-count, aggregate-byte, per-collection/state-entry and aggregate-work limits. Bound directory enumeration before collecting/sorting names, including ignored entries and a bounded overflow witness. Open only regular non-symlink files; reject linked path components beneath the anchor, unexpected segment entries, wrong types, uncertain absence, and observable namespace/handle changes. Read bounded bytes from the opened handle, reject invalid UTF-8, and count snapshot plus every retained segment against one aggregate budget before parsing. A size check followed by unbounded `readFileSync(path)` is insufficient.
- `Schema and identity`: Recognize only supported v1/v2 snapshots, validate every retained segment including revisions covered by the snapshot, and require exact profile/token identity without empty-file rebinding or same-bot token substitution. Maintain one validation-only known bot-ID constraint across expected identity, snapshot and every segment, including redundant segments: two conflicting known IDs fail even when the snapshot omits that field. Preserve the stored snapshot identity; a missing optional bot ID cannot cause identity enrichment. Unknown, mixed, malformed, or incomplete evidence fails closed with bytes untouched. Existing deterministic legacy failure-ID projection may remain a decoder behavior; it does not authorize a rewrite or new recovery identity.
- `Replay`: Start only from a validated snapshot; a retained segment directory without a snapshot is not empty-source proof. Validate filenames, matching revisions, intrinsic `previousRevision === revision - 1`, and unique disposition failure IDs for every segment, but do not demand a complete historical chain below the snapshot. Apply only the contiguous newer chain (`revision === currentRevision + 1`), preserving cursor, revisions, receipts, owners, births/generations, failure/handoff metadata and v2 immutable exclusion checks. Bound raw `entries`, `upsertedEntries`, `removedUpdateIds` and `operatorDispositions` before invoking codecs; also bound reconstructed entries/dispositions. Charge aggregate work for raw collection validation and repeated reconstructed-state validation so small deltas cannot hide excessive replay work. Avoid argument-spread maxima; positive caller limits are not an engine argument-count guarantee. Never infer a polling cursor from follower IDs or repair a gap.
- `Serialization boundary`: The caller owns Workspace/config/journal ordering and must establish writer quiescence or the corresponding existing transaction before consuming evidence. Stable metadata checks and repeat enumeration detect some changes; they do not defeat hostile same-user path substitution or prove namespace closure. Do not hold a filesystem mutex across an await. Profile discovery, namespace creation, recovery and handoff writers must be audited before final grant can use this reader; ignored/archive storage needs proof that it cannot feed an automatic consumer, not an assumption based on its name.
- `Migration boundary`: Rejecting mixed schemas is initially safe but is not a crash-complete migration protocol. A v2 snapshot followed by interrupted cleanup of older v1 segments needs a separately proven reconciliation rule before activation. Do not add a sidecar, remove uncertain segments, or claim downgrade safety to bypass that gate.
- `Proof cohort`: Exercise v1/v2 current and legacy metadata, exact empty-identity rejection, redundant unsupported/mixed segments, revision gaps, missing snapshots, symlinks/wrong types, aggregate size/count/entry overflow, and unchanged tree bytes on every outcome. Fault injection must distinguish detectable concurrent changes from guaranteed serialization. Native Windows/path behavior, complete profile inventory and grant-time contention remain separate evidence gates. The retained single-family reader has no consumer or production wiring; its tests do not establish writer quiescence.

#### Canonical Profile Journal Inventory

`inspectTelegramProfileJournalNamespace()` now inventories the approved directory's canonical polling/follower namespace using the strict family reader; its isolated implementation review is complete. It returns deterministic source roles/paths/evidence, shared accounting and a validation-only bot-ID constraint. Tests cover exact aggregate boundaries, unread foreign contents, segment-only cross-family conflicts and root changes after the last family inspection. It does not certify consumer-reference closure or authorize a grant, migration, cleanup, or startup.

- `Names and scope`: Use the existing `inbox[.<profile>].json` and `follower-inbox-<16 lowercase hex>[.<profile>].json` paths plus their `.segments` directories. Supported profile namespace components are the current lowercase ASCII alphanumeric names of at most 32 characters; `default` has no suffix. Treat any case-insensitive `inbox` substring as journal-like, even in otherwise unrelated names such as `personal-inbox-notes.txt`. Reject noncanonical aliases, malformed journal-like names, journal-shaped symlinks/wrong types, and sanitizing profile names rather than guessing ownership. Canonically distinct foreign-profile names can be classified without reading their contents; count them and unrelated entries against the directory limit. Never infer a Workspace binding or allocation authority from a filename hash.
- `Inventory and limits`: Stream a bounded root census before inspecting families, deduplicate snapshot/segment pairs, always inspect the polling path (including absence), and inspect current-profile follower paths deterministically. Share file/byte/work budgets across families rather than resetting each call. Keep the family collection/state ceiling; inventory work charges at least one unit per family visit, including absence, and otherwise the family's decoded/revalidated collection count. Reject exhaustion before another visit. Re-enumerate within the same root-entry bound and reject observable namespace changes; missing discovered families cannot silently become empty evidence. The census compares metadata for every root entry, including unrelated and foreign files, so their metadata changes also invalidate the result. Operational quiescence must cover this whole observed root, not merely current-profile journal writers. Return source roles, paths, validated evidence and accounting, not a ready flag or partial success.
- `Cross-family identity`: Carry the validation-only known bot-ID constraint across families as well as across each family's retained files. The family reader may expose that constraint separately from its unchanged snapshot identity; do not enrich stored identities or recovery keys. This must catch contradictory IDs present only in segments while both snapshots omit the field.
- `Unclassified storage`: A `recovery` entry initially blocks this inventory without archive traversal. Unknown journal-shaped temporary/legacy residue also blocks. Relaxation requires an audited proof of non-consumption or separately authorized reconciliation, not treating a directory name as permission to ignore accepted work. Arbitrary/out-of-namespace durable consumer references remain a separate mandatory reconciliation gate; a canonical directory census alone cannot establish their absence.
- `Integration boundary`: Leave ordinary discovery, production factories and consumers unchanged. Caller-proven serialization/quiescence remains mandatory; repeated metadata/census checks do not defeat hostile same-user substitutions. Tests must cover exact shared-budget boundaries, default/named namespaces, foreign-file non-reading, ambiguous names/types/links, absent and orphaned families, hidden cross-family ID conflicts, observable census changes, unchanged evidence, and unsupported-platform refusal. Reader/writer closure and final grant-time contention follow only after this evidence layer is independently reviewed.

#### Source Closure Audit Boundaries

Source audit confirms that canonical inventory is not yet usable as a production readiness check:

- `Profile path defect`: `lib/extension.ts` passes `resolveTelegramUpdateJournalPath` and `resolveTelegramWorkspaceAdmissionPath` directly to profile-only ports, although both functions take `(agentDir?, profileName?)`. For a named profile they derive relative `<profile>/tmp/telegram/inbox.json` and `workspace-admission.json`, not the canonical suffixed files. Pure probes confirm cwd-dependent locations; no live contents were inspected. The same relative spelling can refer to different physical namespaces. Do not silently correct the adapters and leave accepted work or admission leases behind; historical-location and reference reconciliation must precede activation or redirection.
- `Reference coverage`: Inspected receipt/handoff callers select exact active lifecycle bindings rather than opening arbitrary receipt-supplied paths. Historical Workspace keys feed the canonical follower-path hasher; arbitrary path resolver wiring currently receives ordinary discovery results. No automatic reader of quarantined journal contents was found. Provision-recovery metadata and follower state hints do have automatic readers, but do not replay journal entries. None of these observations authorizes relaxing recovery refusal or proves live-reference completeness.
- `Consumer ordering`: Replacement now snapshots originating runtime/recovery key values before awaited shutdown, rejects changed/missing bindings afterward, and uses a fresh matching descriptor before worker construction or recovery reads. Startup replacement and forced transport replacement have held-stop regressions, an old-code negative control and independent mutable-descriptor probes. Legacy same-key reuse without replacement remains unchanged; matching keys alone do not establish session/registration lifetime or compatibility of captured worker dependencies. Terminal retry and dead-owner cleanup precede worker start. Status and polling bootstrap also use recovery-capable reads. Workspace protection's reader port is constructed/exposed, but the audit found no active production invocation; it remains a gate before connection, not evidence of a running retirement loop.
- `Quiescence gaps`: Worker stop aborts/awaits draining but can leave unsettled handlers. Config append guards do not serialize other journal mutations or recovery. Whole-root churn also includes transaction staging before acquisition, state staging before owner-fenced rename, logs, admission ledgers, recovery metadata and endpoints. Holding config or owners alone does not quiesce that root. A journal transaction itself creates an `inbox`-containing name rejected by inventory, so acquiring journal locks and then invoking this census is not a supported composition. Metadata stability cannot protect an unguarded interval through grant publication.
- `Preparation prerequisite`: Independent prototype probes falsified fresh-source preparation followed by reuse of lifetime-expired worker ports, and stopped donor readiness after offer/transfer/discard. Durable settlement CAS cannot undo control execution that precedes settlement. Complete queue wiring instead rejected the unprepared pending-mutation query, preserving safety but not proving accepted-work progress. These are bounded prototype counterexamples, not evidence of production duplicate execution. Defer prepared-mode integration until captured dependencies, executable receipt projections and pending-mutation evidence have coherent owners; retaining bytes or stopped-worker maps alone is insufficient. Preserve exact cancellation/settlement authority and actual unsettled-handler barriers across retries. Keep source inspection/grant wiring disconnected until reference, writer and migration closure is proven.

#### Source Operation Serialization

`config.withSourceSerialization(operation)` is a lock-only prerequisite with completed isolated implementation review. Tests cover unchanged config/cache across absent, corrupt and changed-authority states, callback result/error preservation, release/retry, and actual cross-process contention with observation, paired admission and owner-fenced grant. It invokes trusted synchronous code under the existing private config transaction without reading/adopting config, validating the active profile/token/sender, or exposing credentials or a lock capability. The callback's own result is not authority. Acquire required Workspace admission first; never nest config admission or acquire owners inside it. Promise-returning callbacks are outside the contract: their continuations run after lock release, not as protected asynchronous transactions.

The opt-in journal-store composition now separates continuations rather than adding a second prepared-worker projection. Independent review covers all 12 stateful methods, source-callback rejection before journal locking/read/recovery, actual contention, publication-failure release/retry and unchanged receipt CAS:

- `Append`: Canonicalize once → Workspace admission → choose polling observation or paired-human admission → private journal transaction. Without a sender-admission mode, use the lock-only continuation when configured. Do not wrap an admission callback in another config transaction.
- `Other store operations`: Lock-only config serialization → opt-in `sourceAccess` family acquisition → private journal transaction → direct consumption of that operation-local evidence. The same ordering applies after append's selected admission continuation. Acquiring before journal staging is valid only when source serialization excludes all participating family writers through consumption. Without `sourceAccess`, ordinary recovery-capable `readCurrent()` remains unchanged. Receipt/owner/handoff CAS and process-birth proof stay authoritative; no caller-supplied lock-held bypass or ambient reentrancy depth is exposed.
- `Resource binding`: Participating continuations must use the same actual config resource. Arbitrary injected callbacks cannot prove that themselves; real factory/operation tests must establish one acquisition per operation. The dispatcher is implemented and locally reviewed. `sourceAccess` requires the lock-only continuation and captures an independently approved directory plus physical inspection limits; its store integration is independently reviewed, including generated-state refusal remediation. Production wiring remains absent.
- `Evidence`: Real isolated probes show an outer observation plus existing admitted append times out on nested config acquisition. Current-token admission can reject an otherwise valid old-source receipt completion after token rotation; a lock-only transaction still permits exact receipt CAS while rejecting a wrong acquisition ID. Current observation also permits profile-switch/revocation cases, so it is not a lifetime fence. A journal `read()` can repair an empty foreign snapshot while another process holds config, proving append-only coverage is insufficient. These are storage/order witnesses, not permission for obsolete handlers to execute.
- `Remaining gates`: Serialization does not provide exact reference reconciliation, old-token migration policy, settled-handler or legacy-snapshot barriers, executable receipt freshness, whole-root quiescence, all-writer closure or final-grant readiness. The whole-root census cannot run inside a journal transaction. Final grant requires an internal already-in-config-transaction check, not reacquisition of this wrapper. Production stores, inspectors and UI remain disconnected from this candidate.

#### Production Journal Reference Inventory

The current composition has one factory owner: `createTelegramUpdateJournalBindingRuntime()` in `lib/extension.ts`, and it always constructs the legacy `createTelegramUpdateJournalStore()`. Its leader resolver is retained by lifecycle worker assembly, Workspace retirement protection, polling offset reads/cutover, and bootstrap entry inspection. Its follower resolver is retained by follower lifecycle assembly. Recipient/path resolver factories are retained by Workspace retirement discovery and queue-handoff reconciliation. These are readers and/or writers sharing dynamically selected profile paths; none exposes a close acknowledgement, and resolver creation itself is not a lifetime lease.

This inventory closes only repository-visible production composition references. It does not prove external package callers, old running builds, arbitrary injected resolvers, or historical path consumers are absent. Consequently no component may mint `writer-exclusion: excluded` yet. Closure requires explicit lifecycle-owned reference registration/release for each listed class plus mixed-version process evidence; a source census, singleton role, or process-local inventory is insufficient.

#### Strict Source Consumption Gate

Read-only design probes establish that reference admission must precede journal transaction staging: the existing transaction helper can create a missing parent before an inspector invoked in its callback rejects a relative or out-of-anchor reference. An absent anchor/intermediate directory is not absent-source proof. Any bootstrap is caller-owned; the journal reference check does not establish Workspace-ledger path authority.

Strict consumption cannot be implemented as inspection followed by ordinary reading. Probes reproduced empty-profile rebinding, replay/publication enrichment of a token-only snapshot, and different computed receipt scopes despite compatible inspector constraints. Conversely, token rotation can preserve a known-ID receipt scope while failing exact token inspection. The opt-in store consumer now rejects selected-schema/scope mismatch and consumes acquired evidence directly, preserving stored identity rather than merging it during publication. Before any write, the generated complete file passes the existing schema validator for both v1 and v2; an invalid cursor or overflowed attempt count refuses the transition rather than adjusting authority or corrupting subsequent reads. Legacy mode retains its existing validation behavior. Logical-state capacity and `serializedBytes` include the resulting revision and remain separate from physical inspection accounting. Validated operation-local segment names/sizes/raw work and snapshot revision drive compaction planning without ordinary rescans. Before publication, budgets cover segment-first state and snapshot replacement with complete cleanup-failure residue; every subset of that residue is bounded. Existing compaction triggers remain, and an existing redundant segment is retained when it is the bot-ID witness absent from a token-only snapshot. Segment staging is a sibling of the snapshot, outside the strictly enumerated segment directory. These siblings do not establish namespace readiness. Capacity refusal preserves evidence but does not promise progress under insufficient limits. This consumer is independently reviewed but remains unwired; migration is not implemented.

A fault-injected unrelated entry creation invalidated family inspection while both config and journal locks were held. Independent review also reproduced refusal after intermediate permissions changed and were restored, and after an initially absent snapshot was created and deleted. A `dev`/`ino`/`mode`-only ancestor comparison would lose these metadata witnesses; inode equality does not establish continuous identity, and directory descriptors alone do not freeze names, permissions or ACLs. Current checks remain sequential endpoint evidence, not continuous absence or immutable namespace proof.

Keeping the current observation contract requires externally established exclusion of relevant ancestor metadata/namespace changes through consumption, including already-issued asynchronous staging and contenders. Authorized publication changes metadata intentionally and retains its own authority boundary. No production mechanism currently establishes this exclusion for the shared root. Infrastructure errors latch worker blocking; an explicit live-worker signal clears that latch, but retry timers do not, and failed receipt completion is not automatically retried. Byte preservation alone does not establish progress.

The approved direction separates controlled recovery/migration from live source consumption. Existing full family and whole-profile inspectors retain their current evidence contracts. The locally implemented, independently reviewed and unwired `readTelegramUpdateJournalSource()` shares their family decoder/acquisition machinery and requires selected version `1 | 2`, exact profile/token constraints, and equality between expected and unchanged stored receipt scopes. Its caller must serialize relevant cooperating writers through consumption. Snapshot/segment evidence remains strict; ancestor observations establish canonical directory type and endpoint `dev`/`ino`/`mode`/`uid`/`gid`, plus a final anchor realpath check, not detection of every sibling-entry mutation. Concurrent manual relocation, backup restoration, permission/ACL changes or other storage manipulation outside the protocol is unsupported; transient ancestor-change detection is deliberately not promised. This is a declared narrower contract, not equivalent protection inferred from inode equality. No reader alone proves whole-profile readiness or permits activation.

`/telegram-connect` already makes one guarded recovery/retry attempt after startup failure. Its current recovery handler classifies owners/state/transaction artifacts, refuses a verifiable live owner, suspends local polling and serializes reclassification/quarantine. This bootstrap may precede obtaining leader/singleton ownership; requiring an already acquired role for repairing its damaged ownership file would be circular. Being first or becoming leader does not establish shared-root quiescence. Future journal recovery/preparation belongs in an explicit, authority-fenced startup stage, not incidental live reads; existing journal recovery inside ordinary `readCurrent()` remains unchanged until the new path is integrated. Preserve unknown accepted source evidence rather than treating runtime-artifact recovery as journal migration permission.

#### Bus/Journal Design Acceptance

The operator requires reliability and elegance together: minimize independent mechanisms and states while retaining explicit owners and demonstrable safety/progress. The earlier syntax-only comparison favored cursor-ordered, exclusion-bearing polling v2 and cursorless, paired-only follower v1: uniform syntax removes neither admission policy, consent checks nor receipt ownership. That finding constrains rollout but does not resolve custody; the approved ownership correction below supersedes its no-redesign recommendation. A new discriminator, version, ledger or wrapper must remove more conceptual burden than it introduces, including rollout and recovery; superficial uniformity is insufficient.

The comparison exposed a delivery-guarantee boundary independent of schema: retained follower entries deduplicate, but exact receipt completion can remove that evidence and a later identical delivery can be admitted after store reconstruction. Coordinator probes first verified re-admission with actual config, paired admission, journal and follower admission components. A subsequent owned IPC proxy dropped actual ACK bytes between the real forwarder and authenticated receiver; real admission workers/update routing then invoked the authorized callback sink twice for one source, with the leader retaining retry-wait until its successful retry. A queued-message variant also committed a real worker receipt, completed its prompt handoff, and admitted the retry under a new acquisition for the same source. Both variants used injected authorized handlers; actual Pi queue dispatch, model execution and Telegram API effects were not exercised. Uniform entry syntax would not fix the independently settled copies. The next contract must preserve one execution owner across retries, restart and role changes; a fabricated follower watermark, expiry or memory-only set is insufficient. This is narrower than exactly-once external effects across a crash before completion is recorded.

Inline counterexamples reject two standalone remedies: follower completion retention, and delaying follower execution until the polling origin disappears. Both work in a narrow model where only an accepted ACK retires the origin, but both fail when replay may instead execute locally. An owned IPC variant changed the routing runtime's current instance to the same stable recipient before retry: real routing bypassed the receiver, executed the primary source locally and removed it; draining the still-pending follower copy then invoked its sink again. This simulates the role-dependent routing branch, not actual election or process promotion. Origin absence proves neither a specific delivery handoff nor that another source copy remains unexecuted. The probe-only gate/sentinel and explicit wake must not be copied into runtime code; independent waiting-entry progress was not implemented.

A further owned probe uses the actual runtime binding and owner/lifecycle/worker assembly, rather than changing an existing routing runtime's identity. After one lost ACK, the recipient finishes its follower source; the old leader stops; registration becomes false and leader authority becomes true for the same recipient instance/context. Starting the assembly's leader lifecycle executes the retained primary source locally. Both journals finish empty after two sink invocations and only one forwarding request. Transport/election authority remains fixture-supplied: this is not OS election, live promotion, Pi model or Telegram API evidence. The inspected production promotion path stops registration and starts locked polling; the assembly independently binds the two journals and does not reconcile their delivery custody.

The correction therefore needs durable delivery custody, not merely a completion cache or an origin-presence predicate. Existing queue handoff already changes exact ownership in one journal: offer freezes the donor, acceptance CAS installs the recipient, and an ambiguous ACK cannot restore donor authority. Its API requires an already queued receipt; raw input must not masquerade as a queued Pi prompt/control. The operator approved continuing with one authoritative input record and explicit execution ownership, with bus delivery transferring/reference-waking that authority instead of creating a second executable copy. This is logical uniqueness per input, not a requirement to combine every profile into one physical file. Legacy journals and their receipts remain intact until separately authorized reconciliation. The ownership contract below governs the next local implementation slice; storage encoding, raw-owner recovery and lifecycle integration remain separately evidenced, and no production activation is implied. A disconnected worker-facing adapter now proves the first integration seam: acquire and start precede handler execution; complete removes exact custody; a single-input queued outcome atomically becomes its durable queue receipt; deferred returns the running receipt; and an ambiguous running claim never executes again. A session-scoped extension retains deferred receipts and atomically groups them into one queue receipt; consumed raw receipts cannot later settle. Late deferred complete/grouped-queue outcomes require that same retained session receipt, so stale or missing callbacks fail closed. Handler failure and fresh-session observation of retained `running` discard local settlement capability, preventing synthetic completion of outcome-unknown work. A disconnected custodied admission handle now runs the existing registry/default routing inside that session; immediate and late reports settle through the same exact receipt authority. Lifecycle bindings may opt into a strict v3 worker port: v3 snapshots require custodied execution, legacy raw completion/queue/failure methods throw, and custodied handler failure remains blocked running work rather than legacy retry state. A gated resolver reads no v3 dependency while disabled; when enabled, its runtime identity binds both source runtime and recipient execution binding so either change forces lifecycle replacement. Real-v3 assembly evidence preserves retained running input across follower registration-generation replacement and follower-to-leader promotion without another handler call or settlement. Worker selection skips running, foreign-ready, and handoff-frozen inputs while draining independent tail; each custodied commit yields and rereads durable state before unresolved work reports `execution` or `input-custody`. Diagnostics expose only update ID and `running-outcome-unknown`, `foreign-ready`, `handoff-frozen`, or `legacy-retry-state`, never owner, acquisition, binding, payload, path, or token details. Running outcome-unknown wins mixed-class priority. Legacy retry/failed state under v3 is quarantined while independent pending tail drains; it is never replayed through the legacy failure policy. A standalone disposition authority now hashes immutable failure metadata without retaining update payload and permits only exact `requeue-v3|discard` operator intent. Claimed/provenanced entries and generic terminal `retry` are excluded. The bounded segmented operator-disposition audit now accepts a discriminated legacy-custody record, avoiding a second log. Its v3-only mutation is reachable only through an injected operator authorization seam and profile admission: exact evidence atomically becomes unclaimed pending for `requeue-v3` or is removed for `discard`; duplicate authority is idempotent and collisions fail closed. Production omits authorization. The strict worker port now constructs an explicit four-method custody projection rather than returning the structurally narrowed backing store; runtime reflection proves operator listing and disposition are absent. This closes a former capability leak hidden by TypeScript's width subtyping. The disconnected operator runtime requires a caller-owned exact binding-reference scope around every list/apply call and retains no store reference. The scope must span the operation, not only lookup, so retirement/pruning cannot race a stale binding; removal, replacement or recovery mismatch fails closed. The existing bounded reference registry now has an `operator-disposition` class for this composition, but remains process-local evidence only. No production or Telegram command owns this runtime. A source invariant forbids the composition root from acquiring writer closure, installing protocol mode, executing cutover or migration publication, supplying legacy disposition authorization, or constructing the operator runtime. A separate strict non-repairing source-inspection port returns only update ID, retry/failed state, attempt count, bounded failure class and evidence SHA-256; it does not enter writer/source mutation serialization; update payload, failure summary and execution authority remain inside the journal owner. Duplicate reconciliation normalizes exact retained authority before invoking the injected authorizer; malformed extra fields never cross that boundary. If snapshot publication is commit-unknown, exact retry finds the durable audit and performs no second transition; a new disposition cannot target the resulting pending entry. Running custody still has no admissible legacy evidence. A real-v3 replacement regression begins with retained `running` and proves neither the old nor replacement session can execute or settle it after recipient identity changes. Source `recoveryKey` and recipient execution binding are distinct mandatory identities. An assembled regression proves late reports from two deferred sources publish one grouped durable queue receipt. Grouped late reports retain one exact duplicate result for each remaining source callback; the worker accepts the same already-published queue authority idempotently and rejects conflicts. Lifecycle assembly selects this path only for an optional binding-owned custody port. Its strict v3 worker adapter permits exact queued completion but throws before legacy raw completion, queue, or failure mutation; handler errors remain running outcome-unknown. The worker consumes already-durable results without legacy completion/queue writes, synchronizes late claims, and lifecycle assembly selects this path only from an optional binding-owned custody port plus exact binding key. The strict v3 binding also exposes queued receipt offer/accept/cancel through serialized admission wrappers; lifecycle lookup now proves exact recipient acceptance and exact duplicate retries return the same owner. Recipient worker projection is queried only with the matching journal binding key and survives lifecycle replacement. These APIs transfer Pi queue authority and remain distinct from raw input handoff. Cancelling a frozen raw handoff and signaling the worker makes the input executable and clears its redacted blocked projection. A legacy journal writer cannot clear quarantined v3 retry evidence because strict source-version selection rejects that mutation; any future operator disposition therefore needs explicit v3 authority separate from worker settlement. A disconnected bus admission mode validates delivery, source and recipient identity, then wakes the existing durable source reference without journaling the forwarded carrier as a second executable copy. Duplicate delivery can repeat only the wake; stale registration generation cannot wake, and selection fails closed without wake authority rather than falling back to copy admission. The gated delivery identity carries an optional bounded source `recoveryKey` outside the unchanged legacy delivery hash. Reference mode requires that key, bounded exact acquisition/handoff IDs, and explicit authenticated-transport proof; missing or mixed legacy evidence is rejected before wake. Legacy copy mode does not require or infer these fields. Wake carries the complete reference into a disconnected binding-owned resolver. It requires exact recovery key, recipient binding/live owner, update, acquisition and accepted handoff on one unfrozen `ready` claim before signaling; stale, running, missing or mismatched evidence cannot wake or mutate. An authenticated receiver composition against a real shared v3 journal now proves exact and duplicate delivery only signal the accepted ready claim; stale acquisition or registration generation cannot wake or fall back to copy admission. A disconnected sender selector requires mutual `input-custody-reference-v1` capability, and its constructor accepts only an exact accepted handoff when producing recovery/acquisition/handoff evidence. The dedicated `leader.wakeInputCustody` envelope carries no executable Telegram carrier and is forced through authenticated reference admission; legacy durable admission rejects it. A shared-v3 regression composes accepted handoff, leader resolver, mutually capable forwarder, authenticated payload-free receiver and exact wake; exact request replay reuses the cached ACK without another signal. A recipient adapter resolves recovery/binding/live-owner authority, performs exact acceptance CAS, verifies the returned handoff, and only then exposes the reference and signals. The payload-free `leader.offerInputCustodyHandoff` envelope carries exact source/handoff and recipient lifetime evidence into it; parsing, authentication, capability, binding and handler gates fail closed, while duplicate acceptance returns durable duplicate evidence. A real-v3 composition runs donor offer-before-send through the authenticated handoff receiver; acceptance CAS signals once, and client replay reconciles locally without another request. No immediate wake follows acceptance because it would duplicate signaling; payload-free wake remains only an idempotent recovery nudge for already accepted custody. Unknown outcomes remain frozen; after recipient acceptance, retry first reconciles the accepted shared-journal reference and returns duplicate settlement without re-offer, transport, or new authority. Full handoff-then-wake request composition remains disconnected. Frozen pre-accept lookup yields no reference, preventing transport. Missing reference fails retryably before transport; mixed peers retain legacy carrier delivery. A disconnected leader resolver reads shared v3 state without mutation and returns a forward reference only for the exact recipient-owned unfrozen `ready` claim; donor-frozen, running, stale binding or owner mismatch returns no reference. A single optional custody bus bundle derives acceptance, wake and forward-reference resolution from one recovery-key lookup carrying the same recipient binding/live owner, journal and signal authority. Lifecycle assembly/runtime binding exposes that bundle only when supplied; losing its active authority disables lookup and makes acceptance/wake fail closed together. A follower transport adapter rereads the optional bundle for every capability check, acceptance, wake and forward lookup. Removing the bundle on downgrade/reconnect simultaneously disables capability/resolution and makes stale operations fail closed without cached authority. A disconnected receiver assembly rereads these ports across downgrade/reconnect: bundle removal rejects before handlers, replacement generation uses only the new bundle, and stale generation cannot call either bundle or legacy admission. Live target ownership now carries the exact current registration protocol identity into forwarder selection: capability upgrade enables reference mode and downgrade returns to legacy, while persisted records grant neither mode. Forwarders may additionally revalidate current ownership after reference resolution and immediately before transport. If generation, binding, or protocol changed, they return `recipient-ownership-stale` without sending, preventing an old capable snapshot from crossing reconnect. Production follower-client composition uses one canonical live-registry validator immediately before transport. Removal, same-generation protocol replacement, or generation replacement invalidates old ownership; only a fresh current projection passes. The receiver generation check remains an independent second fence. A post-accept retry using stale ownership is rejected before transport after registry replacement, so it cannot reach either receiver execution or its cached ACK. Production still omits the custody bundle and capability; A disconnected readiness evaluator requires requested absent/v3 source, proven legacy-writer exclusion, completed historical migration and capability-ready peers; all mixed, legacy, unsupported, ambiguous or unknown evidence returns a bounded blocker. A disconnected lazy resolver reads no source/peer evidence while disabled or blocked by writer/migration proof, maps inspection loss to source-unready and inventory loss to capability mismatch, and creates no journal/lifecycle binding. Read-only adapters now normalize real strict family inspection as absent/v3/legacy/ambiguous and live follower generation/protocol inventory as ready/legacy/unknown; corrupt source and missing live identity fail closed without mutation. A real disk/registry composition rereads on every call: absent/v3 plus peers advertising durable admission and custody reference enables; peer downgrade, source corruption, or missing identity blocks; replacement upgrade re-enables. Migration and writer exclusion now enter through typed versioned evidence bound to exact profile/recovery identity; missing, unknown, incomplete, present-writer or mismatched evidence blocks before source/peer inspection. A disconnected retained-evidence store adds a bounded strict v1 codec, revision CAS under injected serialization, kind-specific validation and injected publication authority. Malformed, stale, unauthorized or cross-kind evidence never reaches the persistence port. Writer and migration records in one snapshot must share exact profile/recovery identity; even authorized mixed-identity publication fails before persistence. No current component may mint writer exclusion because existing census/serialization cannot prove arbitrary writer or consumer absence. Lifecycle runtime now admits an injected logical source lease: bind acquires, replacement releases/reacquires, shutdown releases despite retaining a reusable binding object, and restart reacquires. Release failure cannot retain runtime authority. One bounded exact-release process-local registry now backs production leader/follower lifecycle leases; capacity and stale release fail closed, and shutdown/restart/replacement update inventory. It is neither durable nor whole-root proof. The registry also scopes sync/async operations and releases on return, throw or rejection. Production status/polling cursor reads, cursor cutover and bootstrap entry reads now use short-lived leader leases; missing binding acquires none. Workspace-retirement protection now scopes shared, retained-binding and discovered journal reads through the same registry and releases before planning continues, including read failure. A composition invariant now rejects direct resolver journal reads in the entrypoint. Remaining production resolver uses are leased lifecycle bindings, scoped polling/retirement operations, or queue-handoff recovery-key computation without journal I/O. Composition invariants reject raw store construction in `index.ts`, require follower durable writes through the leased lifecycle, scope the sole cursor append, and prove current root/API exports expose no journal mutation factory. Old-process exclusion now has a pure evaluator, but not an inventory authority: exact profile/recovery identity, caller-proven complete writer list and process-birth liveness are mandatory. Alive means present; mismatch/incomplete/unverifiable means unknown; only every listed birth proven dead means excluded. Registry absence is never death. The journal transaction owner now exposes an optional outer writer-admission seam through all binding factories. It gates append, mutation and ordinary read (which may repair) before source serialization/journal locking; denial writes nothing, while strict inspection remains read-only. Production leaves it unset. The approved preparation direction is one third Workspace-ledger destructive kind, `journal-writer-closure`, not another ledger and not a cleanup/retirement alias. Its discriminated payload binds profile/recovery identity and closure operation ID but has no target, deletion permit or absence phase. Acquisition requires zero ordinary leases; while held, ordinary profile admission fails. The journal writer seam will acquire/release one ordinary profile lease before config/journal locking, so closure and writers serialize through ledger transactions without nesting the config transaction. Current lock-order regression proves denial never enters pairing/source serialization and allowed append enters writer admission first. Legacy missing-kind fences remain pressure retirement. The third kind and strict identity-only payload codec reject deletion fields and identity drift while retirement/cleanup fence types remain narrowed. The top-level ledger union is integrated across admission, retirement coordination, and cleanup ports: closure blocks ordinary admission and competing destructive work globally, owns no Thread slot, and every deletion operation narrows the kind before accessing deletion authority. Compile-time deletion fence/permit authority is also a dedicated retirement-or-cleanup union; `journal-writer-closure` cannot inhabit it. Exact closure acquisition/release is now available on the ledger but remains disconnected from production. Acquisition requires zero leases, resumes the exact durable authority after lost ACK without republishing, rejects same-operation drift, and blocks other destructive work; release requires exact current-owner authority. The optional journal-writer adapter acquires a profile lease before entering the journal seam and retains its ID after ambiguous acquisition. Release failure is diagnostic-only after the journal operation settles: retaining a possible lease blocks closure safely, while throwing from `finally` would falsely erase the known operation outcome. The existing binding runtime propagates one supplied adapter to leader, active follower, recipient and path-discovered journals; regression coverage closes all four classes. Production omission still means no writer closure activation. Writer-exclusion evidence cannot yet be minted truthfully inside this fence: releasing it allows a newly starting legacy writer, retaining it blocks v3 writers too, and retained v1 `excluded` evidence records neither closure operation nor startup authority. Closure publication therefore requires a prior durable operator-authorized startup-exclusion authority plus protocol-class admission semantics. A strict standalone authority schema now binds profile/recovery, closure operation, inventory SHA-256, operator authority ID, explicit enforced/revoked status, authorization time, and `custody-v3` as the sole allowed protocol. It is retained by the readiness store through strict revisioned CAS, injected publication authorization and cross-kind identity checks. The full candidate is decoded and normalized before authorization, so malformed/extra evidence never crosses the authority callback; enforced and revoked states are retained exactly. Proven readiness now consumes it fail-closed: `excluded` writer evidence must link the same enforced authority ID, closure operation and inventory digest. Legacy unlinked evidence, revocation or any link drift blocks activation, and the store rejects conflicting linked authorities. Proven readiness also requires the durable installed protocol mode to match the same profile/recovery, startup authority, closure and inventory digest. A crash before mode installation or before linked exclusion publication therefore remains disabled. The non-nesting operator coordinator now enforces the safe order: verify retained enforced authority → evaluate exact inventory/liveness under closure → atomically install mode → reread authority → CAS-publish linked exclusion. A pre-publication revocation or race leaves mode installed but no readiness evidence, so activation remains disabled; exact retry reconciles installed mode and existing evidence without duplicate authority. Production has no caller. Migration completion now has a standalone strict authority binding the same cutover identities plus a complete historical source/disposition digest, resulting `absent|v3` family, operator authorization time and explicit revocation. It is now retained through authorized revisioned CAS and consumed by proven readiness only when migration evidence links the same migration/startup/closure/inventory authority. Revocation, legacy unlinked evidence, or a live source family different from the authorized `absent|v3` result blocks activation on every resolution. The disconnected migration publisher rereads retained authority, compares the exact historical inventory digest and inspects the live source before CAS-publishing linked completion. Exact retry is revision-stable; it performs no migration or source mutation. Its strict standalone Workspace mode codec now binds `custody-v3` to exact profile/recovery, startup authority, closure operation, inventory digest, installer owner and install time. The ledger now retains it by atomically replacing the matching zero-lease closure; fence+mode state is invalid and exact lost-ACK installation resumes without republish. While mode exists, generic `journal-write` and new closure acquisition fail closed. Dedicated v3 writer admission must present matching recovery, startup authority, closure and inventory digest; operation kind alone never grants access. While mode is active, generic `journal-write` and `journal.*` admission requires an already-held same-owner dedicated v3 profile lease. This permits nested append/input admission under the outer writer seam but rejects caller-chosen journal operation names as authority. Production installs no mode. Mode→closure publication is crash-proven: after an acknowledged-unknown atomic rename, a fresh ledger resumes the exact closure without invoking authorization again or restoring mode. Re-closing an installed mode is possible only through an injected production-unset authorizer: with zero leases, one ledger transaction replaces the exact recovery-bound mode with a new closure; active writers block before authorization and denied callbacks leave mode unchanged. Exact closure retry handles lost publication ACK. A compile probe established that this must be one compatibility cohort across the ledger, retirement coordinator, and cleanup-manager ledger port; publishing a wider snapshot before those consumers narrow by kind is forbidden. Required slices are: generalize fence payload codec without changing existing kinds; add closure acquire/exact release only; wire writer admission; then allow complete inventory capture and evidence publication while the same fence remains held. No step may infer external writer absence. Exported legacy factories and arbitrary installed-package consumers remain outside process-local proof; writer-exclusion minting therefore requires package/API closure or explicit retirement, and rollout stays gated. Optional lifecycle exposure and replacement invalidation remain disconnected. Production bindings expose neither port. Its optional late-settlement callback lets the worker clear an exact deferred projection or register already-durable queue authority without repeating a legacy journal mutation. The real worker drain accepts an optional already-settled custody result: completion rereads durable state without legacy removal, while outcome-unknown blocks without creating retry authority. Production still selects only the legacy path.

##### Input Custody Contract

- Identity: The actual source journal binding and update ID identify the input; an exact acquisition identifies its current execution owner. Neither current leader/follower role nor a request ID creates a new acquisition. Obtain source references from the worker's bound authority, never from a caller-supplied path or source-ID field alone.
- Admission: Preserve the existing durable admission, immutable exclusion and ordered-source replay veto. A bus receiver validates sender, provenance, recipient binding and lifetime before it can accept ownership; source identity or lock possession is not consent.
- Execution: Persist ownership before semantic processing. A local projection may schedule work but cannot independently authorize it. Role replacement must resume or reconcile the existing ownership, not reconstruct the original input as fresh local work.
- Transfer: Reuse the existing owner/acquisition and offer/accept discipline. Freeze donor execution before offering; acceptance CAS changes ownership of the same source record. Preserve the exact forwarded execution payload and recipient binding. A missing ACK does not cancel an accepted transfer or restore donor execution; repeated requests observe the same acquisition rather than minting another.
- Settlement: Only the exact current ownership and phase can record completion, failure, transfer or transition into a Pi queue receipt. Raw input is not a queued Pi prompt/control. Queue admission must preserve the source reference and remain atomic for grouped source IDs; stale raw-owner settlement cannot erase the resulting queue authority.
- Completion: Once confirmed completion is recorded, the existing ordered-origin replay barrier prevents re-creation. A late reference/notification cannot recreate input from its payload. Missing, malformed or mismatched storage is not completion evidence.
- Recovery: Distinguish work proven not to have started from work whose outcome is unknown. PID death, role replacement and a fresh registration do not prove an external action was unexecuted. Select and validate raw-owner recovery separately; do not inherit queued-owner discard or automatic raw replay merely because those paths already exist.
- Progress: Waiting or foreign-owned entries must not block independent local input. Admission must leave logical and physical headroom for required ownership transitions; capacity refusal preserves data but does not prove drain progress. No mutex spans a handler/UI/transport await. Replacement must account for actual unsettled handlers and pending mutations; do not restore discarded prepared-worker readiness projections.
- Compatibility: Ownership-aware storage/consumers must reject interpretations that would turn an owned input into ordinary executable pending work. Keep old formats, receipt scopes, files and production factories unchanged until explicit versioning, source-reference reconciliation and capability-gated rollout are validated. The guarantee does not cover exactly-once external effects across an unrecorded effect/commit crash.

The custody storage codec is v3. It retains v2's required cursor and immutable exclusion evidence, adding an optional `inputClaim` with `ready`/`running` phase, existing exact owner/acquisition fields, a recipient binding bounded to 256 characters, an optional exact execution-update projection, and an optional handoff `{ handoffId, offeredAtMs, recipientOwner }`. A queued v3 entry may instead retain `inputProvenance` with the former exact owner, binding and projection; this is immutable transition evidence, not concurrent raw execution authority. Excluded input cannot carry a claim; raw and Pi queue authority cannot coexist on one entry; running claims cannot be retry-wait/failed. Snapshots and all retained segments share the decoder. Unsupported claim phases/fields remain rejected rather than speculatively interpreted.

The opt-in `createTelegramInputJournalStore()` reuses the journal's private transaction/publication owner, not a second ledger. It requires strict source access, source serialization, polling admission, a captured process identity and a bound `getInputContext` port. Besides read/append, it exposes veto-only removal, acquisition, ready-claim release/recovery, offer/accept/cancel handoff, grouped input-to-queue transition, exact queue settlement/recovery, start and exact-receipt completion, not the legacy unowned mutation ports. Each input mutation takes optional profile-wide Workspace admission before config/source/journal access. Acquisition only claims admitted pending input; matching repeats retain the same acquisition and projection. Start checks the exact acquisition, originating session and recipient binding, and grants only one ready-to-running transition. Publication rechecks the captured execution context through an operation-local callback. Completion requires running authority and may settle the original exact receipt after its session disappears; its token fingerprint qualifies the fixed source/receipt binding without changing existing receipt scopes. Reading metadata or receiving `started: false` grants no execution or sender consent.

A successful v3 append independently verifies the remaining publication chain for the worst bounded representative of every currently present next-transition class; `acquire` and `start` then bind that reserve to their exact update. The reserve covers logical journal bytes and strict source files/bytes/work, including both a durable segment whose snapshot replacement is commit-unknown and a replaced snapshot whose redundant-segment cleanup entirely fails. The acquisition envelope uses maximally encoded claim fields, a duplicate of the original update and 4 KiB of additional projection growth; a larger routed projection refuses without changing the raw input. Each v3 mutation requests immediate snapshot compaction. Admission covers either direct start/completion or one `ready → unclaimed → reacquire → start → complete` recovery branch across five retained transition segments. A successful offer separately reserves accept-to-completion, cancel-to-completion and dead-donor release/reacquire-to-completion resolution before freezing the donor. Branch reserves are reused rather than summed: this guarantees one operation-bound progress chain at a time, not simultaneous transitions, repeated ready-owner recovery under persistent cleanup failure, or recovery from unbounded filesystem failure. Settlements are never withheld merely to reserve the next unrelated input.

`releaseInput()` uses the exact source/acquisition receipt and owning process identity to clear only a pending unoffered `ready` claim, preserving the original input. It needs no current session context, so the originating process can finish cancellation after replacement. An unclaimed exact source is a no-op postcondition; another acquisition, `running`, offered ownership, failure/queue state or absent input refuses. `recoverReadyInput()` additionally requires a recovery identity matching the store runtime and checks the exact stored claim before its synchronous process-birth liveness probe. `alive` and `unverifiable` retain authority; only `dead` clears it. An offered ready claim is still proven unstarted: accept and dead-owner recovery serialize on the same record, so acceptance first fences the stale donor receipt while recovery first leaves unclaimed input that a later accept cannot recreate. A liveness error fails closed. Commit-unknown release retries observe unclaimed state without claiming that a handler ran. `running` is rejected before liveness and remains outcome-unknown.

`offerInputHandoff()` requires the exact ready donor receipt plus its current session/binding context. Its stable handoff ID binds fresh token entropy, source binding, update ID, exact donor owner, recipient owner and persisted recipient binding. The token is neither persisted nor needed after publication: the returned durable source reference plus stored handoff ID identifies later resolution, and an exact same-recipient offer retry with fresh entropy returns that existing ID. The persisted offer leaves owner, routed projection and original update unchanged while freezing donor start/release. `acceptInputHandoff()` requires only that source reference, stored ID, named recipient runtime and same persisted binding, then CAS-replaces the donor with one new ready acquisition carrying the handoff ID and removes the offer. A repeated accept before or after start returns that same acquisition; after recovery or recorded completion it refuses and cannot recreate input. `cancelInputHandoff()` requires the donor's exact receipt and stored ID and only removes the unaccepted offer under donor context; after acceptance the stale donor no longer matches, so cancellation cannot restore it. Commit-unknown offer/accept/cancel retries observe the durable phase. These primitives do not send IPC, ACK a bus delivery or invoke a handler.

`queueInputs()` atomically replaces a non-empty exact set of `running` claims from the current process/session/binding with one prompt/control queue receipt. It sorts and de-duplicates source IDs, mints one queue acquisition for the whole group, removes every `inputClaim`, and retains each claim's owner, binding and routed projection only as `inputProvenance`. A retry after commit-unknown publication must match the complete receipt group and every former acquisition, then returns the same queue receipt; a stale owner, partial group, changed kind/receipt, offered/ready claim or foreign context refuses without mutation. Consequently raw completion, release, recovery and start cannot erase or regrant queued authority. The exact queue receipt may complete after input session context disappears, behind the same profile-wide Workspace admission. Queue publication reserves its exact grouped completion through segment-before-snapshot and complete cleanup-failure prefixes; it does not promise capacity for an unknown group before that operation is presented.

`removeExcluded()` requires retained v3 source/cursor evidence and checks the complete requested ID set atomically. Every present requested entry must carry the immutable exclusion veto, including pending, retry-wait or failed entries; any non-excluded input, claimed work or Pi receipt rejects the batch. Missing IDs inside the retained cursor are no-ops, not evidence of prior execution; missing storage or IDs above the cursor refuse. Removal needs neither current pairing nor an execution context and preserves the cursor, all non-excluded work and its diagnostics/ownership. Source inspection and crash-visible publication budgets remain mandatory: a readable one-file budget can refuse the removal segment even though the final logical state would shrink. This path grants no capacity exemption or drain-progress guarantee.

Project-native tests cover competing processes, reconstruction, stale owner/session/binding refusal, immutable exclusions, logical/physical headroom, commit-unknown publication, cleanup refusal, projection bounds, exact ready release/dead recovery, frozen-donor refusal, token-free offer reconstruction, cross-process accept/recovery races, grouped running-to-queue transition, exact provenance retention, stale raw settlement refusal and post-context queue completion. A compaction failure after segment publication can leave a durable running marker even when `startInput` throws: retry does not regrant it. This is commit-unknown, not proof that a user handler ran; no handler is invoked by these primitives. Non-excluded raw failure settlement, bus/worker integration of durable source references and queue receipts, v3 queue-handoff exposure, outcome-unknown running-owner recovery and production consumer support remain unimplemented. V1/v2 stores still refuse v3 before recovery or mutation; current workers, production factories and `index.ts` remain unchanged. The reproduced bus/lifecycle repeat is not fixed by these disconnected primitives.

Read-only migration probes constrain that comparison. Publishing a v2 snapshot over retained v1 segments is unreadable. For a logically empty current v1 state with an existing cursor and no segment-only bot-ID witness, identity-preserving v1 consolidation at the existing final revision → verified redundant cleanup → v2 publication survived simulated interrupted prefixes and all eight cleanup subsets of a three-segment fixture. Dispositions, cursor, revision and recovery scope were preserved. This is a candidate, not an implemented migrator or process-crash suite. Nonempty/unknown authority, missing cursor and a segment-only identity witness require refusal; absent family produces no write or invented cursor. Sibling staging remains outside family evidence but blocks the current full-profile inspector. External references, held handlers, writer exclusion and startup authority remain caller-owned gates.

#### Owners And Validation

- A narrow `pairing` runtime is justified only for the shared candidate/dialog lifecycle used by updates and command/bootstrap paths. It owns bounded ephemeral requests, cancellation and decision state, not durable configuration, journal settlement, Telegram transport, or Pi SDK mechanics. `config` owns durable grants, `journal` owns admitted-input evidence, `updates` owns sender admission before routing, and existing Pi/binding/lifecycle owners supply native UI and session ports. Keep `index.ts` as a thin re-export and `lib/extension.ts` composition-only; approve the minimal import graph before extraction.
- Independently verify the refined schema/migration and lock-seam design before implementing it. First prove durable entry exclusion with UI disconnected, then test the ephemeral lifecycle with an injected clock and controllable dialog/publication promises, integrate all admission paths, and run shared validation plus an independent post-integration review.
- Required witnesses: blocked dialog with worker progress; duplicate and competing requesters; exact expiry and cooldown; reject/ESC/error/no-UI; shutdown/profile/token/owner replacement; late Yes; failed/ambiguous publication; competing configured owner; pre-approval journal backlog and restart; message/edit/callback/guest and unbound/foreign paths; configured-owner compatibility; redacted/sanitized UI; no model turn before grant. Mocked UI is not terminal keyboard/rendering acceptance. Live acceptance requires separately authorized disposable profiles and Threads.

### Runtime Ownership

- `/telegram-connect` acquires or moves the active profile's owner slot before polling starts. `/telegram-disconnect` keeps its destructive confirmation, then stops polling and releases only that exact slot. In Threaded Mode it tears down the disconnecting instance's bound Telegram thread: leaders delete their own thread directly, and followers send an authenticated exact-generation disconnect envelope and wait for confirmed leader cleanup before unregistering. Graceful Pi `quit` always preserves the owner slot as restart intent, allowing a reopened same-`cwd` session to reclaim the stale lease. When `threads.automaticCleanup` is enabled (the default), quit also deletes the bound Telegram tab without releasing that restart intent; disabling it preserves the tab through replacement-style suspension. Failed automatic cleanup records diagnostics and falls back to safe suspension so remaining lifecycle cleanup still runs.
- Session start schedules polling resume asynchronously only when the owner slot already points at the current `pid`/`cwd`, or when a stale same-`cwd` owner can be safely replaced after process restart. Under a live foreign leader, startup instead attempts capability-gated restore-only follower admission when the selected profile has a remembered exact-`cwd` Workspace binding; it never provisions an unremembered Workspace. Startup and `/resume` do not wait on leader election, Bot API probes, poller handoff, or thread reconciliation before restoring the Pi session.
- The polling owner alone bounds `getUpdates`: each request derives its cancellation budget from Telegram's declared long-poll timeout plus 10 seconds of transport grace (10 seconds for the zero-timeout initial sync and 40 seconds for the normal 30-second poll). The request-local controller inherits poller cancellation, rejects its owner at the budget, and fences any late transport result. Ordinary Bot API and media operations do not receive speculative blanket deadlines. Existing caller signals remain authoritative through API retry waits, only retry-safe methods replay explicit retryable responses, and non-idempotent sends preserve commit-unknown evidence instead of risking duplicate mutation.
- Ten consecutive `getUpdates` conflict responses, including initial cursor sync, terminate polling with `persistent-conflict`; a successful response or a different error resets the count. The controller detaches its inner promise before notifying the locked lifecycle, avoiding teardown waiting on itself. That lifecycle stops ownership checks, lease refresh, capability monitoring, typing, and classic or bus transport (including leader health, pruning, and IPC), withdraws local direct authority, and transactionally releases only its exact lock. A failed durable release leaves local authority revoked; explicit reacquisition mints a fresh epoch. Accepted queue receipts and local Pi dispatch survive. One terminal diagnostic distinguishes lost local ownership from a competing client despite an apparently owned lock and reports cleanup failures; ordinary status refreshes retain generic `error` until transport recovers. Remove the competing client, then use `/telegram-connect`; no automatic retry continues after the terminal threshold.
- Manual and automatic polling starts share a lifecycle generation. A later suspend, disconnect, persistent-conflict stop, or accepted start invalidates older startup continuations; reconnect captures its generation before waiting for transport teardown and rechecks it before acquiring ownership and after awaited startup work. Obsolete completion or failure cannot report a successful connection or roll back a replacement. Admission rejects stale/unauthorized contexts before advancing its own startup generation, so a rejected call cannot cancel valid in-flight initialization. After awaited bus startup, thread-aware completion checks its generation before starting leader health or changing fallback/startup-option state; teardown clears health independently of the current mode flag. Startup probes, capability-monitor transitions, and observed-target transitions share an orchestration lifecycle fence and check it inside their effect-owning helpers after awaited queries, persistence, or transport work. Monitor stop also invalidates pending observations. These checks suppress subsequent state changes, fallback, health, and status effects; already-issued API/persistence calls retain their own transport/storage fencing.
- `pollingActive` reports only whether this runtime still owns an unresolved polling lifecycle; it is not health evidence. A separate observable state records `starting`, `long-poll`, `persisting-journal`, `persisting-offset`, `retrying`, or `stopped`, together with phase start, current update id, last successful response time/count, and terminal stop reason. This distinguishes a stuck HTTP poll from downstream update work without a wall-clock stale heuristic.
- Built-in read-only menu commands return after required local mutation and schedule context-fenced rendering and command synchronization independently, so those effects cannot withhold the next inbound offset.
- Pi `print`/`json` run modes stay passive. Inherited child sessions that share `telegram.json` but do not own the exact `pid`/`cwd` slot must not poll or call `getUpdates` unless the operator force-takes ownership.
- Session replacement through `reload`, `new`, `resume`, or `fork` suspends polling/watchers without releasing ownership so the next session in the same process can resume. A registered follower snapshots its assigned target into a short-lived same-process handoff, stops the old receiver/heartbeat, and re-registers through the live leader without marking or replacing its Telegram thread. Hard process termination cannot run graceful teardown, so stale recovery retains its restart-hint path.
- Live external owners require explicit takeover confirmation. Long-lived timers compare against snapshotted owner identity and stop local transport work when the slot no longer matches.
- `owners.json` owns only Telegram transport control. Local extension and accepted queue state remain per Pi instance when ownership moves, but previews, final delivery, dispatch transport mutations, and delayed Bot API work fail closed until exact direct or follower authority becomes valid again.
- Exact ownership remains checked every second, while the durable owner heartbeat refresh runs every two seconds and becomes stale after eight seconds. This keeps replacement detection responsive while halving steady-state atomic `owners.json` rewrites without changing the cross-platform file-transaction authority. Every acquisition, refresh, release, takeover, and stale recovery serializes through the sibling `owners.json.transaction` guard. The guard publishes one private generation-named owner record atomically, validates filename/payload generation agreement, fences stale recovery and delayed release against replacement-owner ABA, and fails closed on malformed state, unverifiable ownership, contention timeout, or unsupported filesystem behavior. The JSON store publishes through a private same-directory temporary file and atomic rename; atomic payload replacement does not replace transaction serialization.
- `owners.json` is authoritative and private. `state.json` remains an observable snapshot, `logs.jsonl` remains diagnostics, and followers remain authenticated bus registrations rather than ownership-file writers.
- Ordinary ownership and state mutations fail closed on malformed files. When `/telegram-connect` itself fails and the recovery classifier finds truncated `owners.json`, truncated profile `state*.json`, or an unverifiable `owners.json.transaction`, it may prioritize runtime liveness: a dedicated recovery transaction serializes contenders, the ownership transaction fences a final reread, and only classifier-approved disposable artifacts move atomically into `tmp/telegram/recovery/<timestamp>-<pid>-<generation>/`. A verifiable live owner or transaction holder blocks mutation. Local polling suspension must complete before any quarantine mutation; failure blocks recovery, while a later ownership-release parse failure may proceed only because final guarded classification still protects any live owner. Quarantine renames use the same bounded `EPERM`/`EBUSY`/`EACCES` sharing retries as ownership publication for native Windows. Stale owner heartbeats older than eight seconds remain replaceable even if the operating system reused their PID. Configuration, logs, and unrelated temporary files never enter the recovery candidate set. The command retries startup once; a second failure becomes one explicit restart instruction rather than another recovery loop.

### Persistence I/O Baseline

The three runtime files have different authority and write pressure. Preserve that distinction when optimizing them:

- `owners.json` is safety-critical transport authority. Acquire, release, takeover, stale recovery, and two-second leader lease refresh mutate it. The steady-state baseline is one cached atomic rewrite every two seconds per active profile, or 43,200 refreshes/day; one-second ownership checks are read-only. Every mutation serializes the full cross-process read/check/write through `owners.json.transaction`.
- `state.json` combines recovery-critical thread/capability state with observational runtime projections. Every explicit thread-store `persist()` builds a semantic snapshot, but an unchanged payload skips directory preparation, temporary-name generation, file creation, and rename after comparing JSON values independently of object-key order and ignoring `writtenAtMs`; array order, value types, and explicit null remain significant, and changed snapshots retain the full atomic replacement path. No-op saves still read and compare current disk state, rather than trusting a cache that could hide another owner's publication. Diagnostics scheduling coalesces requests across a bounded 100 ms window. Polling phase transitions and successful `getUpdates` responses schedule this observational projection, so an idle leader normally produces one changed polling snapshot per completed long-poll cycle. Only the exact transport owner commits; non-owners reload current disk state instead of publishing.
- `logs.jsonl` is fail-soft observational evidence, never routing authority. Runtime events admitted in one JavaScript turn batch by captured profile path into one size check, one profile-wide file transaction, and one append while preserving event order. Batching adds no timer or shutdown-loss window; separate profiles remain isolated, and one failed group does not drop another. Scope reset and rotation retain their serialized copy/replace path. The 5 MiB value is a rotation threshold: an authorized writer rotates between batched records before the next record crosses it, so overshoot is bounded to one admitted record plus reset metadata; a writer without reset authority defers rotation to the owner.

This baseline counts write-producing code paths rather than filesystem implementation details that vary between ext4, APFS, NTFS, and network-backed home directories. Optimization evidence should compare these deterministic triggers first, then use platform smoke evidence for rename, named-pipe, crash, and cleanup behavior. Recovery-critical `state.json` fields are `bot`, `identities`, `workspaceBindings`, `reservations`, `pendingProvisions`, `syncObservations`, and `threads`; `runtime`, `liveRoster`, `diagnostics`, and `writtenAtMs` are observational and may use bounded coalescing when authority checks remain unchanged.

Run `node --experimental-strip-types scripts/measure-workspace.mjs` for an isolated, assertion-backed Thread-store work baseline at 1, 13, and 26 bindings. It creates and removes only its own temporary fixtures; it never loads configured profiles or calls Telegram. Counters cover asynchronous filesystem calls, bytes, and JSON parse/stringify calls, not object-spread clones, synchronous existence checks, transport-owner transactions, IPC, or elapsed-time performance. The script reports aggregate counts with each row's repetition count; the following counts are per operation and were identical across these fixture sizes:

| Operation | Reads / parses | Stringifies | Writes / renames |
| --- | --- | --- | --- |
| Cold load | 1 / 1 | 0 | 0 / 0 |
| Last-record lookup | 0 / 0 | 0 | 0 / 0 |
| First persist after reload | 2 / 3 | 1 | 0 / 0 |
| Steady unchanged persist | 2 / 3 | 1 | 0 / 0 |
| Changed diagnostic persist | 2 / 3 | 2 | 1 / 1 |

The first reloaded save and steady no-op saves both preserve the existing file. Structural comparison normalizes the candidate to wire JSON: one extra parse replaces the former disk reserialization, while object insertion order no longer causes an atomic rewrite. This trades comparison work for fewer unnecessary writes; it is not a latency/CPU improvement claim. At 26 bindings the seeded snapshot is 17,776 bytes, so an unchanged save still reads 35,552 bytes in this fixture. Fresh disk comparison and ownership/revision checks remain unchanged; no cache or index was introduced. Full IPC, authenticated registration, end-to-end routing, and allocation/clone costs require separate evidence.

Run `node --experimental-strip-types scripts/measure-bus.mjs` for the complementary synchronous registry baseline. At 1/13/26 entries, re-registration visits each entry once; heartbeat performs one Map get and set without scanning; target lookup visits one entry for the first target and at most the fixture size for the last or a different-chat miss; roster listing visits every entry. Returned target/protocol views are mutation-isolated from registry authority. The script counts Map calls and visited entries, not allocations or time, and restores its process-local instrumentation before exit. Two repeated runs produced identical counts. This bounded result supplies no demonstrated need for a secondary target index, shared mutable views, or persistent IPC multiplexing. Escalate to full transport/provisioning measurement only when attributable latency, event-loop blocking, or unexpectedly growing work supplies a concrete performance claim to test.

Version `0.24.0` intentionally does not read or migrate the former agent-level `locks.json`; upgrading resets Telegram ownership. Run `/telegram-connect` when a fresh owner is not elected automatically. Current builds automatically quarantine recognized unclean-shutdown corruption when no live owner protects it. Manual removal of `~/.pi/agent/tmp/telegram/owners.json` and its transaction guard is only a last resort after stopping every Pi instance that could own Telegram; never delete the whole agent `tmp/` directory to repair this extension.

### Threaded Mode Multi-Instance Bus

Telegram private-chat Threaded Mode is the public switch for multi-instance Telegram operation. Classic single-DM polling is the base mode. When Telegram private-chat threads are available for the bot, the bridge enables the local leader/follower bus automatically; when threads are unavailable or later disabled, the bridge returns to classic single-DM polling as a first-class mode. Before a non-owner `/telegram-connect` chooses follower registration or singleton takeover, it discards process-local status/capability projections and reads the current owner-published mode: `enabled` registers a follower without a takeover prompt, while `disabled` uses the classic confirmation flow.

Named Telegram profiles are orthogonal to Threaded Mode. The selected profile chooses the bot/session slice (`botToken`, `botId`, `botUsername`, `allowedUserId`) and scopes its durable journal cursor, the `owners.json` slot, diagnostics logs, state files, thread/bus ownership, and leader/follower IPC endpoints; it must not change the Threaded Mode rules. Within one selected profile, leader/follower election, bus transport, thread provisioning, routing, ownership forwarding, cleanup, and runtime diagnostics behave exactly as they do for the `default` slot. A different selected profile is a parallel bot runtime: its owner slot, `tmp/telegram/state.<profile>.json`, `tmp/telegram/logs.<profile>.jsonl`, `tmp/telegram/logs.<profile>._prev.jsonl`, thread bindings, Unix sockets, and Windows named pipes are isolated from the default profile and other named profiles while shared bridge settings remain top-level/global.

Profile reality follows three explicit storage classes. `telegram.json` shared settings and extension registries are process-global platform configuration; `profiles.default` and `profiles.<name>` bot/session fields plus observable transport/routing authority are profile-scoped; queues, active turns, ownership caches, menu state, and runtime controllers are session-local memory. Config persistence serializes cross-process writers and applies each recursive mutation delta to the latest disk snapshot, so unrelated global/profile updates do not stale-replace one another. Each profile's journal transaction independently publishes its monotonic admission cursor together with admitted work; polling never writes runtime state through config persistence. Runtime profile switching follows stop-old/commit-new ordering: reload keeps the selected identity stable, old polling/lock/bus teardown finishes while all dynamic resolvers still point at the old profile, and only then may activation expose the new token, lock key, state path, target namespace, and IPC endpoint. Downloaded attachments use UUID-prefixed names in the shared Telegram scratch directory and are session artifacts rather than identity or routing authority, so cross-profile cleanup recognizes only those UUID-prefixed scratch files. It never age-deletes journals, ownership, state, logs, or other top-level runtime files and therefore cannot redirect live traffic or orphan immutable journal segments.

When Threaded Mode is active, the current polling owner is also the Telegram bus leader. The leader owns the local bus endpoint (Unix-domain socket on Unix-like platforms, named pipe on native Windows), polls `getUpdates`, performs direct Bot API calls, records follower heartbeats, prunes stale followers, and provisions Telegram UI thread targets through live runtime/bus state. Followers heartbeat every `1s`; the leader uses a `15s` stale grace and a `1s` prune loop so transient IPC stalls do not create false routing gaps while active forwarded updates/API calls still refresh liveness. Heartbeat pruning is silent liveness bookkeeping and does not send a Telegram-visible disconnected notice, because the common cause may be leader reload or IPC handoff rather than a dead follower. Pruning alone preserves the binding; when Thread cleanup is enabled, only a subsequent OS check that confirms the exact registered PID absent may create fenced cleanup intent, and that cleanup serializes ahead of replacement registration. Successful follower target reuse refreshes the binding's recovery timestamp. Absent follower bindings remain durable restoration hints until explicit stale, deleted, offline, or reconciliation evidence invalidates them; process absence and heartbeat pruning alone do not remove them. If an authenticated live follower carries an exact target that is absent from current bindings, the leader recovers it only behind a synchronous visibility probe: success activates it, explicit stale evidence provisions a replacement, and ambiguous failure rejects registration. A carried slot is restored only when it is not already occupied. `tmp/telegram/logs.jsonl` is a session-local redacted runtime evidence stream for race debugging; it resets on extension start and runtime scope changes, and must not become routing/provisioning authority. `tmp/telegram/state.json` is an extension+bot observable/debug snapshot aligned with status diagnostics: `source: "snapshot"` and `writtenAtMs` mark it as observational, not authoritative. All instances on one Telegram profile read the same snapshot, but only the active transport lock owner persists it; followers become writers only after promotion. Status-only persistence reloads current disk bindings before serialization, preventing a stale follower/status view from erasing newer leader-owned targets. Fresh capability observations may skip redundant startup probes, but stale snapshots re-probe before suppressing bus/thread behavior. Top-level `bot` mirrors bot-wide capabilities such as thread mode, `runtime` describes process role/status, `liveRoster` mirrors followers/current targets/reservations, `diagnostics` mirrors recent status/debug signals including the latest thread-reconciler phase/counts, `threads` stores current routeable bindings, `workspaceBindings` stores profile-scoped normalized exact-`cwd` target/name/slot reuse hints, TTL-bounded reservations explain short-lived slot collision guards, and TTL-pruned `pendingProvisions` protects in-flight topic creation slots from cleanup/allocation races. Fresh provisioning writes pending state before the Bot API create call, adds the returned target to the pending record, persists a `starting` binding, then promotes it to `active` and clears pending state. If final binding persistence fails after Telegram returns a thread id, the targeted pending provision remains as cleanup/retry evidence. Once targeted pending provisions expire, they are retained for `thread-reconciler` close/delete cleanup and pending scratchpad removal after a successful cleanup apply; untargeted expired pending records can prune without cleanup because no Telegram thread id exists. Runtime events coalesce status-snapshot writes so transient bus/API/update failures remain inspectable even when the operator has not opened `/telegram-status`. The bridge must not keep a durable `telegram-targets.json` target history; stale/offline/failed thread observations are pruned instead of reused. Previous-process leader bindings that still probe alive become reservations/collision guards, not routeable active threads, so a reloaded leader can take the next free slot without duplicating the same visible tab name. The thread chat is always the private bot DM with the paired owner (`allowedUserId`). In Telegram private-chat Threaded Mode, the leader creates/reuses its own thread before polling — it is a real bound instance, not a dispatcher. Followers authenticate bus envelopes with the leader-minted capability secret stored in the active lock entry. Bot capability monitoring does not probe through the bus until the process either owns that direct lock or has completed authenticated follower registration. Leader lock entries also carry a stable `leaderEpoch` minted on acquisition and preserved across heartbeat refreshes; leader-owned cleanup/provisioning plans stamp that epoch, and Thread Reconciler apply skips destructive work if leadership has moved on before side effects run. Followers own their own Pi session state, queue, active turns, previews, menus, and lifecycle hooks, but route allowlisted, target-scoped Telegram API calls through the leader. When a follower promotes after heartbeat loss, status/state diagnostics expose only the transient `electing` lifecycle phase; stable `leader`/`follower` identity stays in the bus role so diagnostics do not duplicate role state. The TUI status bar and `/telegram-status` report `leader` or `follower` role so a registered follower is not shown as generically disconnected. Terminal status identity and the `[telegram|thread:name]` prompt label use the same target-aware current-instance resolver: registered local metadata wins over a stale shared binding for the matching target, while the binding remains a fallback for partial metadata.

Fresh follower binding is manual and process-first: the operator starts another Pi process, then runs `/telegram-connect`; only then may that process allocate a profile-scoped normalized exact-`cwd` Workspace identity and cause the leader to create a Thread. A later process reopening that remembered Workspace automatically sends capability-gated restore-only admission under a live leader. The leader may reclaim, visibility-probe, or stale-replace the remembered target, but an absent binding returns quietly without creating a Thread. `/telegram-connect [profile] as=Name` supplies a unique capitalized Latin-word identity only to fresh Workspace provisioning; an existing Workspace keeps its persisted name. Telegram `/name Name` stores the owning Thread's durable `manualThreadName` and immediately applies it over the active automatic display projection; bare `/name` opens five-minute exact-target input whose next valid text is consumed before agent dispatch. Name input, cancel, and reset are consume-once; stale scope, target, message ID, expiry, and duplicate callbacks cannot mutate. Reset clears only the override and restores the current automatic projection. Leader command routing reuses its already-held profile admission for the rename body rather than recursively entering the non-reentrant Workspace gate; standalone leader renames acquire their own admission. Followers send an authenticated exact-generation `workspace-thread-rename-v1` request, and the leader owns any Bot API mutation plus durable binding persistence. Concurrent processes from one directory receive deterministic Workspace suffixes, while leader/follower roles remain transient projections over that durable identity. Telegram does not expose `/thread`, auto-spawn arbitrary unbound threads, or launch hidden follower subprocesses. In Threaded Mode, `/telegram-connect` does not offer manual takeover while a live leader exists; takeover is reserved for stale-leader election/recovery. Leadership remains an ephemeral transport role that another live follower can take over after stale heartbeat detection. A confirmed runtime transition from Threaded to Singleton stops threaded transport and suspends the process-local leader target before classic polling can accept new work; durable Workspace slot, generated name, manual display name, and binding evidence remain retained. Re-enabling Threaded Mode restores or replaces that logical binding before publishing one new live target. Already-admitted turns keep their captured destination and are never silently retargeted or duplicated.

### Unbound Thread Detection

When Threaded Mode is enabled, writing a message in the `All` tab can create a new thread without an existing instance binding. The bridge detects this during update execution: if a message from the owner has a `message_thread_id` that no instance owns, the message is routed to the unbound-thread handler instead of the leader's normal message handler. In the default runtime, this handler first reclaims the thread for the leader when the leader has no active bound thread, assigns the current leader thread identity, persists the active binding, and serves the prompt locally. If the leader already has an active thread, the handler preserves the prompt in the source Telegram thread and shows the complete forward plus replace/restore chooser. Successful forward deletes the chooser and closes/deletes the confirmed temporary source through `thread-reconciler` proof-before-delete planning and stale-epoch fencing. Successful restore always deletes the chooser, rebinds the source thread to the selected Pi instance, and closes/deletes only that instance's replaced old thread. If foreign batch forwarding partially fails, retry sends only the remaining messages before cleanup. If Telegram cannot confirm thread or chooser deletion, the chooser becomes a cleanup-only or deletion-only retry control so already-routed content never dispatches twice and no visible button expires prematurely. Unknown `forum_topic_created` service events are recorded as observations and are not destructive cleanup proof, because Telegram can deliver creation events before local provisioning/binding writes become visible across reloads. If Threaded Mode is unavailable, the message is processed normally through classic routing.

Threadless messages from `All` are not routed as prompts once bound threads exist, because `All` cannot identify the owning Pi instance. Known commands open the same complete forward and replace/restore chooser as ordinary unbound content, while threadless ordinary prompts get guidance to use a bound Pi thread. Successful chooser publication completes the originating command update; a later button press is an independent callback update. Failed chooser publication retains the command for retry, but an untouched chooser never causes the original command to replay after restart. This prevents accidental empty tabs from black-holing prompts or bypassing the manual follower-registration contract above.

The routing identity split is deliberate:

- Live routing owner: `instanceId` from the currently registered follower/leader runtime. A live instance may have only one active bound thread; provisioning a new target removes older current-state bindings for the same `instanceId` and closes duplicate Telegram threads when possible.
- Current binding owner: explicit `owner` metadata (`leader`, `manual-follower`, or API-level pending thread creation) plus cwd/thread-name metadata; string compatibility keys are derived internally and must not be the persisted source of ownership truth.
- Instance slot: extension-owned single-letter `A`-`Z` ordering metadata. New instances advance through the alphabet and wrap after `Z` only to a free slot; live concurrent instances are capped to available alphabet slots rather than duplicating occupied letters. The compact `bot.lastSlot` cursor persists while its binding remains live/recovering, including true `Z → A` wraparound. When post-grace follower compaction removes the binding represented by the cursor, the same reconciliation pass realigns it to the newest-created remaining live binding so removed historical followers cannot dictate fresh allocation; unexpired pending provisions and reservations remain collision guards. Other thread deletion paths may intentionally preserve an orphaned cursor to continue ring sequence.
- Instance thread name: durable human-facing Workspace metadata that replaces slot-only thread titles. Fresh threads choose an unused baked 4-6 letter Latin-word name, excluding current records, dormant Workspace bindings, and pending provisions; exhausted per-slot palettes fall through the remaining curated names rather than duplicating a reserved identity. Telegram-originated prompt prefixes expose this label, never follower/leader roles or generic seeds. Renaming a live target updates its matching Workspace binding.
- Telegram destination: `TelegramTarget` as `{ chatId, threadId? }`, where `threadId` is Telegram `message_thread_id` for UI thread targets.

Guest-mode updates are owned by the current transport leader by default in Threaded Mode. Guest queries have no Telegram thread binding and no local follower identity, so the leader queues and answers them unless a future explicit guest-owner policy is added. Followers may still transport `answerGuestQuery` through the leader for replies to work if a guest turn is ever delegated deliberately, but implicit guest routing does not pick an arbitrary follower.

All inbound updates are gated by the configured authorized user id.

## Core Flows

### Inbound Turn Flow

1. Poll updates through `getUpdates` under the polling owner's request budget.
2. Validate and atomically journal each complete response batch before advancing its offset once.
3. Signal the independent source-bound worker and begin the next poll without awaiting semantic execution.
4. Run stable public raw-update handlers in registration order, then authorize and route retained built-in traffic.
5. Coalesce media groups, likely split long text, and one adjacent forward-plus-comment pair in either order when needed.
6. Download files with size limits and partial-download cleanup, then run configured/programmatic inbound handlers.
7. Build a prompt or control queue item carrying an exact durable receipt for every contributing update id.
8. Remove local prompt journal authority synchronously immediately before `sendUserMessage`, so session/process replacement can lose an unstarted prompt at that narrow crash boundary but can never replay a prompt already admitted to Pi; controls and foreign forwarding retain their explicit settlement boundaries.
9. Handle `edited_message` updates separately while the original turn is still queued and dispatch only when all safety gates are clear.

#### Durable Admission And Recovery

Here, **durable** means recovery across ordinary process exit, crash, kill, and replacement after a successful atomic rename is visible to the filesystem. It does not promise survival across host, kernel, filesystem, storage-device, or power failure: journal and offset publication do not call `fsync`/`fdatasync`, and parent directories are not flushed. A host-level failure may therefore lose a recently acknowledged rename despite correct process-level ordering. Operators requiring that stronger boundary must place the agent directory on storage with an independently managed durability/backup policy; `0.28.0` must not be described as power-loss durable.

The profile-scoped journal separates transport progress from semantic progress. Leader/classic snapshots live at `tmp/telegram/inbox[.<profile>].json`; follower paths add a stable target-binding hash. The selected post-v1 storage design is one revisioned compacted snapshot plus immutable atomic transaction segments beside it. Existing v1 files load as implicit revision `0`, while positive snapshot revisions are explicit. Immutable revision segments publish privately and atomically under the existing journal transaction lock; exact repeats are idempotent, while gaps and conflicting duplicate revisions fail closed. Each segment carries one complete mutation, and readers reconstruct ordered upserts, removals, and operator-disposition state only from revisions newer than the snapshot. Malformed or gapped segments, filename/revision disagreement, and foreign journal identity fail closed. Compatibility recovery for the former broad temp-cleanup bug rebuilds a missing snapshot when its complete revision-1 segment chain removes known base authority before any upsert and reconstructs to an empty journal, and repairs a revisionless snapshot when the first surviving segment supplies its exact positive predecessor revision and the reconstructed tail validates. If repair fails, the transaction-locked loader atomically moves the snapshot when present plus its segment directory under `tmp/telegram/recovery/`, publishes a fresh empty private journal, records an informational recovery event with the quarantine path, and continues startup; no uncertain files are silently deleted. After the initial snapshot, append, batch completion, queue receipt/owner/handoff, retry/terminal, recovery, and operator dispositions publish only changed upserts/removals and disposition replacement in one segment. This avoids rewriting retained raw updates during completion-heavy drains without splitting exact queue, failure, recovery, or disposition transactions.

Compaction runs under the journal transaction lock when either 256 unapplied segments or 4 MiB of segment bytes is reached. It publishes the complete private (`0600`) snapshot at revision `R` before best-effort deletion of segments `<= R`; failed cleanup leaves redundant segments that readers ignore. Interrupted cleanup therefore leaves either an older snapshot plus newer authoritative segments or a newer snapshot plus harmless redundant older segments. Revision gaps, conflicting duplicates, malformed segments, and identity mismatches fail closed. The logical reconstructed journal and aggregate unapplied segment bytes are independently bounded at 10,000 entries and 32 MiB as applicable; rejected growth publishes neither snapshot nor segment bytes. Compaction may temporarily require exactly one private complete snapshot of at most 32 MiB. Capacity pauses polling and valid authority files are never automatically deleted, reset, or quarantined. Only a missing-snapshot history that cannot be reconstructed safely uses the explicit evidence-preserving quarantine-and-reset compatibility fallback above.

`pending` entries remain immediately executable while raw interception, routing, or grouping is incomplete. Execution failures become `retry-wait` with durable attempt count, next eligible time, failure class, bounded summary, and latest failure time, except that an exact HTTP 400 stale/deleted Telegram thread error carrying its proven request `{chatId, threadId}` terminally settles the currently executing source after shared binding invalidation; follower settlement remains idempotent when the leader already persisted that stale binding. The `failed` state remains schema-compatible only for legacy candidate journals and is converted to automatic retry during lifecycle startup. `queued` entries carry exact prompt/control receipts plus the acquiring Pi runtime instance, OS pid/birth identity, session generation, acquisition id, and acquisition time. Queueing alone is never completion.

Queue receipt ownership is independent from the Telegram transport lock. A same-instance, same-process generation may reconstruct its local receipt across a fenced session replacement and may settle it after transport ownership moves. A different process reports the receipt as foreign, never republishes it into its local queue, and cannot complete it even if it reads the acquisition id. Startup no longer treats process replacement as proof that an owner died: foreign and legacy unowned receipts remain durable.

Cleanup and live handoff are compare-and-set under the journal transaction. Queue discard during exact queue-lifecycle cancellation requires the exact local owner/acquisition and removes all receipt sources atomically. Before admission worker start, the lifecycle groups each foreign receipt and asks the journal to recheck OS pid liveness plus process-birth identity under the same transaction; a live owner returns `owner-alive` and a live owner without stable birth proof returns `owner-unverifiable`, both without mutation, while exact negative proof atomically discards the complete session-owned receipt without replay. Replacement registration carries its exact pid/process-birth before this check; when registration and cleanup race, that live identity wins the liveness proof and the queued receipt remains untouched. A replacement or unrelated worker therefore never executes queue work whose prior owner is proven dead.

Authenticated live handoff uses journal CAS plus bounded local IPC. The donor creates a one-time high-entropy token and durably offers the complete receipt to one exact recipient runtime/process/session identity; the journal stores only a digest bound to queue kind, receipt sources, donor acquisition, and recipient identity. While offered, donor completion/discard and dead-owner recovery fail closed, so authority cannot disappear during payload transfer. Prompt payloads carry all queue fields; control payloads carry only their stable `status`/`model` identity and rebuild executable closures locally. The separately negotiated `queue-handoff-v1` capability gates this envelope for leader and both peer generations. Each receipt carries its exact source journal binding; the donor derives the recipient follower-journal binding from the authenticated stable follower profile before routing. The bus validates payload shape/size and exact donor/recipient registration generations, and the recipient selects only that matching active lifecycle, stages one complete receipt idempotently, accepts the journal CAS, and returns the exact receipt plus newly minted owner in its ACK. Malformed, legacy-unbound, inactive-generation, or unavailable bindings fail closed.

During recipient staging, presenting the token atomically replaces the journal owner with a fresh acquisition carrying the handoff digest, removes the offer, and permanently fences donor settlement. The donor treats only an ACK carrying that exact accepted owner as success and never repeats acceptance against a donor-bound journal runtime. The recipient can repeat the same acceptance idempotently; a different token cannot claim an already accepted receipt. The coordinator contract orders offer → stage/accept exact receipt-and-owner ACK → donor removal → recipient readiness for direct leader→follower and follower→follower routing. Before acceptance, negative or mismatched acknowledgement exactly cancels the offer and keeps donor work. After acceptance, a lost acknowledgement cannot roll authority back: cancellation fails closed and donor memory remains frozen until exact accepted-owner reconciliation removes it. Recipient registration carries exact process-birth/session identity, and staged payloads remain outside the live dispatch store until accepted journal authority has been reconstructed. Production advertises `queue-handoff-v1` only with this exact role/journal selection and uses the same coordinator ordering for direct leader→follower and follower→follower routes.

Queued semantic authority has no elapsed-time lease. A timeout cannot prove either owner death or effect quiescence, so it cannot safely resolve a receipt. Resolution is limited to authenticated live handoff, exact owner discard/settlement, or transaction-rechecked negative PID plus process-birth evidence that permits terminal cleanup without replay. Live or unverifiable owners remain queued rather than risking duplicate or cross-session execution.

The initial `offset: -1` cursor bootstrap is allowed only when both cursor and journal are absent or empty. Thereafter process-level ordering is journal atomic rename → one monotonic offset atomic rename → worker signal. Failure before journal publication leaves the offset unchanged; failure after journal publication but before offset publication permits Telegram redelivery and journal dedupe; failure after offset publication but before worker signal replays from the journal on restart. Queue-owner, retry, terminal, handoff, and completion transitions use the same journal publication primitive and therefore share this process-crash boundary. The final completion window is at-least-once, so replay-sensitive external effects must use `update_id` or the stable delivery id as an idempotency key.

Threaded Mode forwarding is a two-journal handoff. Only peers that mutually advertise protocol v1 and `durable-follower-admission-v1` may route or become election-eligible. The follower validates its exact binding and registration generation, durably appends the source-bound delivery, and only then returns the exact receipt. The leader classifies each attempt as `accepted`, `retryable`, or `terminal-rejected` with its delivery identity and failure class; only `accepted` with the expected `deliveryId` and `sourceUpdateId` may complete leader journal authority. Missing, negative, stale-generation, or mismatched-receipt acknowledgements remain durable, and a callback error answer is only an operator-facing side effect.

Delivery ids derive only from envelope kind, source `update_id`, and stable recipient binding. Live registration generation remains a separate attempt fence, while callback/reaction message ownership carries that stable binding and rebinds to its current authenticated follower registration after replacement. Lost acknowledgements therefore replay idempotently into the same follower journal identity without changing the delivery id. Package build skew is allowed only while protocol version and capabilities remain compatible.

Worker execution ownership is per `update_id` across same-runtime generations. Aborting a generation ends its authority but does not prove its handler settled; replacement replay remains blocked on that exact settlement. Late success and failure are both diagnostic events. Public and built-in handlers receive the same optional execution fence (`signal`, generation/update identity, and pre-effect assertion); the runtime binds it non-enumerably to every internal update carrier and checks it before routing-plan effects. Prompt construction rechecks after downloads and inbound handlers before queue mutation, pairing rechecks around persistence, command/menu and extension-command delegation retain the source fence across detached effects, lifecycle sync rechecks after store load before reconciliation, and reroute clones carry the source fence through forwarding, thread replacement, cleanup, persistence, and Bot API rename boundaries. Legacy handlers remain source-compatible but must not commit unfenced late effects.

The canonical update transition contract is:

- `pending → executing`: the generation-local worker selects an unclaimed source; `executing` is a runtime phase, not a separately persisted entry state.
- `executing → completed | queued | pending | retry-wait`: exact local completion removes the entry, queue admission persists its receipt, deferred grouping retains replay authority, and every execution failure persists retry evidence.
- `retry-wait → executing`: only after `nextRetryAtMs`; repeated signals before eligibility do not execute the entry. Automatic retries continue indefinitely with exponential `1s → 2s → 4s → 8s → 16s → 32s → 60s` delay capped at `60s`, while later independent updates continue draining.
- Legacy `failed → retry-wait`: startup atomically resumes terminal entries written by earlier `0.28.0` candidates. Runtime policy never silently discards durable inbound authority and exposes no Pi command for manual retry/discard.
- `queued → offered → staged → queued`: only the exact persisted donor may offer or cancel a live handoff; an offer preserves donor ownership but freezes ordinary settlement and recovery. Authenticated bounded IPC stages one exact payload/receipt outside the live queue. Exact recipient acceptance mints a fresh acquisition, reconstructs local ownership, removes donor work, then publishes recipient dispatch readiness.
- `queued → completed`: only the exact persisted owner receipt may complete or discard queued sources; generic completion rejects queued state. Process-birth-proven owner death atomically discards the complete unoffered session-owned receipt without replay; live, unverifiable, or offered owners remain queued.

The worker executes at most 64 eligible entries from one validated journal snapshot, commits ordinary completions through one journal transaction, then yields through a generation-checked event-loop boundary. Retry, queue, or prior-generation boundaries first flush completed ids and force a fresh snapshot, preserving exact state-transition atomicity without per-entry parse/rewrite churn. A deterministic 2,048-entry stress gate requires exactly 32 completion publications, 33 reads including the final empty snapshot, continued 1ms timer progress, and less than 250ms maximum observed heartbeat delay. Byte-capacity tests cover failed and retry-wait diagnostics, queue receipt/owner and handoff metadata, and operator dispositions; every rejected growth leaves the prior authority bytes unchanged. It still scans later independent entries after retry or terminal persistence. An unresolved reaction remains a queue-mutation dependency even in `retry-wait` or `failed`, but dispatch checks that dependency against the candidate queue item's exact chat and source message ids instead of globally blocking unrelated targets. Successful replay or an exact discard disposition releases the dependency. Worker state, debug status, state snapshots, and redacted runtime events expose journal depth, retry/terminal counts, the next retry, latest terminal identity, copyable operator commands, and the exact first foreign queued owner identity (instance, PID/birth, session, and acquisition) when semantic authority belongs to another process.

The journal is the sole polling/admission authority. Each atomic journal revision publishes the admitted batch and monotonic `acceptedThroughUpdateId` together; cursor-only initial synchronization uses an empty batch revision. Existing config cursors are transferred once before polling: journal publication precedes config removal, restart retries are idempotent, established journal authority never regresses, and an unprovable non-empty journal fails closed. Upgrades create journals lazily before the first post-upgrade offset advance. A bot/profile identity change with unresolved authority fails closed. Once reconstructed authority is empty, the next read atomically rebinds profile and bot identity under the journal transaction and removes redundant old-identity segments best-effort; stable-`botId` token rotation remains valid even with entries. Downgrading below `0.37.0` with a cursor-schema journal is unsafe because an older runtime cannot recover `acceptedThroughUpdateId` and could repoll admitted updates. Run `node scripts/check-downgrade.mjs [agent-dir]`; the conservative older-schema validator rejects that retained authority even when entries are drained. Runtime state from an older release must recover without deleting `telegram.json`, ownership state, or journal authority.

Polling and inbound-worker diagnostics remain separate so an executing, deferred, locally queued, foreign-queued, or blocked journal head cannot masquerade as a stalled `getUpdates` request.

Long-text split recovery remains conservative: only human text at or above the near-limit threshold opens its debounce window. Forward annotation has two semantic layers: the forward owns its source text/caption/media, while an optional separate owner-authored annotation normally precedes it. A bounded one-second pairing window joins that annotation and adjacent forward in either transport order, including a media-only forward without source caption text; the matching opposite-kind message flushes immediately. Same-kind rapid messages, commands, bots, ordinary non-forward captions, media groups, different senders/targets, reversed ids, and distant message ids do not enter this pairing path. Prompt construction always places the owner annotation first, followed by `[forward|from:...]` with the forward's own source text/caption, then source-attributed forwarded attachments, regardless of arrival order.

### Queue And Dispatch Safety

The bridge keeps its own Telegram queue. Queue items have two explicit dimensions:

- `kind`: `prompt` or `control`.
- `queueLane`: `control`, `priority`, or `default`.

Dispatch rank:

1. `control` lane.
2. `priority` prompt lane.
3. `default` prompt lane.

#### Priority, Reactions, Keep, and Skip

Waiting prompts expose two independent dimensions:

- `Priority` / `Normal` selects the FIFO lane and therefore scheduling order.
- `Keep` / `Skip` selects whether the prompt executes when dispatch reaches it.

A prompt is one queue object with exactly one active lane membership and one current position. It has no duplicate, shadow entry, or reserved return slot in the other lane. Each prompt admitted directly to a lane joins that lane's tail. A `Normal → Priority` transition removes it from Normal and appends that same object to the Priority tail; a later `Priority → Normal` transition removes it from Priority and appends it to the current Normal tail rather than restoring any historical position. The immutable `queueOrder` records original admission identity only and is never a return address; `laneOrder` records the current destination-lane position. Keep/Skip changes and emoji changes within the same reaction category preserve both lane and lane position exactly.

Telegram reactions are shortcut controls over those dimensions. Positive reactions (`👍`, `⚡️`, `❤️`, `🕊`, `🔥`) control Priority; negative reactions (`👎`, `👻`, `💔`, `💩`, `🗑`) control Skip. The runtime compares the complete old and new reaction sets and mutates only categories that changed, so adding or removing a negative reaction cannot silently change Priority, and changing a positive reaction cannot silently change Skip. The listed order selects the retained display emoji when several recognized emoji from one category coexist; it does not let one category override the other.

Priority and Skip may coexist, including `👍 + 💩`. The prompt remains at its Priority-lane position while waiting, and its durable journal receipts remain intact so Keep stays reversible and exact live handoff remains possible. Skip wins when dispatch reaches the prompt: the dispatcher first settles those receipts durably, then drops it without a model turn and continues; settlement failure retains the skipped head instead of allowing replay ambiguity. The prompt stays visible with only its negative emoji, is excluded immediately from the executable queue count shared by the Pi status bar and Telegram main menu, and keeps a struck-through physical ordinal. Returning it to Keep restores its contribution to the count without moving it. Graceful session shutdown discards all remaining queue receipts before clearing session-local memory, and startup discards receipts only after proving their former process dead, so a new or unrelated session never inherits queued work. Queue item detail exposes symmetric Priority/Normal and Keep/Skip selectors instead of an irreversible Delete action.

Menu and reaction controls share the same canonical queue state. A menu Keep can clear internal Skip without changing Priority or queue position, but Telegram's Bot API cannot remove a reaction created by the user; the visible user reaction can therefore remain until that user removes it. Once Pi has consumed or dropped a prompt, later reactions cannot retract or restore it.

Admission and planning validate lane contracts. Invalid lane/kind pairings fail predictably instead of being silently coerced.

Dispatch requires:

- No active Telegram turn.
- No pending Telegram dispatch already sent to Pi.
- No compaction in progress.
- `ctx.isIdle()` is true.
- `ctx.hasPendingMessages()` is false.

A dispatched prompt remains queued until `agent_start` consumes it. This keeps the active Telegram turn bound for previews, attachments, aborts, and final replies. A low-level `agent_end` error also retains that active turn because Pi may retry automatically; a later successful `agent_end` delivers through the original target and metadata, while `agent_settled` proves that an unrecovered error can be finalized once before queue dispatch resumes.

Post-agent-end queue dispatch uses a session-bound deferred dispatcher. It is activated on session start, clears timers on shutdown, and skips callbacks from older generations before touching `ExtensionContext`. Dispatch stays session-bound after polling ownership moves elsewhere. When a queued Telegram prompt is forwarded into Pi, the bridge synchronously commits its exact durable receipt before calling normal `sendUserMessage(content)`; failed receipt commitment blocks dispatch, while the unavoidable crash window between commitment and Pi admission favors at-most-once execution over replay. It does not use Pi's `followUp` delivery option or inject terminal input.

One monotonic session generation also fences agent/tool/message events, compaction callbacks, preview state, scheduled final delivery, controls, and shutdown. Distinct Pi context objects observed within one session adopt that generation; contexts already observed under an older generation remain stale after replacement. Session start invalidates pending preview work, delayed finals check their captured context before delivery, and shutdown rechecks after asynchronous polling/preview boundaries with a bounded preview-clear wait.

For a configured Rich response with final text and exactly one supported queued PNG/JPEG, MP4, or MP3 artifact, queue orchestration asks `outbound-attachments` for one reply-anchored multipart Rich result before finalizing ordinary text. A successful result clears the preview, records exact message ownership, and suppresses duplicate text/file delivery. A known-safe rejection returns to the established paths; an ambiguous send stops the turn without fallback or replay. HTML mode, multiple or unsupported files, Guest Mode, and all voice-policy outputs bypass this optimization.

### Controls And Menus

Telegram controls execute through command/callback domains, not by entering the normal prompt queue unless they intentionally create a prompt turn. Built-in read-only menu commands are admitted once required local state mutation finishes: first-user pairing still persists before `/start` is accepted, while menu rendering and BotFather command synchronization run as context-fenced best-effort effects with diagnostic failure sinks. Their unresolved Telegram calls therefore cannot retain the durable polling offset or prevent the next `getUpdates` request. Detached effects, deferred dispatch/watchdog, typing, and diagnostics callbacks contain primary and diagnostic failure; stale typing context is ignored, while snapshot publication serializes one write plus one retained coalesced rerun. Raw companion handlers still run before durable built-in routing and should return quickly even though their execution no longer retains polling.

Immediate controls:

- `/start` opens the main inline application menu.
- `/model`, `/thinking`, `/queue`, and `/settings` are hidden shortcuts to menu sections.
- `/compact` opens an inline confirmation dialog and then runs compaction when the bridge is idle.
- `/next` dispatches the next queued turn, aborting Pi first when needed. When an active Telegram turn is aborted, its single Pi-aligned informational notice replies to a pre-abort snapshot of that turn; otherwise the command message is the fallback target. Aborted pending assistant text is not projected as a second reply, while already completed intermediate output remains visible.
- `/abort` aborts active work while preserving queued items. Abort-history preservation is enabled only for Telegram-owned active turns; later local/non-Telegram agent starts clear stale abort-history mode so the next Telegram prompt appends instead of absorbing old queued turns as history.
- `/stop` aborts and clears waiting Telegram queue items.

Queued controls:

- `/continue` creates a priority Telegram-owned `continue` prompt.
- Prompt-template commands expand Telegram-safe Pi template aliases before entering the prompt queue.
- Model-switch continuation uses the control lane when an in-flight Telegram-owned run must be stopped and resumed.

Queue and menu mutations are reachable through Telegram updates handled by the current polling owner. After ownership moves, the old instance keeps processing its accepted local queue, but it no longer receives new menu callbacks or control updates for remote mutation. UI label, navigation, tab, toggle, card, and dialog rules are defined in [UI Style](./ui-style.md). Callback prefix ownership is defined in [Callback Namespaces](./callback-namespaces.md).

### Compaction And Typing Status

Manual `/compact` requires inline confirmation because accidental taps are disruptive. Confirmed manual compaction and auto-compaction both set the bridge compaction flag, block queued prompt dispatch, retain that flag in explicit diagnostics, and clear it on native compact completion or failure, timeout fallback, or session shutdown. Pi owns its terminal compaction lifecycle; pi-telegram keeps `Active` scoped to Telegram-owned work and otherwise preserves the stable connected/leader/follower role. Mid-run threshold compaction reports notices in place between tool output and the next assistant response; compaction observed after terminal assistant output waits for final Telegram delivery so transport chronology matches the terminal.

Native typing during compaction follows connected-instance activity rather than terminal status:

- Confirmed manual `/compact` starts a native `typing` keepalive in the command target and stops it on completion/failure.
- Automatic/session compaction with an active Telegram turn reuses that turn's target.
- Automatic/session compaction without an active Telegram turn uses the connected instance's assigned target; an unconnected instance sends nothing.
- Thread-targeted typing is sent to the concrete thread and mirrored to `All` as the aggregate activity surface; completion, native failure, timeout, and shutdown stop the keyed loop.
- Pi `ui_prompt_start` pauses typing while an extension-owned local prompt waits for the operator; `ui_prompt_end` emits the matching Activity boundary and resumes typing whenever agent or compaction work remains unsettled.

At every connected instance `agent_start`, the lifecycle binding starts Telegram's native `…typing` indicator in that instance's assigned target, whether the run came from Telegram, the local TUI, or an autonomous continuation such as Grow Loop. Terminal `Active` remains Telegram-turn-specific; the native indicator answers the separate question of whether the instance is doing agent work. Each loop keeps one action in flight, while the leader API runtime coalesces identical chat/thread/action calls across local and follower traffic for two seconds; expired gates prune opportunistically and at most 256 currently active keys are retained. A Telegram 429 response opens the exact action's shared `retry_after` suppression window without scheduling delayed retries or projecting expected activity throttling as a terminal status error. Assistant message start/update hooks still re-arm it during Telegram-owned turns so transient provider/model errors do not leave a continuing run without activity feedback, and agent/session completion stops it.

### Rendering And Delivery

Rich Markdown is the default model-answer membrane. Complete assistant replies send final Markdown directly as `InputRichMessage.markdown` through `sendRichMessage` when `assistant.rendering` is `rich`, and through the legacy Markdown-to-HTML renderer when `assistant.rendering` is `html`; guest replies use native Rich Markdown through `InputRichMessageContent` in `answerGuestQuery` results. Reasoning/thinking blocks, menus, status rows, queue controls, settings, diagnostics, and other harness-owned surfaces stay on explicit Telegram HTML/plain rendering, while completed tool activity uses native Rich block objects for visually distinct structured disclosure. Streaming previews may use `sendRichMessageDraft` only when `assistant.draftPreviews` is enabled and draft delivery succeeds. The bridge still strips top-level assistant action comments before delivery and may split output only for Telegram transport limits.

Assistant delivery guarantees:

- Model-authored Markdown is the source of truth; the bridge does not pre-render assistant Markdown to HTML unless the operator selects `assistant.rendering: "html"` for compatibility.
- Before native Rich Markdown delivery, the bridge normalizes known Bot-API-fragile source forms without changing visible meaning, including space-after-marker blockquotes and dollar-prefixed ticker atoms that Telegram may otherwise treat as unterminated math.
- Prompt context blocks use compact metadata (`[tag|key:value]`) as the stable inbound contract. `[telegram...]` names the current surface only: owner/current turns use `[telegram]` or `[telegram|thread:<name>]`; guest-mode turns use `[telegram|guest:<group-title-or-peer-username-or-id>]`. In a private Guest Mode turn the paired owner's `from` identity is never the guest: the remote private-chat identity wins, then non-owner caller metadata, with a non-bot replied peer available only as a final identity fallback when stronger conversation evidence is absent; username falls back to the remote display name and numeric id. Reply attribution still belongs independently in `[reply|from:...]`, and a replied bot can never define or replace the current `[telegram|guest:...]` location identity. Source authors for quoted/forwarded material and their files are carried by `[reply|from:<username-or-id>]`, `[forward|from:<username-or-id>]`, and `[attachments|from:<username-or-id>]`, while plain `[attachments]` remains current-turn attachments and is ordered before reply/forward/source context. Media embedded in inbound Telegram `rich_message` blocks is downloaded like ordinary message media and stays attached to its forward-source block instead of being mislabeled as current-user material. Guest-mode turns append a `[guest]` block to their turn text stating the one-reply/limited-window constraint and instructing a fast, concise, self-contained answer; the note travels with the turn text rather than the system prompt.
- Quoted rich replies use Telegram `rich_message` blocks as the prompt-context source when available, so `[reply]` context receives rendered plain text instead of raw `InputRichMessage.markdown` fallback text. Replied media runs through the same inbound handlers and voice transcription providers as current-message media, with provenance-scoped `[outputs|from:…]` appended inside the reply block.
- Long native Markdown replies are split only at Telegram Rich Message transport limits; oversized fenced code, display-math, and fully wrapped inline-formatting blocks are rewrapped per chunk so persisted Rich Markdown chunks remain structurally valid.
- When Draft previews are enabled, streaming previews pass structurally closed assistant Markdown prefixes through to `sendRichMessageDraft` with ownership checks, voice and guest-turn suppression, and serialized flushes. Telegram Guest Mode allows exactly one answer that cannot be patched afterward, so guest turns never start preview state even while `assistant.draftPreviews` is enabled. Unclosed inline spans, links, fenced code, comments, and display-math blocks are held back until a safe boundary exists. Draft failures are recorded and the failing frame is skipped instead of degrading to raw plain-message previews, because partial Markdown can be invalid while the final message remains valid.
- Preview flushes are serialized so older edits cannot race newer drafts; final delivery waits for active draft flushes and does not perform a post-final draft-clear call. Successful final text delivery clears the local pending preview text only while the captured session and transport remain active, so a late delivery or Rich-attachment cleanup cannot erase replacement preview state.

UI/compat rendering guarantees:

- Bridge-owned UI surfaces such as tool rows, reasoning/thinking blocks, commands, menus, status messages, queue controls, diagnostics, settings, and interactive sections use Telegram HTML/plain rendering helpers by default. These texts are authored for operational UI rather than model output, so explicit HTML/plain markup remains clearer, safer, and easier to maintain.
- In those UI/compat surfaces, real code blocks stay literal and escaped, supported absolute links stay clickable, unsupported links degrade safely, tables use compact monospace rendering with grapheme/display-width accounting, and list/quote/heading spacing stays Telegram-safe.

Final delivery attaches reply metadata only where requested. Reply parameters apply only to the first chunk of split messages; continuation chunks are adjacent normal messages. Media-group turns reply to the representative message id.

### Outbound Artifacts And Assistant Actions

Outbound files staged during an active Telegram turn are delivered after that turn completes but before any separate final text. Final delivery clears an existing preview first so an edited older message cannot appear above a later upload. Files use `telegram_attach`, are checked atomically per tool call, and use configurable size limits before photo/document upload. When no Telegram turn is active, `telegram_attach` sends files immediately to the paired/default chat, an assigned follower thread, or an explicit `chat_id` plus optional `thread_id`; `telegram_message` provides direct local/TUI Markdown text delivery for explicit user requests and runs the same `telegram_button` markup planner so buttons attach to that text message. Direct local/TUI delivery is singleton-controlled: classic mode requires this Pi instance to own `/telegram-connect`, while Threaded Mode followers must be registered and route through the leader-owned transport. Already accepted active-turn reply/attachment delivery remains session-local.

The channel-post journal records only this agent path's own publication intent. `prepare` is exact and idempotent; `beginPublication` durably changes one prepared operation to outcome-unknown before any future non-idempotent send; only `confirmPublished` records the returned numeric channel/message identity. A retry never regains issuance from outcome-unknown or published state. The store validates profile and token fingerprint, refuses malformed/duplicate/over-capacity state, and lists newest retained records without claiming Telegram history. The public-`@username` direct-leader sender now uses the tool-call ID to prepare and begin this journal transition before `sendRichMessage`, then records the returned numeric channel/message identity plus bounded username/title observed by `getChat`. Retained outcome-unknown state refuses automatic replay. Bounded agent-facing listing is available locally. Exact edit/delete journal transitions fence each tool-call mutation as outcome-unknown before direct-leader `editMessageText` or `deleteMessage`, and require the same mutation ID to confirm edited or deleted state. Numeric-channel publication requires explicit `channel: true`; direct-leader `getChat` must prove the exact negative ID and channel type before issuance, and the send response must preserve that identity. Cross-process regressions prove one winner for concurrent publication/edit/delete issuance and no restart regrant. Reads fail closed before parsing links, foreign/loose files, unsupported no-follow platforms, identity races, or oversized input. No legacy post schema is migrated: absent state starts empty and unknown versions/fields fail closed. The 0.44 downgrade checker leaves this inert journal untouched because old code cannot issue its effects. Only an explicit successful list returns retained authored Markdown. Channel publication/list/mutation failures and runtime events use fixed messages without retained content, token, path, or arbitrary transport detail. Production-helper regressions model a lost successful caller ACK and an ambiguous send response: the same operation returns retained success or refuses outcome-unknown without a second transport call. The retry uses a freshly opened replacement store, proving durable behavior across helper replacement/restart. A native entrypoint regression disconnects/reconnects direct ownership and proves both retained success and ambiguous outcome avoid a second `sendRichMessage`. Sender-rights and live rollout evidence remain separate 0.45.0 work. Channel media posts upload one local `.jpg`/`.jpeg`/`.png`/`.webp` photo or `.mp4` video through the multipart transport with `text` as the HTML caption; kind, byte size (photo ≤ 10 MiB, video ≤ 50 MiB), and 1024 visible caption characters are validated before issuance, and unsupported types or albums are rejected rather than downgraded to links. The journal binds the inspected kind/file name/byte size/SHA-256 media identity and caption, so duplicate requests and lost acknowledgements never re-upload. A media-post edit replaces the caption through `editMessageCaption` with the same Markdown-to-HTML rendering, where `||spoiler||` now renders as `<tg-spoiler>`.

Assistant-authored final-message actions use hidden top-level comments, with an additional fenced wrapper for in-body buttons:

- `telegram_voice` accepts one positional compact action cell or JSON object and creates one voice artifact through configured outbound handlers, programmatic voice handlers, or registered synthesis providers.
- `telegram_button` accepts a JSON object, adaptive JSON/CML matrix, or positional Compact Matrix Literal. Named JSON objects and positional cells may coexist, with commas optional only between completed matrix or row elements. Each top-level cell creates one full-width row, while a nested row groups buttons horizontally without an artificial parser-width cap; every callback enqueues its configured prompt text as a normal Telegram prompt turn. The JSON-first grammar, positional trim/escape rules, atomic rejection, and renderer-owned width policy are specified in [Adaptive Button Literal](./compact-matrix-literal.md).

Standalone column-zero triple-backtick `telegram_button` blocks reuse the same cell/matrix grammar and callback store, compiling to native Rich Markdown button rows between paragraphs. Rendering validates each complete block before registering any callbacks, escapes label text, preserves disabled cells, and leaves larger enclosing fences literal. Previews hide complete and unfinished action fences. HTML mode projects those controls into the footer. In-body callbacks acknowledge without rewriting the Rich message; selected-style highlighting remains footer-only.

Action recognition remains restricted to top-level column-zero comments and exact-name button fences so nested examples cannot trigger voice, buttons, or callbacks. The Telegram surface independently strips every complete assistant-authored HTML comment from previews and final delivery regardless of Markdown position or comment owner; an unclosed comment is withheld through the remaining tail, and a comment-only result sends no text message. Pi's terminal transcript and model context remain unchanged.

Unknown callback data outside owned prefixes is forwarded as `[callback] <data>` only after built-in and extension handlers decline it.

### Generative Apps

The `generative-apps` Skill owns the general Generative Apps concept, application shapes, and hybrid method/prompt model. This section records only how the `pi-telegram` runtime composes that concept into the bridge.

The bridge owns canonical `<agent-dir>/genapps/<app>/<app>.mjs` identity, installation/replacement, bounded runtime ports, state revision/timeline integrity, `app::method` routing before Pi queue admission, ordinary prompt routing through Pi, and Telegram delivery of returned Markdown/buttons. External capability ownership remains outside the bridge.

`telegram_bind` is the agent-facing lifecycle surface. Installation and explicit replacement invoke mandatory `init`; replacement stages and initializes a complete candidate before publishing it under the same app name. During an active Telegram turn, successful Tool output is planned and delivered directly to that exact target by default, and the Tool result suppresses model duplication; `display: false` retains agent-only diagnosis. Outside an active turn no implicit target is chosen. Buttons emitted as `app::method` or `app::method(<strict JSON>)` take the inference-bypass route. Missing apps, malformed actions, stale revisions, invalid output, or method failure fail closed without becoming ordinary prompts.

The concrete runtime and wire reference remains in [Generative Apps Runtime For Telegram](./generative-apps.md). The bundled `generative-apps` Skill owns the concept and agent workflow; `generated-control-surface` owns its separate ephemeral interface protocol; `telegram-bridge` owns transport, target authority, delivery, and general turn operation.

## Extension Surfaces

`pi-telegram` intentionally owns one `getUpdates` loop per bot. `polling` owns that internal loop; `updates` owns classification/default-routing plans plus the public handler registry layered extensions use to observe or consume updates without opening a competing polling connection. Layered extensions should integrate through extension surfaces instead of polling the same bot independently.

- Raw update observation/consumption: [Updates](./updates.md).
- Telegram-native slash commands: `registerTelegramCommand()` from [Public API](./public-api.md#commands).
- Target-aware operational views and chat actions: [Telegram Delivery API](./delivery.md).
- Normalized non-blocking Pi lifecycle events: [Telegram Activity API](./activity.md); the separate [`pi-telegram-extension-demo`](https://github.com/llblab/pi-telegram-extension-demo) project remains the companion-extension reference.
- Structured inline UI sections: [Sections](./sections.md).
- Callback namespace discipline: [Callback Namespaces](./callback-namespaces.md).
- Voice/STT/TTS providers: [Voice Integration](./voice.md).
- Inbound/outbound command-template handlers: [Command Templates](./command-templates.md).

Extension callbacks must avoid `pi-telegram` owned prefixes such as `compact:`, `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `settings:`, and `section:`. Workflow-specific Telegram slash commands should use the public command registry instead of becoming new core built-ins unless they are bridge lifecycle, transport ownership, queue safety, or essential operator controls.

The bridge does not mirror arbitrary `ctx.ui.confirm/input/select/custom` prompts from other extensions into Telegram. Companion extensions that need Telegram operation should expose a Telegram-native command, section, settings row, callback, status line, inbound/update handler, or assistant action-markup path instead of relying on hidden TUI-only prompts.

## Diagnostics And Operational Behavior

Status rendering distinguishes connected, active, dispatching, queued, tool-running, model-switching, and compacting states; the Telegram status menu gives compaction precedence over generic active or pending work. Its compact Tokens row shows only input and output totals, while the adjacent Cache row groups `R` cache-read tokens, `W` cache-write tokens, and `CH` for the latest assistant request's cache-read share of prompt tokens rather than a misleading cumulative-session ratio; the labels remain distinct from companion-provided usage limits. Observed automatic compaction sends the same start and completion notices as the manual command without duplicating notices for command-owned compaction. If no terminal hook arrives, the current observation times out after five minutes: it releases only observer-owned status/typing, records a diagnostic, and requests a guarded queue-dispatch recheck. Timeout is not proof that Pi compaction completed or was cancelled and emits no invented terminal notice. Superseded timeout callbacks and stale-context terminal hooks cannot abandon or cancel a newer observation. If a queue mutation removes the last waiting item while Telegram-owned work still has running tools, status remains active instead of degrading to connected.

Queue reaction behavior, lane-tail transitions, Keep/Skip independence, multi-reaction precedence, and the Bot API reaction-removal limitation are defined in [Priority, Reactions, Keep, and Skip](#priority-reactions-keep-and-skip). Reaction changes first flush a matching delayed text or media group so the governed turn exists before mutation, and dropping marked heads cannot leave status permanently queued.

`/telegram-status` records grouped diagnostics for transport/API, polling/update, prompt dispatch, controls, typing, compaction, setup, session lifecycle, attachment queue/delivery, and recent redacted runtime events. Polling diagnostics expose the exact phase, phase start, current update, last successful `getUpdates` response, and stop reason; outbound success never substitutes for inbound progress. Expected preview noise such as unchanged edit responses is filtered out. The compact TUI status renders only `error`; detailed failure text remains in diagnostics and profile-scoped logs instead of expanding the status line.

Complete intermediate assistant text blocks from Telegram-originated activity are sent once to the immutable originating target before active-turn final delivery; final and terminal-partial segments stay with settlement so replies are not duplicated. While this instance has exact direct or follower transport authority, completed public blocks from local/autonomous work are always sent once and in source order to the instance's authorized target. Connected companion projection is not configurable; disconnect or authority loss is its boundary. Both paths use the configured Rich or HTML renderer and exclude reasoning, tool traffic, token deltas, local prompt text, unknown sources, and stale generations. Each admitted block remains fenced to its exact target, profile/token stamp, leader epoch or follower registration generation, and session generation; non-idempotent acknowledgement ambiguity never authorizes replay.

`assistant.activity` is an independent bridge-owned projection over normalized Activity events. Each process reloads the shared file-backed setting at `agent-start` before activity admission, so multi-instance mode cannot continue projecting a stale broader process-local selection. Omitted values resolve to `verbose`, while invalid values fail closed to `quiet`; `thinking` and `tools` select one technical class, while `verbose` enables both. Provider-exposed thinking uses persistent ordinary HTML containing only a standard expandable blockquote with a bounded redacted latest-text window and inline Markdown rendered as Telegram HTML. Completed executed tools use native Rich Messages: each closed `<Tool>: <status>` root details node renders snake-case names as title words while preserving an uppercase two- or three-letter repeated prefix per word, then the native disclosure chevron reveals an open-by-default `arguments` child plus closed retained `update N` and `result`/`error` child details with lowercase monospaced, marker-free summaries and JSON pre blocks; known-safe Rich rejections fall back to the previous HTML disclosure. The projection captures the exact target and transport stamp at activity admission, serializes updates, preserves tool-start order, closes coalescing across assistant/thinking boundaries, bounds retained text/update memory plus edit frames and message/tool size, disables previews and HTTP(S) auto-link recognition inside technical evidence, and never replays a possibly committed send. Session generations own independent queues, so replacement drops queued old work without waiting on an old call. Bridge-owned assistant output and activity projection share one activity-publication sequence admitted synchronously from the activity bus, so later tool disclosures cannot overtake earlier assistant blocks. Activity targets and transport authority are captured before queued work starts. Active Telegram-turn final replies/artifacts and automatic compaction notices use that same publication owner before the existing direct/follower transport split, without mutually waiting on separate output tails. Final settlement captures the exact active turn and assistant result and reserves its publication position before config loading. Empty outcomes without a publication candidate reserve no position. Replacement, preparation failure, or an unused reservation releases the position without resetting or replying for a replacement turn. Final errors also publish through this owner. Reservations accept one task; cancellation cannot undo a published task, and session reset releases unresolved reservations while fencing old queued tasks. Terminal assistant messages with a publication candidate reserve the final position synchronously; settlement consumes that reservation only for the exact originating turn. Compaction notices enter the same queue immediately, behind the reserved final rather than through a separate notice buffer. Settlement, a new agent run, or session replacement cancels an unconsumed reservation. Notice target and authority are captured when the event is observed. Pi lifecycle completion does not wait for network publication. This order is process-local and does not serialize unrelated instances or independent registered activity handlers. Settlement, replacement, disconnect, failure, or stale authority clears only local ownership; already-sent activity messages remain in chat.

Telegram prompt guidance is context- and authority-aware. The package and source-checkout extension contribute `telegram-bridge`, optional `generated-control-surface`, and `generative-apps` Skills through Pi resource discovery. Generated Control Surface treats `interface = f(state, capabilities, intent)` as a renderer-neutral primitive, compiling transient evidence-backed controls over domain-owned workflows, systems, navigation, supervision, and decisions without creating parallel application state. It composes an ordered ragged sequence of independently sized semantic rows rather than filling a rectangular grid: compact rows contain genuine peers, singleton rows isolate structurally independent actions, and rectangular layouts remain reserved for genuinely spatial state. Text-bearing controls use at most two columns and flow into additional rows, while denser rows are reserved for short position-bearing glyphs or codes and never exceed the eight-column phone-width UX maximum. Vertical extent is independent: a true spatial surface may retain substantially more rows, while non-spatial button walls route to grouping, disclosure, or pagination. Symmetry is treated as an evidence claim about equal relationships or real spatial topology; an abstract layout catalog supplies adaptable singleton, peer, staged, navigational, repeated-pair, and rectangular shapes without forcing tasks into preset grids. Repeated controls carry the smallest sufficient action delta when visible conversation is unambiguous; larger or error-prone state moves to a deterministic task-owned Markdown artifact, correctness-sensitive transitions move to a small domain-owned transition implementation, and repeated clicks are adjudicated against current state rather than stale button appearance. Its filesystem adapter reserves the first full-width row for parent traversal outside root, places available Previous/Next controls together in one compact row immediately afterward, orders visible directories, hidden directories, visible files, and hidden files alphabetically within each category before fixed ten-entry pagination, renders path/range metadata as stacked status-style key-value rows instead of middle-dot prose, emits the complete Telegram control set through one JSON-matrix action, suppresses duplicate plain/monospaced listings and default Refresh unless user preference overrides presentation, and retains an ordinary numbered fallback when buttons are unavailable. Only an exact direct owner or live registered follower exposes the two pi-telegram delivery tools, their active-tool metadata, and the compact routing suffix. Disconnect or authority loss removes those tool surfaces for subsequent requests without touching foreign tools; reconnect/recovery restores only the pi-telegram subset that was active before suspension, including across same-process reload. Repeated stable interactions may graduate from that model-mediated surface into a reviewed Generative App whose deterministic bound methods bypass Pi queue admission; the `generative-apps` Skill owns this compilation and operating workflow while the underlying capability retains domain authority. Telegram-originated turns route to the stable Skill contracts and retain dynamic blocks such as `[voice] delivery: automatic voice`; the Skills and public documentation own syntax, target routing, Threaded Mode behavior, Generative App operation, and diagnostics.

## In-Flight Model Switching

When `/model` is used during an active Telegram-owned run, the bridge can emulate Pi's interactive stop/switch/continue workflow:

1. Apply the selected model immediately.
2. Queue or stage a synthetic Telegram continuation turn.
3. Abort the active Telegram turn immediately, or wait for the current tool to finish before aborting.
4. Dispatch the continuation after abort completion.

This is limited to Telegram-owned runs. If Pi is busy with non-Telegram work, the bridge refuses the switch instead of hijacking unrelated activity.

## Shutdown And Timer Lifecycle

`session_shutdown` is the hard boundary for session-bound runtime work. It suspends Telegram polling through the locked polling runtime, aborts the poll controller, stops native typing, unbinds deferred queue dispatch, suspends pending media/text-group debounce work for rebinding to the replacement session, clears preview state, clears active turns, and drops the active abort handler.

Non-critical timers are `unref()`ed so print/headless processes are not kept alive only by Telegram housekeeping. This includes typing keepalive intervals, bounded typing-idle waits, deferred queue dispatch, media/text-group debounce windows, preview flush timers, and polling retry sleeps. Polling retry sleep is abort-aware, so shutdown does not wait for the normal retry delay after a polling error.

Non-interactive `pi -p` runs must remain passive unless Pi provides a live Telegram session lifecycle. Loading the extension with `telegram.json` or existing lock state must not by itself keep the print-mode process alive or let a non-owner send companion Telegram output.

## Related

- [README.md](../README.md)
- [Project Context](../AGENTS.md)
- [Project Backlog](../BACKLOG.md)
- [Changelog](../CHANGELOG.md)
