Mobile companion boundary: Telegram extends a running Pi session; it is not a remote terminal, PTY supervisor, process launcher, session browser, or replacement TUI. Never emulate Pi navigation through private internals, ANSI/TTY injection, or a shadowpiprocess.Runtime safety: Prefer explicit, fenced, recoverable behavior over shortcuts that can desynchronize Telegram transport, durable admission, local queue state, or Pi lifecycle state.Pi-native extensibility: Add capabilities through stable Pi and pi-telegram contracts. Do not fork polling, transport, menu ownership, or package-private runtime internals.Bidirectional binding: Treat Pi instance ↔ Telegram thread and bot ↔ client state as two-way relationships. Create, observe, repair, and reflect bindings on both surfaces.Progressive enhancement: Use richer Telegram/Pi capability when proven available and retain a useful fail-closed fallback when it is not.Boundary clarity: Keep Telegram transport, Pi integration, rendering/delivery, durable admission, extension APIs, and release/context state under distinct owners.
pi-telegram is a session-local Telegram runtime adapter for Pi: a private-DM operator surface for prompts, streaming previews, queue controls, settings, files, voice/buttons, and companion-extension interop. Its core loop is mobile continuation of a live Pi session.
Canonical terms:
Telegram turn: One Telegram input unit processed by Pi, including a coalesced media group.Queued/active Telegram turn: Accepted-but-not-running / currently bound Pi work.TelegramTarget:{ chatId, threadId? }; classic private chats omitthreadId.Thread: Product term for Telegram's tabbed private-chat surface. Usetopiconly for Bot API primitives.Leader/follower: The process owninggetUpdatesand direct Bot API transport / a registered process routing through that leader.Instance slot: Extension-ownedA–Zordering metadata, not the normal visible thread title. Naming and allocation details live indocs/multi-instance-bus.md.
Keep each fact in one authoritative layer:
README.md: Public product entrypoint. Preserve the flow identity → install/connect → examples → product model → compact capabilities → controls/safety → docs. Balance strong positioning with a practical catalogue; neither hide capabilities nor duplicate implementation docs.AGENTS.md: Stable engineering boundaries, recurring runtime invariants, and work protocol. Link to evolving subsystem contracts instead of copying them here.BACKLOG.md: Canonical unresolved work. Keep only open top-level outcomes with nested decomposition and done criteria. Remove completed outcomes rather than retaining checked history.CHANGELOG.md: Completed user/operator/developer impact. A release has at most eight outcome bullets of at most 512 characters, each beginning with an inline-code domain label and colon. Exclude personal names and real user/chat/message/thread identifiers. Consolidate the current pre-release section before release; do not rewrite historical sections without an explicit retrospective request and evidence pass.docs/README.md: Technical documentation index.docs/architecture.md: Canonical runtime, domain-ownership, queue, journal, delivery, and lifecycle contract.docs/public-api.md: Canonical public commands, config, markup, package entrypoints, and compatibility contract.docs/multi-instance-bus.md: Canonical Threaded Mode, leader/follower, binding, election, and transport protocol.- Other
/docsfiles own their named subsystem contracts; keep them reachable fromdocs/README.md.
/index.ts: Sole extension entrypoint and composition root./api/*.ts: Stable public package membranes documented indocs/public-api.md./lib/*.ts: Flat, cohesive runtime domains; package-private unless re-exported through/api./tests/*.test.ts: Domain-mirrored suites;tests/integration.test.tsowns cross-domain runtime flows./skills/telegram-bridge: Stable agent operating protocol for Telegram turns, delivery, actions, Threaded Mode, and diagnosis./skills/generated-control-surface: Optional state-derived, late-bound interface over truthful domain evidence, capabilities, workflows, and choices; it remains renderer-neutral, independent from the bridge skill, and owns no parallel state./skills/generative-apps: Agent operating contract for compiling stable repeated Telegram interaction into deterministic standalone applications or bounded view/controller adapters whose buttons bypass model inference./.agents/skills/telegram-bot: Bot API lookup guidance and vendoredapi.md; keep the reference intact./.agents/skills/domain-dag: Repository architecture guidance and validator.
Use the relevant local skill before non-trivial work in its domain. Keep skill operating guidance in its SKILL.md, not duplicated here.
- Cohesive domains live as flat
/lib/*.tsmodules whose local import graph is acyclic. index.tsconstructs high-level runtimes and wires live ports. Domain policy, mutable state, sequencing, identity, retries, normalization, and lifecycle recovery belong to the owning/libmodule.- Extract only when ownership, substitution, independent testing/mutation, cycle pressure, or repeated coupling earns a boundary. Do not atomize a cohesive module or create one-use wrappers merely to shrink
index.ts. bindingsowns Pi-facing registration and narrow cross-domain assembly; it may connect established ports but must not absorb routing, rendering, transport, or mutable policy.piowns direct Pi SDK imports and concrete adapter contracts. Other domains use narrow ports; domains that register Pi hooks/tools/commands consume contracts through that adapter.- Do not introduce shared buckets such as
lib/constants.ts,lib/types.ts,lib/globals.ts, or broad global-augmentation modules. Keep state, constants, registry keys, and concrete transport shapes with their domain owner. - Every source
.tsfile starts with a brief responsibility header containingZones:tags such astelegram,pi agent,tui, orshared utils. - Use namespace imports for local domains in
index.ts(Queue.*,Turns.*) and keep directnode:*, filesystem, process, and local-adapter mechanics in owning domains when one exists.
- The bridge is session-local and paired to one allowed Telegram user. Preserve
{ chatId, threadId? }through every inbound, queue, callback, reaction, media, preview, reply, menu, voice, attachment, and direct-delivery path. - Telegram transport ownership is not semantic queue ownership. Losing the exact transport lock must not erase accepted local queue work or stop valid local Pi dispatch; direct Bot API mutations fail closed until exact direct or follower authority exists.
tmp/telegram/owners.jsonis the sole transport-owner authority. Cross-process read/check/write operations serialize transactionally and acquisition, refresh, release, takeover, and irreversible leader work fence the exact owner/epoch.state.jsonandlogs.jsonlare diagnostics, never routing authority.- Threaded Mode has exactly one live leader per bot profile. Followers are real operator-started Pi processes and must authenticate/register over local IPC; Telegram never spawns hidden Pi processes. A live but unreachable owner does not authorize split-brain polling.
- Local IPC is a trust boundary, not merely a private socket. Unknown, stale, mismatched-generation, or unauthorized requests must not inject prompts, callbacks, API sends, artifacts, liveness, or bindings.
- Protocol compatibility is independent from package version. Registration negotiates protocol version, runtime build, and canonical capabilities before target provisioning or live publication.
durable-follower-admission-v1gates source forwarding;queue-handoff-v1independently gates semantic queue transfer for every participant and is advertised only with exact source/recipient journal-binding composition.follower.registeris the sole bootstrap request; other requests require exact live-registry generation authority, andbus.ackis response-only. - Long-lived timers, pollers, watchers, receivers, heartbeats, background delivery, and deferred dispatch are session-bound. Replacement stops stale activity and makes late work inert; same-process handoff may preserve exact profile/target identity but never stale Pi context or cross-profile authority. Aborting a durable update generation does not release that
update_id: replacement replay waits for its actual handler settlement, and effectful handlers use the shared execution fence immediately before commit and after awaited delegation. Internal clones explicitly carry the hidden fence; reroute forwarding, thread-store mutation, cleanup, and Bot API boundaries retain the originating generation. - Runtime state is event-driven reconciliation of local assumptions against Telegram signals, not a complete bot read-model and not permission to query Telegram on every action. Destructive thread cleanup goes through
thread-reconcilerwith current proof and leader fencing.
- Admission is journal-first: validate and persist the complete
getUpdatesresponse before one monotonic offset commit, then signal an independent worker without awaiting semantic execution. Missing cursor with a non-empty journal, malformed/foreign authority, or capacity exhaustion fails closed. “Durable” means process-crash recovery after atomic rename, not unflushed host/kernel/filesystem/device/power-loss survival. - Foreign forwarding settles as
accepted,retryable, orterminal-rejected. Only an authenticated acknowledgement carrying the expecteddeliveryIdandsourceUpdateIdreleases leader journal authority. Negative, missing, stale, mismatched, or capacity-failed settlement remains durable; callback error answers are side effects only. - A forwarding delivery id is stable across registration replacement and derives from envelope kind, source
update_id, and stable recipient binding. Runtime instance and registration generation remain separate attempt fences. Persisted message ownership carries the stable binding so replay can rebind only to its current authenticated registration. - A queued receipt persists its acquiring runtime instance, OS pid/process-birth identity, session generation, acquisition id, and acquisition time. Only exact authority may settle or discard it. Same-process session replacement may reconstruct the claim and the original process may settle after transport ownership moves; a foreign process may neither replay nor settle it through generic removal or a copied acquisition id.
- Startup and elapsed time are not owner-death proof; queued authority has no time lease. Dead-owner cleanup groups the complete receipt and transactionally rechecks pid liveness plus process-birth identity: only an absent PID or mismatched stable Linux/macOS birth proof discards all session-owned sources without replay; a matching proof is
alive, while Windows or inaccessible birth metadata isunverifiable, and both non-dead outcomes keep authority queued. The live-transfer contract is authenticated offer → exact-generation bounded payload staging → recipient CAS acceptance → exact receipt-and-owner ACK → donor removal → recipient readiness. The offer freezes donor settlement/recovery; controls rebuild local closures; negative/mismatched pre-acceptance ACK cancels only an unaccepted offer and retains donor work; a lost post-acceptance ACK cannot cancel recipient authority and leaves donor memory frozen for explicit reconciliation. - Execution failures persist bounded diagnostics and attempt state as
retry-wait. Automatic retry continues indefinitely with exponential1s → 2s → 4s → 8s → 16s → 32s → 60sdelay capped at 60 seconds; later independent updates continue draining, durable authority is never silently discarded, and legacyfailedentries resume automatically at startup. Snapshot-plus-segment journals compact only after 256 unapplied revisions or 4 MiB; snapshot-first cleanup tolerates redundant segments, and empty authority may atomically rebind bot/profile identity. Missing snapshots left by the retired broad temp cleanup rebuild only from a complete provably empty segment chain, while revisionless snapshots may recover from a validated later segment predecessor; otherwise the snapshot and segments move atomically undertmp/telegram/recovery/before a fresh journal is published and startup continues with informational recovery evidence. - An unresolved reaction delays only the exact governed queue item identified by chat/message sources, not unrelated queue work. Queue receipt publication follows in-memory append and precedes dispatch request; receipt-bearing turns remain queued until every exact source commits.
- The detailed implementation and release gates live in
docs/architecture.md,docs/multi-instance-bus.md, andBACKLOG.md.
- Queue lane/kind admission is explicit. Dispatch waits for active-turn, pending-dispatch, control, compaction,
ctx.isIdle(), and Pi pending-message guards; a dispatched prompt stays queued untilagent_startconsumes it. Each prompt is one object with one active lane and no reserved return slot. Normal and Priority are separate FIFO lanes: crossing lanes removes it from the source and appends it at the destination tail, while Keep/Skip and same-category emoji changes preserve lane position. Complete reaction sets independently derive Priority from recognized positive emoji and Skip from recognized negative emoji; both may coexist, suppressed turns retain durable receipts while waiting, and Skip settles them only when the prompt reaches dispatch before dropping it without inference. Suppressed turns remain visible at a struck-through physical ordinal without contributing to executable queue counters, while graceful session shutdown discards all remaining queue authority before clearing memory. /stop,/abort,/next, and/continuerespectively reset+abort, abort while preserving queue, force the next turn, and enqueue a control-lane continuation. Abort-history folding applies only to Telegram-owned active turns.- Telegram extension side effects must not hold Pi's core lifecycle hostage after semantic completion. Preserve ordering in extension-owned background work, record failures, and fence target/profile/transport/session authority.
- Complete assistant/guest model answers use Telegram-native Rich Markdown. Harness-owned menus, status, diagnostics, thinking, and tool evidence remain explicit HTML/plain or their documented native surface. Preserve literal code and structurally safe chunking; never split invalid markup.
previewowns streaming lifecycle only, not assistant rendering. Finalization waits for active preview flushes and must not issue pre/post-final draft-clear calls that create transient Telegram draft UI.- Native
sendChatAction(typing)is the automatic activity signal. Do not invent extra in-chat work indicators or emit activity for startup/connect/reload/recovery alone. - Public activity handlers and companion delivery are asynchronous, target-bound, generation-fenced surfaces. Token deltas, hidden reasoning, unknown sources, and stale authority never enter public projection.
- UI labels, emoji semantics, navigation, settings controls, callback namespaces, voice behavior, command templates, and assistant markup follow the linked
/docscontracts; do not restate those evolving details here.
The detailed map is canonical in docs/architecture.md. This index is only for routing work:
queue,runtime,lifecycle,locks: Scheduling, session coordination, lifecycle, and locking.api,polling,bus*,ownership,target,sync,thread-reconciler,threads,updates,routing,media,turns,inbound,config,setup: Telegram transport, profiles, durable admission, routing, and inbound flow.preview,replies,rendering,keyboard,delivery,activity,outbound*,voice,status: Response and delivery surfaces.commands,menu*,model,prompts: Controls and application-menu UI; core queue mechanics remain inqueue.sections,delivery,activity,voice: Extension registries/runtime membranes for their named capabilities.Companiondescribes consumers, not a source-domain owner.pi,bindings: Pi SDK boundary and Pi-facing registration/composition.
- Companion extensions use documented package subpaths such as
@llblab/pi-telegram/sections,/delivery,/voice,/inbound,/outbound, and/updates; never importlib/*.ts. - Low-level handler buses have no caller-supplied ids; high-level registries use stable identities. Imperative delivery resolves the current runtime on every call and returns generation-bound logical handles rather than captured Pi contexts.
- Extension sections receive only documented context ports. They do not access raw bot clients/filesystems or run a second polling loop; unregister on shutdown.
- Unknown callback data may reach extension handlers only after built-in namespaces decline it. Follow
docs/callback-namespaces.md. - Command templates remain compact and shell-free. Use string leaves or ordered
templatearrays; shell operators are not an execution contract. Examples use portable executable placeholders, never machine-local paths. telegram_attachis the canonical file path andtelegram_messagethe direct Markdown text/buttons path. Both require current direct or registered-follower authority and must not replace the normal active-turn reply.- Inbound handlers transform text/media before queueing; outbound handlers precede programmatic/provider fallbacks. Public contracts and ordering live in
docs/inbound.md,docs/outbound.md, anddocs/public-api.md. - Pi integration uses public hooks and APIs. A Telegram
/newor equivalent session replacement requires a public Pi API that executes the real terminal path.
- Keep comments and user-facing docs in English. Comment non-obvious rationale/contracts, not names or standard idioms.
- Name flat modules by bare domain (
queue.ts,queue.test.ts);telegram-api.tsis the intentional transport exception. Tests primarily protect their mirrored module; shared fixtures require real cross-suite reuse. - Keep interfaces consistent with their owning exported contract. Use local structural
*Like/view types only for deliberate narrow projections, not duplicate source-of-truth models. - Remove dead code immediately. Reachability from composition roots, public exports, tests, registered surfaces, and documented APIs—not recent usefulness—determines whether code is live.
- Treat every meaningful
index.tsedit as a composition-pressure check, but keep one-off live adapter wiring there when extraction would only hide cross-domain state. - Follow
docs/ui-style.mdfor interface copy, emoji, buttons, menus, and dialogs. Update the registry before assigning a new UI emoji meaning. Standalone notices use one fully bold emoji-led heading with terminal punctuation; callback alerts preserve equivalent emoji-led plain text because Telegram does not support rich formatting there. - Markdown tables use compact source formatting with
---separator cells and one surrounding space per cell. Preserve vendored references unchanged. - Treat Windows filesystem, named-pipe, lock, heartbeat, and atomic-rename reports as high-signal evidence; reduce them to regressions or explicit platform caveats.
- Route significant runtime failures through the redacted recent-event recorder. Keep the compact TUI status at generic
error; details belong in diagnostics.
Before non-trivial work:
- Read
README.mdfor current product behavior and positioning. - Read
BACKLOG.mdbefore runtime or documentation changes. - Read the relevant indexed docs; read
docs/architecture.mdbefore architecture, queue, preview, rendering, lifecycle, or command restructuring. - Inspect the owning module, its callers, mirrored tests, and the relevant
index.tswiring before editing. - Run an
AGENTS.mdcompliance pass for implementation, release, and architecture work; update an obsolete rule instead of silently working around it.
While working:
- Keep changes inside this repository; updating an installed Pi checkout is a separate operator action.
- Read large artifacts search-first and range-bounded. For
CHANGELOG.md, inspect only the current release section unless older history is relevant. - Keep successful validation output compact; inspect focused failure tails. Prefer focused tests/typecheck during iteration and broad validation at a stable gate.
- Preserve unrelated work and do not commit, publish, tag, deploy, or perform external actions without explicit authorization.
Before completion:
- Run the smallest decisive validation for the affected closure. Queue/rendering/lifecycle changes normally require
npm run typecheckandnpm testat the stable gate. - For Domain DAG changes, run
SKILL_DIR=.agents/skills/domain-dag bash .agents/skills/domain-dag/scripts/validate-domain-dag.sh --root .. - Keep strict unused-local/parameter checking. Validate queue dispatch around abort, compaction, pending dispatch, and Pi pending-message guards; validate rendering around literal code, nesting, and long-message chunks.
- When context files change, run the ABCd context validator and review warnings rather than relying on exit status alone.
- Sync
README.md,CHANGELOG.md,BACKLOG.md, and relevant/docsonly when behavior, shipped impact, open-work truth, or durable contracts actually changed. - Do not call a release ready until its canonical backlog gates and required platform/live evidence are complete.